mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-06 20:17:00 +00:00
Compare commits
877 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
410aec734a | ||
|
|
6078b17d38 | ||
|
|
3b36d607ac | ||
|
|
156cd85474 | ||
|
|
0e29798e2b | ||
|
|
19dbe81c79 | ||
|
|
62838961bb | ||
|
|
ee84162470 | ||
|
|
7a76dcce2c | ||
|
|
0c21ed2e31 | ||
|
|
39daa763ab | ||
|
|
f0c9690152 | ||
|
|
450091e71f | ||
|
|
3a5c64b877 | ||
|
|
949d96db7a | ||
|
|
c974ce8864 | ||
|
|
caab8e9bbb | ||
|
|
12469ee6f9 | ||
|
|
dcd90b6f70 | ||
|
|
f92b3c3d16 | ||
|
|
f3b41783c5 | ||
|
|
3ec33c6054 | ||
|
|
eb7c0da40c | ||
|
|
2817c0027e | ||
|
|
c7457b95d1 | ||
|
|
05244d598c | ||
|
|
f55e0b2cb0 | ||
|
|
ed6256da2c | ||
|
|
0398137ba7 | ||
|
|
a6448a2769 | ||
|
|
71cf0d01f8 | ||
|
|
d2a6d7ee99 | ||
|
|
a07bc15f00 | ||
|
|
52ebd35785 | ||
|
|
636d24a117 | ||
|
|
8afbfaeb31 | ||
|
|
117084f835 | ||
|
|
eae92d8638 | ||
|
|
31f5df9db4 | ||
|
|
00419a4cfc | ||
|
|
3f36e5b2e8 | ||
|
|
cc2abc961e | ||
|
|
bea0398776 | ||
|
|
fd4c55ba9d | ||
|
|
fef5edce28 | ||
|
|
6423e4cb68 | ||
|
|
2acaf1f7ff | ||
|
|
fe31b078f6 | ||
|
|
e3d81f4a61 | ||
|
|
fb1d1d8fc5 | ||
|
|
cabef5906c | ||
|
|
2dfffe00d0 | ||
|
|
78390efe45 | ||
|
|
8ae0d2da88 | ||
|
|
ecb3273883 | ||
|
|
256b94f877 | ||
|
|
422f02b5c9 | ||
|
|
7e4c31db1a | ||
|
|
c9918c07b8 | ||
|
|
ed438b1a31 | ||
|
|
83843cfe46 | ||
|
|
462de7b025 | ||
|
|
01dbc183f8 | ||
|
|
8817c82996 | ||
|
|
3493addec5 | ||
|
|
d41a588bb5 | ||
|
|
7953705e30 | ||
|
|
763a1ad96f | ||
|
|
b58d958f51 | ||
|
|
0b8d43f734 | ||
|
|
87518f33a9 | ||
|
|
d83530bbf6 | ||
|
|
517e1bab7c | ||
|
|
cd22552e4a | ||
|
|
abc394512c | ||
|
|
fa0cb01660 | ||
|
|
e6d509bd0b | ||
|
|
775641aee1 | ||
|
|
3c51c6bc77 | ||
|
|
9ddd7b1d32 | ||
|
|
6020a01a62 | ||
|
|
1ad9ee10f6 | ||
|
|
59863b8a91 | ||
|
|
1a083c247c | ||
|
|
cd7ca31212 | ||
|
|
9be9ff3d68 | ||
|
|
e3b1ead830 | ||
|
|
b6041592ea | ||
|
|
0fa8b3da0b | ||
|
|
7dd0fe07ac | ||
|
|
2d1770f439 | ||
|
|
6be192514a | ||
|
|
63fc42f8cf | ||
|
|
6f8a593f09 | ||
|
|
b00e8f8900 | ||
|
|
4e226ebcb7 | ||
|
|
30909c497b | ||
|
|
efd5ccfa96 | ||
|
|
88d159d4c6 | ||
|
|
29788c353b | ||
|
|
7897272f65 | ||
|
|
5cabc76782 | ||
|
|
b0ef416a4f | ||
|
|
0de0768bfb | ||
|
|
e4ac128a2e | ||
|
|
2307f21d04 | ||
|
|
89d706c94f | ||
|
|
23350a0ffe | ||
|
|
65d047f831 | ||
|
|
dc40045de6 | ||
|
|
50d91ea6d8 | ||
|
|
3a8865e382 | ||
|
|
fddcef284f | ||
|
|
c0854250e4 | ||
|
|
71c9501354 | ||
|
|
a85d0f91a2 | ||
|
|
977514f5ef | ||
|
|
bf9ad953a1 | ||
|
|
5b1d96f649 | ||
|
|
bf4ac0c9f3 | ||
|
|
3c8b2534eb | ||
|
|
16c9f46583 | ||
|
|
ef9609b710 | ||
|
|
a4c15992a9 | ||
|
|
14d1173a0c | ||
|
|
1e7acbdbd2 | ||
|
|
35e729b25c | ||
|
|
4ee5ea78e3 | ||
|
|
fe971f9f67 | ||
|
|
a828523f26 | ||
|
|
af9ca6788e | ||
|
|
36d1c3af63 | ||
|
|
1853e98209 | ||
|
|
f526d43798 | ||
|
|
ae8c76cecb | ||
|
|
2b32c94543 | ||
|
|
b6f5909df1 | ||
|
|
ef35bbbb63 | ||
|
|
64f2cc6c7e | ||
|
|
a16635eb26 | ||
|
|
6e614f0a7d | ||
|
|
859f4eab0a | ||
|
|
104e0b9079 | ||
|
|
612abbf5c0 | ||
|
|
2dfbf71806 | ||
|
|
c43f4d129d | ||
|
|
8d47a48f62 | ||
|
|
9a333e6e38 | ||
|
|
f6e0ef8cc6 | ||
|
|
36a45717f1 | ||
|
|
86f63d72e1 | ||
|
|
ade4078f0d | ||
|
|
fdeac68216 | ||
|
|
4c220a5a65 | ||
|
|
f148bd82c5 | ||
|
|
0d983ef34c | ||
|
|
75ad9f5cf2 | ||
|
|
893efe212e | ||
|
|
4a4d2e473f | ||
|
|
9d663ad140 | ||
|
|
f377dfc5b1 | ||
|
|
a4054f5273 | ||
|
|
fcc3ac978f | ||
|
|
2bbeaa8198 | ||
|
|
70308d19ae | ||
|
|
143897cf18 | ||
|
|
ddc90a05d9 | ||
|
|
bbbadb1c32 | ||
|
|
8995835d8e | ||
|
|
df3c119f5f | ||
|
|
6a2ba73412 | ||
|
|
cbee8cc541 | ||
|
|
22f186af29 | ||
|
|
77ab385db8 | ||
|
|
13495ee32e | ||
|
|
254ce7d5d2 | ||
|
|
4e1053d037 | ||
|
|
2fcc6e168f | ||
|
|
96ea0ae690 | ||
|
|
aa66001981 | ||
|
|
173ff943a4 | ||
|
|
79ac73bb95 | ||
|
|
2143832c25 | ||
|
|
c656e36265 | ||
|
|
8564adca6a | ||
|
|
19c9922115 | ||
|
|
b22a6f204a | ||
|
|
daa0cf9fc9 | ||
|
|
6365aa04df | ||
|
|
ff20e077b1 | ||
|
|
1106aaf18e | ||
|
|
872ba225b4 | ||
|
|
95c15efa92 | ||
|
|
c179cec8a0 | ||
|
|
97f96396c5 | ||
|
|
e05cac1261 | ||
|
|
f9d8894f37 | ||
|
|
5bd5a76722 | ||
|
|
2fbb4f3725 | ||
|
|
61e1b18eb9 | ||
|
|
08c1710326 | ||
|
|
39d3baf1b5 | ||
|
|
fd4753d190 | ||
|
|
29745367f7 | ||
|
|
25bb4f08ba | ||
|
|
40a13c16eb | ||
|
|
8ad2c425bf | ||
|
|
2b6c9bc57e | ||
|
|
bebbee5abd | ||
|
|
99156499f1 | ||
|
|
9ad81581bb | ||
|
|
1b4cd1478b | ||
|
|
5fdf2e4403 | ||
|
|
319b523117 | ||
|
|
62823646b0 | ||
|
|
3c16758605 | ||
|
|
4705d2e38d | ||
|
|
05b405f76e | ||
|
|
f54b2788de | ||
|
|
6ac0c7b301 | ||
|
|
c44cc9906e | ||
|
|
ecd5671e1a | ||
|
|
c83f729eea | ||
|
|
10afd2dfef | ||
|
|
8229b8320f | ||
|
|
50493cf725 | ||
|
|
3937b80254 | ||
|
|
bcb7aaf13c | ||
|
|
f9d7ec7f47 | ||
|
|
b7d43edf22 | ||
|
|
0f0060f0f6 | ||
|
|
036bad3912 | ||
|
|
c9d675798b | ||
|
|
10703750f9 | ||
|
|
4d942851d3 | ||
|
|
1434c31b2f | ||
|
|
74405b2842 | ||
|
|
1070d18085 | ||
|
|
b729c13c7a | ||
|
|
1c99327bfd | ||
|
|
61279b0e59 | ||
|
|
31cb17a41d | ||
|
|
f9349d5545 | ||
|
|
c3703494eb | ||
|
|
d94ff7c4fa | ||
|
|
01e0bf4d6d | ||
|
|
ca5f1a47fd | ||
|
|
54af8ee7c6 | ||
|
|
8572b565f3 | ||
|
|
ea1cc1c65a | ||
|
|
4610c61bd4 | ||
|
|
02f04ea9c4 | ||
|
|
1ce4db6be4 | ||
|
|
888291a99d | ||
|
|
3f9773a4cf | ||
|
|
a223c721aa | ||
|
|
5bce57b1f8 | ||
|
|
3666c34845 | ||
|
|
0dceccac46 | ||
|
|
59a2644e0f | ||
|
|
6c33a7304d | ||
|
|
e89f01a525 | ||
|
|
7181b45461 | ||
|
|
b7ce52f476 | ||
|
|
d2a8cd961f | ||
|
|
c152dbf4bf | ||
|
|
594a111bc3 | ||
|
|
005be2fe5a | ||
|
|
8f194e6d9e | ||
|
|
e3a5cb464c | ||
|
|
779f0df83e | ||
|
|
086d9bdbb6 | ||
|
|
9dcaa46bc7 | ||
|
|
3bca95a4da | ||
|
|
be09935287 | ||
|
|
410c338da8 | ||
|
|
be0606ede6 | ||
|
|
f54a84420a | ||
|
|
6a0d1e3b8b | ||
|
|
33127cd1bb | ||
|
|
5cd03312e1 | ||
|
|
391c0dcaf8 | ||
|
|
0b7b9599d3 | ||
|
|
b45ff69376 | ||
|
|
1836d0a127 | ||
|
|
048e156e59 | ||
|
|
a85be60e30 | ||
|
|
90d5c9eca6 | ||
|
|
1c8f0c7d9c | ||
|
|
f8d1c9ce05 | ||
|
|
28052bac23 | ||
|
|
c361a940eb | ||
|
|
fbc1e936db | ||
|
|
84661f88fd | ||
|
|
10b4c88872 | ||
|
|
10ddc7d941 | ||
|
|
b1c63e2cb9 | ||
|
|
3f6544d03b | ||
|
|
a9f211dfd5 | ||
|
|
f2427abbf4 | ||
|
|
a6187f9838 | ||
|
|
384600ed95 | ||
|
|
6d06f03ef9 | ||
|
|
0829c714fd | ||
|
|
9e2251755f | ||
|
|
436f871be2 | ||
|
|
8d09f8e9b2 | ||
|
|
e4d43a6790 | ||
|
|
bb3d9d3466 | ||
|
|
bd208e259c | ||
|
|
1a1cfec3e5 | ||
|
|
1e95e98549 | ||
|
|
82056b5650 | ||
|
|
c0d118ee98 | ||
|
|
da13177292 | ||
|
|
4cc38ca564 | ||
|
|
fa945c981e | ||
|
|
63d76c6b71 | ||
|
|
9b749a8f67 | ||
|
|
1ce1d5c79a | ||
|
|
0c2dd73334 | ||
|
|
7331f14e0a | ||
|
|
d44f2fd7cd | ||
|
|
4da7eddc69 | ||
|
|
3ebe30a9c0 | ||
|
|
e959361df9 | ||
|
|
7773506361 | ||
|
|
9db652a501 | ||
|
|
27ebfd7f23 | ||
|
|
96f80e0008 | ||
|
|
5f3c208161 | ||
|
|
d402f33c74 | ||
|
|
879da4dca2 | ||
|
|
3e862790e6 | ||
|
|
425e36bff6 | ||
|
|
705ecbdf72 | ||
|
|
c658d67ac8 | ||
|
|
c46055b469 | ||
|
|
66e4b89d1a | ||
|
|
38b9f1d68c | ||
|
|
19b8a4e771 | ||
|
|
937ffea4ab | ||
|
|
1ae49a3d47 | ||
|
|
c76daf50db | ||
|
|
9e5c797243 | ||
|
|
662c4afcdf | ||
|
|
9b50b97e79 | ||
|
|
d00cefc37f | ||
|
|
1f94c13e25 | ||
|
|
75801ea717 | ||
|
|
b845f5086e | ||
|
|
8c8e6ec551 | ||
|
|
a4aecee18d | ||
|
|
3c0d445824 | ||
|
|
c36c6dc42d | ||
|
|
f7f8f14725 | ||
|
|
9402bf4baf | ||
|
|
12fcdcdb2d | ||
|
|
61735ed4aa | ||
|
|
3c65d252b6 | ||
|
|
6423859195 | ||
|
|
53df34d6f2 | ||
|
|
ed762c9be3 | ||
|
|
f05f14b12a | ||
|
|
183b5cb29b | ||
|
|
07193b1fb2 | ||
|
|
b24f8f538b | ||
|
|
a259174415 | ||
|
|
3d93c2a4b1 | ||
|
|
8c112791ef | ||
|
|
0ff4553c94 | ||
|
|
1e6094b5ee | ||
|
|
a646d97aec | ||
|
|
57f4980802 | ||
|
|
2477f5b696 | ||
|
|
6463422e81 | ||
|
|
3daf041698 | ||
|
|
933456ecdf | ||
|
|
ab18f72f69 | ||
|
|
28465cf2bf | ||
|
|
4cef5c547e | ||
|
|
057abf8024 | ||
|
|
20de5658e9 | ||
|
|
4a2ed2e0f7 | ||
|
|
3dc574bc48 | ||
|
|
d43a42b646 | ||
|
|
823c46a6df | ||
|
|
919c0bc8ae | ||
|
|
54d62a19cd | ||
|
|
18304fb710 | ||
|
|
cc1afbed5d | ||
|
|
a195acfee9 | ||
|
|
e22ec4c20b | ||
|
|
7c8f4f9db8 | ||
|
|
d28ff1e438 | ||
|
|
5af7499fdb | ||
|
|
6588d783cc | ||
|
|
f1b8279d50 | ||
|
|
252d31ffac | ||
|
|
8a98cca4fa | ||
|
|
0b8486c1a7 | ||
|
|
b0bd0664f9 | ||
|
|
286fd7e0ce | ||
|
|
42f5d28369 | ||
|
|
ea5d873cc3 | ||
|
|
ee8608469e | ||
|
|
f541625318 | ||
|
|
ee70fb8483 | ||
|
|
f79e093f52 | ||
|
|
254d9280b9 | ||
|
|
ddd9c765b8 | ||
|
|
080f204eed | ||
|
|
f30325849a | ||
|
|
8caa5b3e60 | ||
|
|
0daeed49c1 | ||
|
|
34540e12f3 | ||
|
|
b954a81630 | ||
|
|
56472d6746 | ||
|
|
2721649313 | ||
|
|
24d7de60ab | ||
|
|
441f9296ad | ||
|
|
3116a152e7 | ||
|
|
77784c4224 | ||
|
|
bf13c7ca8c | ||
|
|
26c779a2c5 | ||
|
|
1bfaf6c3ad | ||
|
|
69229ff6af | ||
|
|
719164524b | ||
|
|
5a67b17916 | ||
|
|
db4968b1ca | ||
|
|
e50f46e38a | ||
|
|
adc8988f6c | ||
|
|
a480ad9c11 | ||
|
|
d9be685f89 | ||
|
|
bd9db140de | ||
|
|
74c0204590 | ||
|
|
ce9d3299b1 | ||
|
|
8b27bc194d | ||
|
|
1a6b4a6683 | ||
|
|
2ae4d6e090 | ||
|
|
9467171c84 | ||
|
|
402db729b7 | ||
|
|
e16be12438 | ||
|
|
605912443d | ||
|
|
62acc14f4a | ||
|
|
017066a311 | ||
|
|
273b122ca8 | ||
|
|
ce2ec3dd6c | ||
|
|
fda22605cc | ||
|
|
1076fa6230 | ||
|
|
019af59c8a | ||
|
|
08bb644b58 | ||
|
|
6ec1273e61 | ||
|
|
ba5df9a4ae | ||
|
|
050d77ef12 | ||
|
|
f8ccaeab08 | ||
|
|
3c4c464e4a | ||
|
|
2a5730def9 | ||
|
|
48fe3e6b86 | ||
|
|
eecbc3aa2c | ||
|
|
e177f14446 | ||
|
|
493ab0396a | ||
|
|
c776150ce0 | ||
|
|
b6bca982bb | ||
|
|
aa8a243bf1 | ||
|
|
75da037656 | ||
|
|
56a06a5581 | ||
|
|
0af67bea3a | ||
|
|
08b8d899b7 | ||
|
|
cf07cb39c4 | ||
|
|
810d10c1c2 | ||
|
|
342bdcfaac | ||
|
|
d933dbb31a | ||
|
|
fe61364178 | ||
|
|
88c3c03f99 | ||
|
|
c21b16f5c7 | ||
|
|
2481a2504f | ||
|
|
dcb1e8040a | ||
|
|
0e1c611034 | ||
|
|
fccd9d642c | ||
|
|
849f1becc7 | ||
|
|
be6c4a63ed | ||
|
|
4e48483276 | ||
|
|
6817a54aa0 | ||
|
|
482f235dcc | ||
|
|
90b863c4ae | ||
|
|
8541a87d1f | ||
|
|
002adeb00e | ||
|
|
30e7d1ba7c | ||
|
|
0923f2e120 | ||
|
|
38a0c8e19f | ||
|
|
facf90d964 | ||
|
|
cca3482bc0 | ||
|
|
6178ca550b | ||
|
|
dc51e467d4 | ||
|
|
300827c1f0 | ||
|
|
aacf09a444 | ||
|
|
0123488828 | ||
|
|
a43b188f36 | ||
|
|
f0285ef8f2 | ||
|
|
70a0ffbf3d | ||
|
|
f2e6a4fc55 | ||
|
|
df5fd76ae2 | ||
|
|
f34d258b45 | ||
|
|
28c52763c8 | ||
|
|
ebe22fa7f0 | ||
|
|
160327e471 | ||
|
|
dd080dbff2 | ||
|
|
d509886cd1 | ||
|
|
239a8f0af8 | ||
|
|
c0565306c3 | ||
|
|
79d159511d | ||
|
|
e123fc7e6f | ||
|
|
52fa30f389 | ||
|
|
1d744d5abc | ||
|
|
2629c12ade | ||
|
|
8fb6b1a5a9 | ||
|
|
d4a23c9a4f | ||
|
|
05d0f982a7 | ||
|
|
e3c8a3fcda | ||
|
|
8cd56d2705 | ||
|
|
f94d1f3cd8 | ||
|
|
5f64b74fad | ||
|
|
b422cd6f52 | ||
|
|
17698409b9 | ||
|
|
fb0dabebe1 | ||
|
|
f81805fbf4 | ||
|
|
596c9d4996 | ||
|
|
a7530dcce9 | ||
|
|
55d59eea80 | ||
|
|
0c66a1fb7b | ||
|
|
ad7ca5d287 | ||
|
|
b34be4730d | ||
|
|
1c264055eb | ||
|
|
12f41baff5 | ||
|
|
9a9ad1baa7 | ||
|
|
46c31278e7 | ||
|
|
a08dc3d422 | ||
|
|
158a74f79b | ||
|
|
bacf85ccf8 | ||
|
|
ef683bd7fa | ||
|
|
20c233fb8c | ||
|
|
ac21313190 | ||
|
|
8ba6918139 | ||
|
|
5e357b460d | ||
|
|
d660abe1f6 | ||
|
|
fd9617ec8f | ||
|
|
48ccdaab5e | ||
|
|
52d9548333 | ||
|
|
4706d28560 | ||
|
|
033f5de85d | ||
|
|
6760f6f03d | ||
|
|
e1a7862194 | ||
|
|
8ef53d229d | ||
|
|
c6d215f87c | ||
|
|
dccaed124c | ||
|
|
ccfca4a61a | ||
|
|
417a05f24f | ||
|
|
7a85f7576d | ||
|
|
8407ab3a74 | ||
|
|
c120ebd3ca | ||
|
|
d0635484a2 | ||
|
|
d3c487a28a | ||
|
|
14f46a5d5d | ||
|
|
00a8ca8273 | ||
|
|
deec457728 | ||
|
|
5a5508a973 | ||
|
|
357b285346 | ||
|
|
f8444b9e67 | ||
|
|
e0a224e414 | ||
|
|
9dfb128e1d | ||
|
|
462d1f0274 | ||
|
|
2e3803eef1 | ||
|
|
42b186eb94 | ||
|
|
ccdee9ec09 | ||
|
|
05eeb82efd | ||
|
|
6eefe64f4c | ||
|
|
5b96b19735 | ||
|
|
3764d877ce | ||
|
|
30e713c161 | ||
|
|
da3a76e441 | ||
|
|
adff467b50 | ||
|
|
d59f05ad8b | ||
|
|
9c3d124f48 | ||
|
|
4404b0f02e | ||
|
|
77533fba72 | ||
|
|
62d36ed0c2 | ||
|
|
94ad0123c7 | ||
|
|
109842f0fe | ||
|
|
d931d65cd8 | ||
|
|
b7861a7cb1 | ||
|
|
a49ad3bb48 | ||
|
|
d94acf5b78 | ||
|
|
fd0e1d1460 | ||
|
|
2ed9d5487e | ||
|
|
5dc44cb701 | ||
|
|
9e07c383fe | ||
|
|
9e1ec2768b | ||
|
|
39674e8c7f | ||
|
|
5dca4b7061 | ||
|
|
5d3cb25d14 | ||
|
|
6e337affbb | ||
|
|
87ab8cc15f | ||
|
|
b654e5cebd | ||
|
|
11958f2abf | ||
|
|
2a329ba92d | ||
|
|
b22487ef70 | ||
|
|
bf03a2f540 | ||
|
|
ece51a6f9f | ||
|
|
915dbffe72 | ||
|
|
fda0d6df1f | ||
|
|
25891f9dc0 | ||
|
|
35902adbdc | ||
|
|
981d15ee55 | ||
|
|
adfec3441b | ||
|
|
a208aa051f | ||
|
|
9068302357 | ||
|
|
1bb0439f19 | ||
|
|
f28585e245 | ||
|
|
99cfeb25ee | ||
|
|
6b7da9a39e | ||
|
|
18bba7baec | ||
|
|
ec8ffbc090 | ||
|
|
60eb1ca236 | ||
|
|
5e3756313c | ||
|
|
d303d5a423 | ||
|
|
938415aa79 | ||
|
|
9033710e56 | ||
|
|
2fd77e4034 | ||
|
|
b733d8205e | ||
|
|
f725ad09d7 | ||
|
|
e9a53c5b25 | ||
|
|
43e94cfe68 | ||
|
|
07d713734f | ||
|
|
4f004b8402 | ||
|
|
b1f4131af9 | ||
|
|
1adb19f427 | ||
|
|
fce4a4b1d9 | ||
|
|
61f35bcb5e | ||
|
|
0db68917a1 | ||
|
|
9da8ab9bdb | ||
|
|
8d1724df84 | ||
|
|
cd88d59937 | ||
|
|
cab4f5ce64 | ||
|
|
62ab1f8d00 | ||
|
|
f0e1439c79 | ||
|
|
5060918087 | ||
|
|
bf6b058152 | ||
|
|
40b607a73f | ||
|
|
2bdbba0eff | ||
|
|
058d821a7c | ||
|
|
4575ff89ad | ||
|
|
608454fcf2 | ||
|
|
58c1bec2da | ||
|
|
e1ce16c399 | ||
|
|
2d3a748286 | ||
|
|
06834dd8bf | ||
|
|
e4c0f19b1c | ||
|
|
84faf7a782 | ||
|
|
61db675ab1 | ||
|
|
59e72796e3 | ||
|
|
76e61a6358 | ||
|
|
455eceefa1 | ||
|
|
834e979060 | ||
|
|
ece36c8711 | ||
|
|
9b999a1ec8 | ||
|
|
80c53e15e5 | ||
|
|
a37b6077c1 | ||
|
|
fabfa392b7 | ||
|
|
8ff717cfd5 | ||
|
|
eae042d730 | ||
|
|
f6fda22160 | ||
|
|
c120ecab1a | ||
|
|
6afcbe90a1 | ||
|
|
130bde63cf | ||
|
|
c76e8f96d9 | ||
|
|
7b92014249 | ||
|
|
33df80559d | ||
|
|
c0d9d1e514 | ||
|
|
ed2567913b | ||
|
|
9a7bd94ce1 | ||
|
|
036aea5c7f | ||
|
|
1bc1311113 | ||
|
|
135fe6f2e6 | ||
|
|
f366a1d0dd | ||
|
|
b4cda3b349 | ||
|
|
0e92ae792c | ||
|
|
cf4247321c | ||
|
|
994be85579 | ||
|
|
8cdaf1a534 | ||
|
|
f9996d3643 | ||
|
|
6b7bcca21b | ||
|
|
c7109906bc | ||
|
|
63583ad630 | ||
|
|
4d063194ed | ||
|
|
c579e08614 | ||
|
|
40414a0f17 | ||
|
|
5e1ee6f06f | ||
|
|
f885333310 | ||
|
|
72149851cf | ||
|
|
e2ce971b70 | ||
|
|
802bb9b1c2 | ||
|
|
30ce642d88 | ||
|
|
bc901fad0b | ||
|
|
dae251fb43 | ||
|
|
f98a916a55 | ||
|
|
a7fd14ef36 | ||
|
|
5ff2eb1945 | ||
|
|
a60e82591f | ||
|
|
2ae59a5820 | ||
|
|
1d46427d51 | ||
|
|
8bd7009c02 | ||
|
|
cc90d546a5 | ||
|
|
e916ff5a0b | ||
|
|
54fa8f4867 | ||
|
|
7d92ee7d7a | ||
|
|
884388d0df | ||
|
|
50b386388e | ||
|
|
f03c0d7c57 | ||
|
|
83a3552b21 | ||
|
|
1462f54a65 | ||
|
|
b7cd1657b0 | ||
|
|
9d1ae432d5 | ||
|
|
1e32df5a26 | ||
|
|
07128d04b0 | ||
|
|
8d0c1348b1 | ||
|
|
7bcacdbd27 | ||
|
|
65c3bb3291 | ||
|
|
a08fa65fae | ||
|
|
85d8c9795a | ||
|
|
9a17dbbd68 | ||
|
|
f83827886e | ||
|
|
8964e98873 | ||
|
|
63e17e91bd | ||
|
|
f95cbe4ed2 | ||
|
|
9ddd1dac3a | ||
|
|
5ff0e67392 | ||
|
|
a60a2885e1 | ||
|
|
7e9ddf0c14 | ||
|
|
5cb82cca42 | ||
|
|
02edc35d3a | ||
|
|
a30c17ed91 | ||
|
|
d2dfd7ddb0 | ||
|
|
2a35b9912c | ||
|
|
bc097db09f | ||
|
|
75eb49ced3 | ||
|
|
cd2e3eec08 | ||
|
|
d72884a0e4 | ||
|
|
315e35965c | ||
|
|
f2fb3fc077 | ||
|
|
f4d9e3fbc6 | ||
|
|
d44f8e7ab4 | ||
|
|
b0438991bf | ||
|
|
0a23a469e2 | ||
|
|
2900e8afb7 | ||
|
|
e3e25785c7 | ||
|
|
7c4e1393fe | ||
|
|
b06736351b | ||
|
|
9c5a8c63ce | ||
|
|
e8c171be92 | ||
|
|
1b7aa9e48c | ||
|
|
51fd3e681c | ||
|
|
484510f763 | ||
|
|
7fde7327fe | ||
|
|
2c8094bf28 | ||
|
|
256d30847e | ||
|
|
0bbfee8716 | ||
|
|
8d58e81d28 | ||
|
|
8fb80d12c9 | ||
|
|
2b5eed3a38 | ||
|
|
dccd5305e7 | ||
|
|
bbb418d2af | ||
|
|
a5bae56f8d | ||
|
|
aa3d2f8787 | ||
|
|
e9750e145f | ||
|
|
d0cdefd972 | ||
|
|
451aa7f5da | ||
|
|
43ee6ef8f6 | ||
|
|
c366e8decc | ||
|
|
8610c61c18 | ||
|
|
bee3a3b012 | ||
|
|
dc2d46ca21 | ||
|
|
1b5346e86d | ||
|
|
8bc633aed9 | ||
|
|
ee3dee0570 | ||
|
|
69048d8f89 | ||
|
|
427025c2ed | ||
|
|
6deb336115 | ||
|
|
a8be115d4b | ||
|
|
115ae82b17 | ||
|
|
e4b357e8d3 | ||
|
|
560a7b2465 | ||
|
|
acfe05297b | ||
|
|
16579ff1a8 | ||
|
|
2dcf81a516 | ||
|
|
44f379e3d2 | ||
|
|
9be406c0a2 | ||
|
|
a030c50780 | ||
|
|
c9ca3a497f | ||
|
|
71886cd91c | ||
|
|
e60d46b2ec | ||
|
|
9829ca8d28 | ||
|
|
28c4651599 | ||
|
|
14de81520b | ||
|
|
3a11d335a2 | ||
|
|
0d3554cc49 | ||
|
|
6b458f3b55 | ||
|
|
874885671b | ||
|
|
a75f4dacab | ||
|
|
8c5a455f08 | ||
|
|
1b12cd46b2 | ||
|
|
42581469da | ||
|
|
562fa03c6c | ||
|
|
7389acd87f | ||
|
|
8feca2783f | ||
|
|
a290f7aba9 | ||
|
|
eb11f9f757 | ||
|
|
2ace19face | ||
|
|
25b6a94fd6 | ||
|
|
d0768b3dce | ||
|
|
b4c27f50ea | ||
|
|
669cb10f10 | ||
|
|
e9c0cd6d53 | ||
|
|
06708a8b7e | ||
|
|
fba8ca7817 | ||
|
|
37cae817a7 | ||
|
|
7d847cf179 | ||
|
|
a32c560f60 | ||
|
|
a4d4e640a2 | ||
|
|
74093976db | ||
|
|
76f17e3f95 | ||
|
|
ab4ce21927 | ||
|
|
eb14dedb63 | ||
|
|
004dfc1687 | ||
|
|
62b4b92815 | ||
|
|
e429bde5b8 | ||
|
|
6c3ee8d462 | ||
|
|
43f1c0d889 | ||
|
|
2cd1bb8cdd | ||
|
|
3d8bbdd01d | ||
|
|
1e90153f75 | ||
|
|
61ef68d4ae | ||
|
|
517953a097 | ||
|
|
5ca757ac05 | ||
|
|
1e69075099 | ||
|
|
5e12a4b238 | ||
|
|
9ddbad0b50 | ||
|
|
6e444c3289 | ||
|
|
385f8733ab | ||
|
|
42c0c50e1b | ||
|
|
3c17a8982a | ||
|
|
e0abf85014 | ||
|
|
35db6e5cde | ||
|
|
7abdb854ea | ||
|
|
924e50d135 | ||
|
|
0f70f7a169 | ||
|
|
7d4683d130 | ||
|
|
8803e96bb7 | ||
|
|
3a4220878d | ||
|
|
21ee02d4ed | ||
|
|
fbecb09d45 | ||
|
|
bbbec3b54a | ||
|
|
d62f7f3628 | ||
|
|
fc07a06248 | ||
|
|
c32682b245 | ||
|
|
fc502a55b3 | ||
|
|
d580a3fa26 | ||
|
|
639686253b | ||
|
|
1274731a02 | ||
|
|
9ca51fcd94 | ||
|
|
8cabec8a3b | ||
|
|
967b6cfc81 | ||
|
|
416ae76a94 | ||
|
|
05cfe65d8e | ||
|
|
d7dd804822 | ||
|
|
aa56a20240 | ||
|
|
23b85d078b |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -8,15 +8,6 @@
|
||||
/node_modules
|
||||
/test.php
|
||||
/main.html
|
||||
/client/css/espo.css
|
||||
/client/css/espo-vertical.css
|
||||
/client/css/sakura.css
|
||||
/client/css/sakura-vertical.css
|
||||
/client/css/violet.css
|
||||
/client/css/violet-vertical.css
|
||||
/client/css/espo-rtl.css
|
||||
/client/css/hazyblue.css
|
||||
/client/css/hazyblue-vertical.css
|
||||
|
||||
/tests/unit/testData/cache/*
|
||||
!/tests/unit/testData/cache/.data
|
||||
|
||||
@@ -6,4 +6,4 @@ Before we can merge your pull request you need to accept our CLA [here](https://
|
||||
|
||||
## Issues
|
||||
|
||||
We don't provide developer help or any kind of support on github. Please use our [forum](http://forum.espocrm.com/) for this.
|
||||
We don't provide developer help or any kind of support on github. Please use our [forum](https://forum.espocrm.com) for this.
|
||||
|
||||
156
Gruntfile.js
156
Gruntfile.js
@@ -35,12 +35,72 @@ module.exports = function (grunt) {
|
||||
'client/lib/bootstrap.min.js',
|
||||
'client/lib/bootstrap-datepicker.js',
|
||||
'client/lib/bull.js',
|
||||
'client/lib/marked.min.js',
|
||||
|
||||
'client/src/namespace.js',
|
||||
'client/src/exceptions.js',
|
||||
'client/src/loader.js',
|
||||
'client/src/utils.js'
|
||||
'client/src/utils.js',
|
||||
|
||||
'client/src/acl.js',
|
||||
'client/src/model.js',
|
||||
'client/src/model-offline.js',
|
||||
'client/src/ajax.js',
|
||||
'client/src/controller.js',
|
||||
|
||||
'client/src/ui.js',
|
||||
'client/src/acl-manager.js',
|
||||
'client/src/cache.js',
|
||||
'client/src/storage.js',
|
||||
'client/src/models/settings.js',
|
||||
'client/src/language.js',
|
||||
'client/src/metadata.js',
|
||||
'client/src/field-manager.js',
|
||||
'client/src/models/user.js',
|
||||
'client/src/models/preferences.js',
|
||||
'client/src/model-factory.js',
|
||||
'client/src/collection-factory.js',
|
||||
'client/src/pre-loader.js',
|
||||
'client/src/controllers/base.js',
|
||||
'client/src/router.js',
|
||||
'client/src/date-time.js',
|
||||
'client/src/layout-manager.js',
|
||||
'client/src/theme-manager.js',
|
||||
'client/src/session-storage.js',
|
||||
'client/src/view-helper.js',
|
||||
|
||||
'client/src/app.js'
|
||||
];
|
||||
|
||||
function camelCaseToHyphen (string){
|
||||
if (string == null) {
|
||||
return string;
|
||||
}
|
||||
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
}
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
var themeList = [];
|
||||
fs.readdirSync('application/Espo/Resources/metadata/themes').forEach(function (file) {
|
||||
themeList.push(file.substr(0, file.length - 5));
|
||||
});
|
||||
|
||||
var lessData = {};
|
||||
themeList.forEach(function (theme) {
|
||||
var name = camelCaseToHyphen(theme);
|
||||
var files = {};
|
||||
files['client/css/espo/'+name+'.css'] = 'frontend/less/'+name+'/main.less';
|
||||
files['client/css/espo/'+name+'-iframe.css'] = 'frontend/less/'+name+'/iframe/main.less';
|
||||
var o = {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: files
|
||||
};
|
||||
lessData[theme] = o;
|
||||
});
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
|
||||
@@ -58,90 +118,11 @@ module.exports = function (grunt) {
|
||||
clean: {
|
||||
start: ['build/*'],
|
||||
final: ['build/tmp'],
|
||||
},
|
||||
less: {
|
||||
espo: {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: {
|
||||
'client/css/espo.css': 'frontend/less/espo/main.less',
|
||||
}
|
||||
},
|
||||
espoVertical: {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: {
|
||||
'client/css/espo-vertical.css': 'frontend/less/espo-vertical/main.less',
|
||||
}
|
||||
},
|
||||
espoRtl: {
|
||||
options: {
|
||||
yuicompress: true
|
||||
},
|
||||
files: {
|
||||
'client/css/espo-rtl.css': 'frontend/less/espo-rtl/main.less'
|
||||
}
|
||||
},
|
||||
hazyblueVertical: {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: {
|
||||
'client/css/hazyblue-vertical.css': 'frontend/less/hazyblue-vertical/main.less',
|
||||
}
|
||||
},
|
||||
hazyblue: {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: {
|
||||
'client/css/hazyblue.css': 'frontend/less/hazyblue/main.less',
|
||||
}
|
||||
},
|
||||
sakura: {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: {
|
||||
'client/css/sakura.css': 'frontend/less/sakura/main.less',
|
||||
}
|
||||
},
|
||||
sakuraVertical: {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: {
|
||||
'client/css/sakura-vertical.css': 'frontend/less/sakura-vertical/main.less',
|
||||
}
|
||||
},
|
||||
violet: {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: {
|
||||
'client/css/violet.css': 'frontend/less/violet/main.less',
|
||||
}
|
||||
},
|
||||
violetVertical: {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: {
|
||||
'client/css/violet-vertical.css': 'frontend/less/violet-vertical/main.less',
|
||||
}
|
||||
beforeFinal: {
|
||||
src: ['build/tmp/custom/Espo/Custom/*', '!build/tmp/custom/Espo/Custom/.htaccess']
|
||||
}
|
||||
},
|
||||
cssmin: {
|
||||
minify: {
|
||||
files: {
|
||||
'build/tmp/client/css/espo.css': [
|
||||
'client/css/espo.css',
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
less: lessData,
|
||||
uglify: {
|
||||
options: {
|
||||
mangle: false,
|
||||
@@ -168,10 +149,6 @@ module.exports = function (grunt) {
|
||||
],
|
||||
dest: 'build/tmp/client',
|
||||
},
|
||||
frontendHtml: {
|
||||
src: 'frontend/reset.html',
|
||||
dest: 'build/tmp/reset.html'
|
||||
},
|
||||
frontendLib: {
|
||||
expand: true,
|
||||
dot: true,
|
||||
@@ -311,16 +288,15 @@ module.exports = function (grunt) {
|
||||
'clean:start',
|
||||
'mkdir:tmp',
|
||||
'less',
|
||||
'cssmin',
|
||||
'uglify',
|
||||
'copy:frontendFolders',
|
||||
'copy:frontendHtml',
|
||||
'copy:frontendLib',
|
||||
'copy:backend',
|
||||
'replace',
|
||||
'clean:beforeFinal',
|
||||
'copy:final',
|
||||
'chmod',
|
||||
'clean:final',
|
||||
'clean:final'
|
||||
]);
|
||||
|
||||
};
|
||||
|
||||
@@ -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.1 or above.
|
||||
* MySQL 5.5.3 or above.
|
||||
|
||||
For more information about server configuration see [this article](https://www.espocrm.com/documentation/administration/server-configuration/).
|
||||
|
||||
|
||||
63
application/Espo/Acl/EmailAddress.php
Normal file
63
application/Espo/Acl/EmailAddress.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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 EmailAddress extends \Espo\Core\Acl\Base
|
||||
{
|
||||
public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity)
|
||||
{
|
||||
$id = $entity->id;
|
||||
|
||||
$isFobidden = false;
|
||||
|
||||
$repository = $this->getEntityManager()->getRepository('EmailAddress');
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$entityWithSameAddressList = $repository->getEntityListByAddressId($id, $excludeEntity);
|
||||
foreach ($entityWithSameAddressList as $e) {
|
||||
if (!$this->getAclManager()->check($user, $e, 'edit')) {
|
||||
$isFobidden = true;
|
||||
if (
|
||||
$e->get('isPortalUser') && $excludeEntity->getEntityType() === 'Contact' &&
|
||||
$e->get('contactId') === $excludeEntity->id
|
||||
) {
|
||||
$isFobidden = false;
|
||||
}
|
||||
if ($isFobidden) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return !$isFobidden;
|
||||
}
|
||||
}
|
||||
|
||||
45
application/Espo/Acl/Note.php
Normal file
45
application/Espo/Acl/Note.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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 Note extends \Espo\Core\Acl\Base
|
||||
{
|
||||
public function checkIsOwner(EntityUser $user, Entity $entity)
|
||||
{
|
||||
if ($entity->get('type') === 'Post' && $user->id === $entity->get('createdById')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
63
application/Espo/Acl/PhoneNumber.php
Normal file
63
application/Espo/Acl/PhoneNumber.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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 PhoneNumber extends \Espo\Core\Acl\Base
|
||||
{
|
||||
public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity)
|
||||
{
|
||||
$id = $entity->id;
|
||||
|
||||
$isFobidden = false;
|
||||
|
||||
$repository = $this->getEntityManager()->getRepository('PhoneNumber');
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$entityWithSameNumberList = $repository->getEntityListByPhoneNumberId($id, $excludeEntity);
|
||||
foreach ($entityWithSameNumberList as $e) {
|
||||
if (!$this->getAclManager()->check($user, $e, 'edit')) {
|
||||
$isFobidden = true;
|
||||
if (
|
||||
$e->get('isPortalUser') && $excludeEntity->getEntityType() === 'Contact' &&
|
||||
$e->get('contactId') === $excludeEntity->id
|
||||
) {
|
||||
$isFobidden = false;
|
||||
}
|
||||
if ($isFobidden) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !$isFobidden;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Acl;
|
||||
|
||||
use \Espo\ORM\Entity;
|
||||
use \Espo\Entities\User as EntityUser;
|
||||
|
||||
class User extends \Espo\Core\Acl\Base
|
||||
{
|
||||
@@ -37,5 +38,36 @@ class User extends \Espo\Core\Acl\Base
|
||||
{
|
||||
return $user->id === $entity->id;
|
||||
}
|
||||
}
|
||||
|
||||
public function checkEntityCreate(EntityUser $user, Entity $entity, $data)
|
||||
{
|
||||
if (!$user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
return $this->checkEntity($user, $entity, $data, 'create');
|
||||
}
|
||||
|
||||
public function checkEntityDelete(EntityUser $user, Entity $entity, $data)
|
||||
{
|
||||
if ($entity->id === 'system') {
|
||||
return false;
|
||||
}
|
||||
if (!$user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
return parent::checkEntityDelete($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityEdit(EntityUser $user, Entity $entity, $data)
|
||||
{
|
||||
if ($entity->id === 'system') {
|
||||
return false;
|
||||
}
|
||||
if (!$user->isAdmin()) {
|
||||
if ($user->id !== $entity->id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return $this->checkEntity($user, $entity, $data, 'edit');
|
||||
}
|
||||
}
|
||||
|
||||
56
application/Espo/AclPortal/EmailAddress.php
Normal file
56
application/Espo/AclPortal/EmailAddress.php
Normal file
@@ -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\AclPortal;
|
||||
|
||||
use \Espo\Entities\User as EntityUser;
|
||||
use \Espo\ORM\Entity;
|
||||
|
||||
class EmailAddress extends \Espo\Core\AclPortal\Base
|
||||
{
|
||||
public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity)
|
||||
{
|
||||
$id = $entity->id;
|
||||
|
||||
$isFobidden = false;
|
||||
|
||||
$repository = $this->getEntityManager()->getRepository('EmailAddress');
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$entityWithSameAddressList = $repository->getEntityListByAddressId($id, $excludeEntity);
|
||||
foreach ($entityWithSameAddressList as $e) {
|
||||
if (!$this->getAclManager()->check($user, $e, 'edit')) {
|
||||
$isFobidden = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return !$isFobidden;
|
||||
}
|
||||
}
|
||||
45
application/Espo/AclPortal/Note.php
Normal file
45
application/Espo/AclPortal/Note.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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\AclPortal;
|
||||
|
||||
use \Espo\Entities\User as EntityUser;
|
||||
use \Espo\ORM\Entity;
|
||||
|
||||
class Note extends \Espo\Core\AclPortal\Base
|
||||
{
|
||||
public function checkIsOwner(EntityUser $user, Entity $entity)
|
||||
{
|
||||
if ($entity->get('type') === 'Post' && $user->id === $entity->get('createdById')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
57
application/Espo/AclPortal/PhoneNumber.php
Normal file
57
application/Espo/AclPortal/PhoneNumber.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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\AclPortal;
|
||||
|
||||
use \Espo\Entities\User as EntityUser;
|
||||
use \Espo\ORM\Entity;
|
||||
|
||||
class PhoneNumber extends \Espo\Core\AclPortal\Base
|
||||
{
|
||||
public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity)
|
||||
{
|
||||
$id = $entity->id;
|
||||
|
||||
$isFobidden = false;
|
||||
|
||||
$repository = $this->getEntityManager()->getRepository('PhoneNumber');
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$entityWithSameNumberList = $repository->getEntityListByPhoneNumberId($id, $excludeEntity);
|
||||
foreach ($entityWithSameNumberList as $e) {
|
||||
if (!$this->getAclManager()->check($user, $e, 'edit')) {
|
||||
$isFobidden = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !$isFobidden;
|
||||
}
|
||||
}
|
||||
67
application/Espo/Controllers/AuthLogRecord.php
Normal file
67
application/Espo/Controllers/AuthLogRecord.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\Controllers;
|
||||
|
||||
use \Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
class AuthLogRecord extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
protected function checkControllerAccess()
|
||||
{
|
||||
if (!$this->getUser()->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
public function actionUpdate($params, $data, $request)
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
public function actionMassUpdate($params, $data, $request)
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
public function actionCreate($params, $data, $request)
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
public function actionCreateLink($params, $data, $request)
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
public function actionRemoveLink($params, $data, $request)
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
55
application/Espo/Controllers/DataPrivacy.php
Normal file
55
application/Espo/Controllers/DataPrivacy.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
class DataPrivacy extends \Espo\Core\Controllers\Base
|
||||
{
|
||||
protected function checkControllerAccess()
|
||||
{
|
||||
if ($this->getAcl()->get('dataPrivacyPermission') === 'no') {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
public function postActionErase($params, $data)
|
||||
{
|
||||
if (empty($data->entityType) || empty($data->id) || empty($data->fieldList) || !is_array($data->fieldList)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
return $this->getServiceFactory()->create('DataPrivacy')->erase($data->entityType, $data->id, $data->fieldList);
|
||||
}
|
||||
|
||||
}
|
||||
35
application/Espo/Controllers/EmailTemplateCategory.php
Normal file
35
application/Espo/Controllers/EmailTemplateCategory.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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;
|
||||
|
||||
class EmailTemplateCategory extends \Espo\Core\Templates\Controllers\CategoryTree
|
||||
{
|
||||
|
||||
}
|
||||
@@ -83,6 +83,20 @@ class EntityManager extends \Espo\Core\Controllers\Base
|
||||
if (isset($data['textFilterFields']) && is_array($data['textFilterFields'])) {
|
||||
$params['textFilterFields'] = $data['textFilterFields'];
|
||||
}
|
||||
if (!empty($data['color'])) {
|
||||
$params['color'] = $data['color'];
|
||||
}
|
||||
if (!empty($data['iconClass'])) {
|
||||
$params['iconClass'] = $data['iconClass'];
|
||||
}
|
||||
if (isset($data['fullTextSearch'])) {
|
||||
$params['fullTestSearch'] = $data['fullTextSearch'];
|
||||
}
|
||||
|
||||
$params['kanbanViewMode'] = !empty($data['kanbanViewMode']);
|
||||
if (!empty($data['kanbanStatusIgnoreList'])) {
|
||||
$params['kanbanStatusIgnoreList'] = $data['kanbanStatusIgnoreList'];
|
||||
}
|
||||
|
||||
$result = $this->getContainer()->get('entityManagerUtil')->create($name, $type, $params);
|
||||
|
||||
@@ -245,7 +259,9 @@ class EntityManager extends \Espo\Core\Controllers\Base
|
||||
|
||||
$params = array();
|
||||
foreach ($paramList as $item) {
|
||||
$params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING);
|
||||
if (array_key_exists($item, $data)) {
|
||||
$params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($additionalParamList as $item) {
|
||||
@@ -322,4 +338,16 @@ class EntityManager extends \Espo\Core\Controllers\Base
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionResetToDefault($params, $data, $request)
|
||||
{
|
||||
if (empty($data->scope)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getContainer()->get('entityManagerUtil')->resetToDefaults($data->scope);
|
||||
$this->getContainer()->get('dataManager')->clearCache();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,11 @@ class FieldManager extends \Espo\Core\Controllers\Base
|
||||
return $fieldManager->read($params['scope'], $data->name);
|
||||
}
|
||||
|
||||
public function patchActionUpdate($params, $data)
|
||||
{
|
||||
return $this->putActionUpdate($params, $data);
|
||||
}
|
||||
|
||||
public function putActionUpdate($params, $data)
|
||||
{
|
||||
if (empty($params['scope']) || empty($params['name'])) {
|
||||
|
||||
60
application/Espo/Controllers/Pdf.php
Normal file
60
application/Espo/Controllers/Pdf.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?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\Error;
|
||||
|
||||
class Pdf extends \Espo\Core\Controllers\Base
|
||||
{
|
||||
public function postActionMassPrint($params, $data)
|
||||
{
|
||||
if (empty($data->idList) || !is_array($data->idList)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
if (empty($data->entityType)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
if (empty($data->templateId)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
if (!$this->getAcl()->checkScope('Template')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
if (!$this->getAcl()->checkScope($data->entityType)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $this->getServiceFactory()->create('Pdf')->massGenerate($data->entityType, $data->idList, $data->templateId, true)
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -90,11 +90,9 @@ class Settings extends \Espo\Core\Controllers\Base
|
||||
throw new Error('Cannot save settings');
|
||||
}
|
||||
|
||||
/** Rebuild for Currency Settings */
|
||||
if (isset($data->baseCurrency) || isset($data->currencyRates)) {
|
||||
if (isset($data->defaultCurrency) || isset($data->baseCurrency) || isset($data->currencyRates)) {
|
||||
$this->getContainer()->get('dataManager')->rebuildDatabase([]);
|
||||
}
|
||||
/** END Rebuild for Currency Settings */
|
||||
|
||||
return $this->getConfigData();
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ class Base implements Injectable
|
||||
}
|
||||
}
|
||||
|
||||
if ($entity->hasAttribute('assignedUsersIds') && $entity->hasRelation('assignedUsers')) {
|
||||
if ($entity->hasLinkMultipleField('assignedUsers')) {
|
||||
if ($entity->hasLinkMultipleId('assignedUsers', $user->id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ class Table
|
||||
|
||||
protected $forbiddenFieldsCache = array();
|
||||
|
||||
protected $isStrictModeForced = false;
|
||||
|
||||
protected $isStrictMode = false;
|
||||
|
||||
public function __construct(User $user, Config $config = null, FileManager $fileManager = null, Metadata $metadata = null, FieldManagerUtil $fieldManager = null)
|
||||
@@ -83,7 +85,11 @@ class Table
|
||||
'fieldTableQuickAccess' => (object) [],
|
||||
];
|
||||
|
||||
$this->isStrictMode = $config->get('aclStrictMode', false);
|
||||
if ($this->isStrictModeForced) {
|
||||
$this->isStrictMode = true;
|
||||
} else {
|
||||
$this->isStrictMode = $config->get('aclStrictMode', false);
|
||||
}
|
||||
|
||||
$this->user = $user;
|
||||
|
||||
@@ -163,7 +169,7 @@ class Table
|
||||
if (isset($this->data->$permission)) {
|
||||
return $this->data->$permission;
|
||||
}
|
||||
return null;
|
||||
return 'no';
|
||||
}
|
||||
|
||||
public function getLevel($scope, $action)
|
||||
@@ -204,6 +210,7 @@ class Table
|
||||
$this->applyDisabled($aclTable, $fieldTable);
|
||||
$this->applyMandatory($aclTable, $fieldTable);
|
||||
$this->applyAdditional($aclTable, $fieldTable, $valuePermissionLists);
|
||||
$this->applyReadOnlyFields($fieldTable);
|
||||
} else {
|
||||
$aclTable = (object) [];
|
||||
foreach ($this->getScopeList() as $scope) {
|
||||
@@ -405,7 +412,12 @@ class Table
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $this->metadata->get('app.'.$this->type.'.default.scopeLevel', array());
|
||||
$defaultsGroupName = 'default';
|
||||
if ($this->isStrictMode) {
|
||||
$defaultsGroupName = 'strictDefault';
|
||||
}
|
||||
|
||||
$data = $this->metadata->get(['app', $this->type, $defaultsGroupName, 'scopeLevel'], []);
|
||||
|
||||
foreach ($data as $scope => $item) {
|
||||
if (isset($table->$scope)) continue;
|
||||
@@ -416,7 +428,7 @@ class Table
|
||||
$table->$scope = $value;
|
||||
}
|
||||
|
||||
$defaultFieldData = $this->metadata->get('app.'.$this->type.'.default.fieldLevel', array());
|
||||
$defaultFieldData = $this->metadata->get(['app', $this->type, $defaultsGroupName, 'fieldLevel'], []);
|
||||
|
||||
foreach ($this->getScopeList() as $scope) {
|
||||
if (isset($table->$scope) && $table->$scope === false) continue;
|
||||
@@ -424,7 +436,7 @@ class Table
|
||||
|
||||
$fieldList = array_keys($this->getMetadata()->get("entityDefs.{$scope}.fields", []));
|
||||
|
||||
$defaultScopeFieldData = $this->metadata->get('app.'.$this->type.'.default.scopeFieldLevel.' . $scope, array());
|
||||
$defaultScopeFieldData = $this->metadata->get('app.'.$this->type.'.'.$defaultsGroupName.'.scopeFieldLevel.' . $scope, []);
|
||||
|
||||
foreach (array_merge($defaultFieldData, $defaultScopeFieldData) as $field => $f) {
|
||||
if (!in_array($field, $fieldList)) continue;
|
||||
@@ -702,25 +714,29 @@ class Table
|
||||
private function buildCache()
|
||||
{
|
||||
$this->fileManager->putPhpContents($this->cacheFilePath, $this->data, true);
|
||||
/*$contents = '<' . '?'. 'php return ' . $this->varExport($this->data) . ';';
|
||||
$this->fileManager->putContents($this->cacheFilePath, $contents);*/
|
||||
}
|
||||
|
||||
/*private function varExport($variable)
|
||||
protected function applyReadOnlyFields(&$fieldTable)
|
||||
{
|
||||
if ($variable instanceof \StdClass) {
|
||||
$result = '(object) ' . $this->varExport(get_object_vars($variable), true);
|
||||
} else if (is_array($variable)) {
|
||||
$array = array();
|
||||
foreach ($variable as $key => $value) {
|
||||
$array[] = var_export($key, true).' => ' . $this->varExport($value, true);
|
||||
// TODO Enable in 5.4.0
|
||||
return;
|
||||
$scopeList = $this->getScopeWithAclList();
|
||||
foreach ($scopeList as $scope) {
|
||||
if (!property_exists($fieldTable, $scope)) continue;
|
||||
$fieldList = array_keys($this->getMetadata()->get(['entityDefs', $scope, 'fields'], []));
|
||||
foreach ($fieldList as $field) {
|
||||
if ($this->getMetadata()->get(['entityDefs', $scope, 'fields', $field, 'readOnly'])) {
|
||||
if (property_exists($fieldTable->$scope, $field)) {
|
||||
$fieldTable->$scope->$field->edit = 'no';
|
||||
} else {
|
||||
$fieldTable->$scope->$field = (object) [];
|
||||
foreach ($this->fieldActionList as $action) {
|
||||
$fieldTable->$scope->$field->$action = 'yes';
|
||||
}
|
||||
$fieldTable->$scope->$field->edit = 'no';
|
||||
}
|
||||
}
|
||||
}
|
||||
$result = '['.implode(', ', $array).']';
|
||||
} else {
|
||||
$result = var_export($variable, true);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ class AclManager
|
||||
|
||||
protected $tableClassName = '\\Espo\\Core\\Acl\\Table';
|
||||
|
||||
protected $userAclClassName = '\\Espo\\Core\\Acl';
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
@@ -180,9 +182,15 @@ class AclManager
|
||||
|
||||
$impl = $this->getImplementation($scope);
|
||||
|
||||
$methodName = 'checkEntity' . ucfirst($action);
|
||||
if (method_exists($impl, $methodName)) {
|
||||
return $impl->$methodName($user, $entity, $data);
|
||||
if (!$action) {
|
||||
$action = 'read';
|
||||
}
|
||||
|
||||
if ($action) {
|
||||
$methodName = 'checkEntity' . ucfirst($action);
|
||||
if (method_exists($impl, $methodName)) {
|
||||
return $impl->$methodName($user, $entity, $data);
|
||||
}
|
||||
}
|
||||
|
||||
return $impl->checkEntity($user, $entity, $data, $action);
|
||||
@@ -279,5 +287,11 @@ class AclManager
|
||||
{
|
||||
return $this->checkUserPermission($user, $target, 'assignmentPermission');
|
||||
}
|
||||
}
|
||||
|
||||
public function createUserAcl(User $user)
|
||||
{
|
||||
$className = $this->userAclClassName;
|
||||
$acl = new $className($this, $user);
|
||||
return $acl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ class Table extends \Espo\Core\Acl\Table
|
||||
|
||||
protected $levelList = ['yes', 'all', 'account', 'contact', 'own', 'no'];
|
||||
|
||||
protected $isStrictModeForced = true;
|
||||
|
||||
public function __construct(User $user, Portal $portal, Config $config = null, FileManager $fileManager = null, Metadata $metadata = null, FieldManagerUtil $fieldManager = null)
|
||||
{
|
||||
if (empty($portal)) {
|
||||
|
||||
@@ -91,6 +91,7 @@ class Application
|
||||
public function runClient()
|
||||
{
|
||||
$this->getContainer()->get('clientManager')->display();
|
||||
exit;
|
||||
}
|
||||
|
||||
public function runEntryPoint($entryPoint, $data = array(), $final = false)
|
||||
|
||||
@@ -217,7 +217,8 @@ class Container
|
||||
$this->get('acl'),
|
||||
$this->get('aclManager'),
|
||||
$this->get('metadata'),
|
||||
$this->get('config')
|
||||
$this->get('config'),
|
||||
$this->get('injectableFactory')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -272,7 +273,7 @@ class Container
|
||||
return new \Espo\Core\Utils\Metadata\OrmMetadata(
|
||||
$this->get('metadata'),
|
||||
$this->get('fileManager'),
|
||||
$this->get('config')->get('useCache')
|
||||
$this->get('config')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,6 +296,16 @@ class Container
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadBaseLanguage()
|
||||
{
|
||||
return new \Espo\Core\Utils\Language(
|
||||
'en_US',
|
||||
$this->get('fileManager'),
|
||||
$this->get('metadata'),
|
||||
$this->get('useCache')
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadDefaultLanguage()
|
||||
{
|
||||
return new \Espo\Core\Utils\Language(
|
||||
@@ -329,8 +340,6 @@ class Container
|
||||
protected function loadFieldManager()
|
||||
{
|
||||
return new \Espo\Core\Utils\FieldManager(
|
||||
$this->get('metadata'),
|
||||
$this->get('language'),
|
||||
$this
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,11 +133,12 @@ class Record extends Base
|
||||
$q = $request->get('q');
|
||||
$textFilter = $request->get('textFilter');
|
||||
|
||||
$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("Max should should not exceed " . self::MAX_SIZE_LIMIT . ". Use pagination (offset, limit).");
|
||||
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
|
||||
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
}
|
||||
|
||||
$params = array(
|
||||
@@ -160,6 +161,49 @@ class Record extends Base
|
||||
);
|
||||
}
|
||||
|
||||
public function getActionListKanban($params, $data, $request)
|
||||
{
|
||||
if (!$this->getAcl()->check($this->name, 'read')) {
|
||||
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
|
||||
);
|
||||
|
||||
$this->fetchListParamsFromRequest($params, $request, $data);
|
||||
|
||||
$result = $this->getRecordService()->getListKanban($params);
|
||||
|
||||
return (object) [
|
||||
'total' => $result->total,
|
||||
'list' => $result->collection->getValueMapList(),
|
||||
'additionalData' => $result->additionalData
|
||||
];
|
||||
}
|
||||
|
||||
protected function fetchListParamsFromRequest(&$params, $request, $data)
|
||||
{
|
||||
if ($request->get('primaryFilter')) {
|
||||
@@ -171,6 +215,10 @@ class Record extends Base
|
||||
if ($request->get('filterList')) {
|
||||
$params['filterList'] = $request->get('filterList');
|
||||
}
|
||||
|
||||
if ($request->get('select')) {
|
||||
$params['select'] = explode(',', $request->get('select'));
|
||||
}
|
||||
}
|
||||
|
||||
public function actionListLinked($params, $data, $request)
|
||||
@@ -186,11 +234,12 @@ class Record extends Base
|
||||
$q = $request->get('q');
|
||||
$textFilter = $request->get('textFilter');
|
||||
|
||||
$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 should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
}
|
||||
|
||||
$params = array(
|
||||
@@ -450,7 +499,7 @@ class Record extends Base
|
||||
}
|
||||
$targetId = $data->targetId;
|
||||
$sourceIds = $data->sourceIds;
|
||||
$attributes = get_object_vars($data->attributes);
|
||||
$attributes = $data->attributes;
|
||||
|
||||
if (!$this->getAcl()->check($this->name, 'edit')) {
|
||||
throw new Forbidden();
|
||||
|
||||
@@ -116,9 +116,10 @@ class CronManager
|
||||
{
|
||||
$lastRunData = $this->getFileManager()->getPhpContents($this->lastRunTime);
|
||||
|
||||
$lastRunTime = time() - intval($this->getConfig()->get('cron.minExecutionTime')) - 1;
|
||||
if (is_array($lastRunData) && !empty($lastRunData['time'])) {
|
||||
$lastRunTime = $lastRunData['time'];
|
||||
} else {
|
||||
$lastRunTime = time() - intval($this->getConfig()->get('cronMinInterval', 0)) - 1;
|
||||
}
|
||||
|
||||
return $lastRunTime;
|
||||
@@ -136,9 +137,9 @@ class CronManager
|
||||
{
|
||||
$currentTime = time();
|
||||
$lastRunTime = $this->getLastRunTime();
|
||||
$minTime = $this->getConfig()->get('cron.minExecutionTime');
|
||||
$cronMinInterval = $this->getConfig()->get('cronMinInterval', 0);
|
||||
|
||||
if ($currentTime > ($lastRunTime + $minTime) ) {
|
||||
if ($currentTime > ($lastRunTime + $cronMinInterval)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -167,10 +168,30 @@ class CronManager
|
||||
$pendingJobList = $this->getCronJobUtil()->getPendingJobList();
|
||||
|
||||
foreach ($pendingJobList as $job) {
|
||||
$skip = false;
|
||||
$this->getEntityManager()->getPdo()->query('LOCK TABLES `job` WRITE');
|
||||
if ($this->getCronJobUtil()->isJobPending($job->id)) {
|
||||
if ($job->get('scheduledJobId')) {
|
||||
if ($this->getCronJobUtil()->isScheduledJobRunning($job->get('scheduledJobId'), $job->get('targetId'), $job->get('targetType'))) {
|
||||
$skip = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$skip = true;
|
||||
}
|
||||
|
||||
if ($skip) {
|
||||
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
|
||||
continue;
|
||||
}
|
||||
|
||||
$job->set('status', self::RUNNING);
|
||||
$job->set('pid', $this->getCronJobUtil()->getPid());
|
||||
$this->getEntityManager()->saveEntity($job);
|
||||
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
|
||||
|
||||
$isSuccess = true;
|
||||
$skipLog = false;
|
||||
|
||||
try {
|
||||
if ($job->get('scheduledJobId')) {
|
||||
@@ -180,7 +201,12 @@ class CronManager
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$isSuccess = false;
|
||||
$GLOBALS['log']->error('CronManager: Failed job running, job ['.$job->id.']. Error Details: '.$e->getMessage());
|
||||
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;
|
||||
@@ -188,7 +214,7 @@ class CronManager
|
||||
$job->set('status', $status);
|
||||
$this->getEntityManager()->saveEntity($job);
|
||||
|
||||
if ($job->get('scheduledJobId')) {
|
||||
if ($job->get('scheduledJobId') && !$skipLog) {
|
||||
$this->getCronScheduledJobUtil()->addLogRecord($job->get('scheduledJobId'), $status, null, $job->get('targetId'), $job->get('targetType'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ class DataManager
|
||||
*/
|
||||
public function rebuild($entityList = null)
|
||||
{
|
||||
$this->populateConfigParameters();
|
||||
|
||||
$result = $this->clearCache();
|
||||
|
||||
$result &= $this->rebuildMetadata();
|
||||
@@ -143,13 +145,17 @@ class DataManager
|
||||
if ($job) {
|
||||
$entityManager->removeEntity($job);
|
||||
}
|
||||
$name = $jobName;
|
||||
if (!empty($defs['name'])) {
|
||||
$name = $defs['name'];
|
||||
}
|
||||
$job = $entityManager->getEntity('ScheduledJob');
|
||||
$job->set(array(
|
||||
'job' => $jobName,
|
||||
'status' => 'Active',
|
||||
'scheduling' => $defs['scheduling'],
|
||||
'isInternal' => true,
|
||||
'name' => $jobName
|
||||
'name' => $name
|
||||
));
|
||||
$entityManager->saveEntity($job);
|
||||
}
|
||||
@@ -168,5 +174,25 @@ class DataManager
|
||||
$this->getContainer()->get('config')->save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected function populateConfigParameters()
|
||||
{
|
||||
$config = $this->getContainer()->get('config');
|
||||
|
||||
$pdo = $this->getContainer()->get('entityManager')->getPDO();
|
||||
$query = "SHOW VARIABLES LIKE 'ft_min_word_len'";
|
||||
$sth = $pdo->prepare($query);
|
||||
$sth->execute();
|
||||
|
||||
$fullTextSearchMinLength = null;
|
||||
if ($row = $sth->fetch(\PDO::FETCH_ASSOC)) {
|
||||
if (isset($row['Value'])) {
|
||||
$fullTextSearchMinLength = intval($row['Value']);
|
||||
}
|
||||
}
|
||||
|
||||
$config->set('fullTextSearchMinLength', $fullTextSearchMinLength);
|
||||
|
||||
$config->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,19 +31,35 @@ namespace Espo\Core\Export;
|
||||
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
|
||||
use \Espo\Core\ORM\Entity;
|
||||
|
||||
class Csv extends \Espo\Core\Injectable
|
||||
{
|
||||
protected $dependencyList = [
|
||||
'config',
|
||||
'preferences'
|
||||
'preferences',
|
||||
'metadata'
|
||||
];
|
||||
|
||||
public function loadAdditionalFields(Entity $entity, $fieldList)
|
||||
{
|
||||
foreach ($fieldList as $field) {
|
||||
if ($this->getInjection('metadata')->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']) === 'linkMultiple') {
|
||||
if (!$entity->has($field . 'Ids')) {
|
||||
$entity->loadLinkMultipleField($field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function process($entityType, $params, $dataList)
|
||||
{
|
||||
if (!is_array($params['attributeList'])) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$dataList = $this->prepareDataList($dataList);
|
||||
|
||||
$attributeList = $params['attributeList'];
|
||||
|
||||
$delimiter = $this->getInjection('preferences')->get('exportDelimiter');
|
||||
@@ -62,4 +78,21 @@ class Csv extends \Espo\Core\Injectable
|
||||
|
||||
return $csv;
|
||||
}
|
||||
|
||||
protected function prepareDataList($dataList)
|
||||
{
|
||||
$prepareDataList = [];
|
||||
foreach ($dataList as $row) {
|
||||
$preparedRow = [];
|
||||
foreach ($row as $item) {
|
||||
if (is_array($item) || is_object($item)) {
|
||||
$item = \Espo\Core\Utils\Json::encode($item);
|
||||
}
|
||||
$preparedRow[] = $item;
|
||||
}
|
||||
$prepareDataList[] = $preparedRow;
|
||||
}
|
||||
|
||||
return $prepareDataList;
|
||||
}
|
||||
}
|
||||
@@ -62,22 +62,51 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
public function loadAdditionalFields(Entity $entity, $fieldList)
|
||||
{
|
||||
foreach ($entity->getRelationList() as $link) {
|
||||
if ($entity->getRelationType($link) === 'belongsToParent') {
|
||||
if (in_array($link, $fieldList)) {
|
||||
$parent = $entity->get($link);
|
||||
if ($parent instanceof Entity) {
|
||||
$entity->set($link . 'Name', $parent->get('name'));
|
||||
if (in_array($link, $fieldList)) {
|
||||
if ($entity->getRelationType($link) === 'belongsToParent') {
|
||||
if (!$entity->get($link . 'Name')) {
|
||||
$entity->loadParentNameField($link);
|
||||
}
|
||||
}
|
||||
} else if ($entity->getRelationType($link) === 'belongsTo' && $entity->getRelationParam($link, 'noJoin') && $entity->hasField($link . 'Name')) {
|
||||
if (in_array($link, $fieldList)) {
|
||||
$related = $entity->get($link);
|
||||
if ($related instanceof Entity) {
|
||||
$entity->set($link . 'Name', $related->get('name'));
|
||||
} else if (
|
||||
(
|
||||
(
|
||||
$entity->getRelationType($link) === 'belongsTo'
|
||||
&&
|
||||
$entity->getRelationParam($link, 'noJoin')
|
||||
)
|
||||
||
|
||||
$entity->getRelationType($link) === 'hasOne'
|
||||
)
|
||||
&&
|
||||
$entity->hasAttribute($link . 'Name')
|
||||
) {
|
||||
if (!$entity->get($link . 'Name') || !$entity->get($link . 'Id')) {
|
||||
$entity->loadLinkField($link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($fieldList as $field) {
|
||||
if ($this->getMetadata()->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']) === 'linkMultiple') {
|
||||
if (!$entity->has($field . 'Ids')) {
|
||||
$entity->loadLinkMultipleField($field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function filterFieldList($entityType, $fieldList, $exportAllFields)
|
||||
{
|
||||
if ($exportAllFields) {
|
||||
foreach ($fieldList as $i => $field) {
|
||||
$type = $this->getMetadata()->get(['entityDefs', $entityType, 'fields', $field, 'type']);
|
||||
if (in_array($type, ['linkMultiple', 'attachmentMultiple'])) {
|
||||
unset($fieldList[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($fieldList);
|
||||
}
|
||||
|
||||
public function addAdditionalAttributes($entityType, &$attributeList, $fieldList)
|
||||
@@ -123,7 +152,7 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$phpExcel = new \PHPExcel();
|
||||
$phpExcel = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||
$sheet = $phpExcel->setActiveSheetIndex(0);
|
||||
|
||||
if (isset($params['exportName'])) {
|
||||
@@ -155,10 +184,11 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
)
|
||||
);
|
||||
|
||||
$now = new \DateTime();
|
||||
$now->setTimezone(new \DateTimeZone($this->getInjection('config')->get('timeZone', 'UTC')));
|
||||
|
||||
$sheet->setCellValue('A1', $exportName);
|
||||
|
||||
$sheet->setCellValue('B1', \PHPExcel_Shared_Date::PHPToExcel(strtotime(date('Y-m-d H:i:s'))));
|
||||
|
||||
$sheet->setCellValue('B1', \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime($now->format('Y-m-d H:i:s'))));
|
||||
|
||||
$sheet->getStyle('A1')->applyFromArray($titleStyle);
|
||||
$sheet->getStyle('B1')->applyFromArray($dateStyle);
|
||||
@@ -270,6 +300,8 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
}
|
||||
} else if ($type == 'int') {
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name] ?: 0);
|
||||
} else if ($type == 'float') {
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name] ?: 0);
|
||||
} else if ($type == 'currency') {
|
||||
if (array_key_exists($name.'Currency', $row) && array_key_exists($name, $row)) {
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name] ? $row[$name] : '');
|
||||
@@ -282,7 +314,7 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
}
|
||||
} else if ($type == 'currencyConverted') {
|
||||
if (array_key_exists($name, $row)) {
|
||||
$currency = $this->getConfig()->get('baseCurrency');
|
||||
$currency = $this->getConfig()->get('defaultCurrency');
|
||||
$currencySymbol = $this->getMetadata()->get(['app', 'currency', 'symbolMap', $currency], '');
|
||||
|
||||
$sheet->getStyle("$col$rowNumber")
|
||||
@@ -309,7 +341,7 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
}
|
||||
} else if ($type == 'date') {
|
||||
if (isset($row[$name])) {
|
||||
$sheet->setCellValue("$col$rowNumber", \PHPExcel_Shared_Date::PHPToExcel(strtotime($row[$name])));
|
||||
$sheet->setCellValue("$col$rowNumber", \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime($row[$name])));
|
||||
}
|
||||
} else if ($type == 'datetime' || $type == 'datetimeOptional') {
|
||||
$value = null;
|
||||
@@ -334,14 +366,14 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
}
|
||||
}
|
||||
if ($value) {
|
||||
$sheet->setCellValue("$col$rowNumber", \PHPExcel_Shared_Date::PHPToExcel(strtotime($value)));
|
||||
$sheet->setCellValue("$col$rowNumber", \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime($value)));
|
||||
}
|
||||
} else if ($type == 'image') {
|
||||
if (isset($row[$name . 'Id']) && $row[$name . 'Id']) {
|
||||
$attachment = $this->getEntityManager()->getEntity('Attachment', $row[$name . 'Id']);
|
||||
|
||||
if ($attachment) {
|
||||
$objDrawing = new \PHPExcel_Worksheet_Drawing();
|
||||
$objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
|
||||
$filePath = $this->getInjection('fileStorageManager')->getLocalFilePath($attachment);
|
||||
|
||||
if ($filePath && file_exists($filePath)) {
|
||||
@@ -367,14 +399,97 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
}
|
||||
$sheet->setCellValue("$col$rowNumber", $value);
|
||||
}
|
||||
} else if ($type == 'linkMultiple') {
|
||||
if (array_key_exists($name . 'Ids', $row) && array_key_exists($name . 'Names', $row)) {
|
||||
$nameList = [];
|
||||
foreach ($row[$name . 'Ids'] as $relatedId) {
|
||||
$relatedName = $relatedId;
|
||||
if (property_exists($row[$name . 'Names'], $relatedId)) {
|
||||
$relatedName = $row[$name . 'Names']->$relatedId;
|
||||
}
|
||||
$nameList[] = $relatedName;
|
||||
}
|
||||
$sheet->setCellValue("$col$rowNumber", implode(', ', $nameList));
|
||||
}
|
||||
} else if ($type == 'address') {
|
||||
$value = '';
|
||||
if (!empty($row[$name . 'Street'])) {
|
||||
$value = $value .= $row[$name.'Street'];
|
||||
}
|
||||
if (!empty($row[$name.'City']) || !empty($row[$name.'State']) || !empty($row[$name.'PostalCode'])) {
|
||||
if ($value) {
|
||||
$value .= "\n";
|
||||
}
|
||||
if (!empty($row[$name.'City'])) {
|
||||
$value .= $row[$name.'City'];
|
||||
if (
|
||||
!empty($row[$name.'State']) || !empty($row[$name.'PostalCode'])
|
||||
) {
|
||||
$value .= ', ';
|
||||
}
|
||||
}
|
||||
if (!empty($row[$name.'State'])) {
|
||||
$value .= $row[$name.'State'];
|
||||
if (!empty($row[$name.'PostalCode'])) {
|
||||
$value .= ' ';
|
||||
}
|
||||
}
|
||||
if (!empty($row[$name.'PostalCode'])) {
|
||||
$value .= $row[$name.'PostalCode'];
|
||||
}
|
||||
}
|
||||
if (!empty($row[$name.'Country'])) {
|
||||
if ($value) {
|
||||
$value .= "\n";
|
||||
}
|
||||
$value .= $row[$name.'Country'];
|
||||
}
|
||||
$sheet->setCellValue("$col$rowNumber", $value);
|
||||
} else if ($type == 'duration') {
|
||||
if (!empty($row[$name])) {
|
||||
$seconds = intval($row[$name]);
|
||||
|
||||
$days = intval(floor($seconds / 86400));
|
||||
$seconds = $seconds - $days * 86400;
|
||||
$hours = intval(floor($seconds / 3600));
|
||||
$seconds = $seconds - $hours * 3600;
|
||||
$minutes = intval(floor($seconds / 60));
|
||||
|
||||
$value = '';
|
||||
if ($days) {
|
||||
$value .= (string) $days . $this->getInjection('language')->translate('d', 'durationUnits');
|
||||
if ($minutes || $hours) {
|
||||
$value .= ' ';
|
||||
}
|
||||
}
|
||||
if ($hours) {
|
||||
$value .= (string) $hours . $this->getInjection('language')->translate('h', 'durationUnits');
|
||||
if ($minutes) {
|
||||
$value .= ' ';
|
||||
}
|
||||
}
|
||||
if ($minutes) {
|
||||
$value .= (string) $minutes . $this->getInjection('language')->translate('m', 'durationUnits');
|
||||
}
|
||||
|
||||
$sheet->setCellValue("$col$rowNumber", $value);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (array_key_exists($name, $row)) {
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name]);
|
||||
$sheet->setCellValueExplicit("$col$rowNumber", $row[$name], \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
$link = false;
|
||||
|
||||
$foreignLink = null;
|
||||
$isForeign = false;
|
||||
if (strpos($name, '_')) {
|
||||
$isForeign = true;
|
||||
list($foreignLink, $foreignField) = explode('_', $name);
|
||||
}
|
||||
|
||||
if ($name == 'name') {
|
||||
if (array_key_exists('id', $row)) {
|
||||
$link = $this->getConfig()->getSiteUrl() . "/#".$entityType . "/view/" . $row['id'];
|
||||
@@ -385,7 +500,13 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
}
|
||||
} else if ($type == 'link') {
|
||||
if (array_key_exists($name.'Id', $row)) {
|
||||
$foreignEntity = $this->getMetadata()->get(['entityDefs', $entityType, 'links', $name, 'entity']);
|
||||
$foreignEntity = null;
|
||||
if (!$isForeign) {
|
||||
$foreignEntity = $this->getMetadata()->get(['entityDefs', $entityType, 'links', $name, 'entity']);
|
||||
} else {
|
||||
$foreignEntity1 = $this->getMetadata()->get(['entityDefs', $entityType, 'links', $foreignLink, 'entity']);
|
||||
$foreignEntity = $this->getMetadata()->get(['entityDefs', $foreignEntity1, 'links', $foreignField, 'entity']);
|
||||
}
|
||||
if ($foreignEntity) {
|
||||
$link = $this->getConfig()->getSiteUrl() . "/#" . $foreignEntity. "/view/". $row[$name.'Id'];
|
||||
}
|
||||
@@ -417,12 +538,15 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
|
||||
$sheet->getStyle("A2:A$rowNumber")
|
||||
->getNumberFormat()
|
||||
->setFormatCode(\PHPExcel_Style_NumberFormat::FORMAT_TEXT);
|
||||
->setFormatCode(\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_TEXT);
|
||||
|
||||
$startingRowNumber = 4;
|
||||
|
||||
foreach ($fieldList as $i => $name) {
|
||||
$col = $azRange[$i];
|
||||
if (!array_key_exists($name, $typesCache)) {
|
||||
break;
|
||||
}
|
||||
$type = $typesCache[$name];
|
||||
|
||||
switch ($type) {
|
||||
@@ -435,6 +559,11 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
->getNumberFormat()
|
||||
->setFormatCode('0');
|
||||
} break;
|
||||
case 'float': {
|
||||
$sheet->getStyle($col.$startingRowNumber.':'.$col.$rowNumber)
|
||||
->getNumberFormat()
|
||||
->setFormatCode(\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1);
|
||||
} break;
|
||||
case 'date': {
|
||||
$sheet->getStyle($col.$startingRowNumber.':'.$col.$rowNumber)
|
||||
->getNumberFormat()
|
||||
@@ -468,7 +597,7 @@ class Xlsx extends \Espo\Core\Injectable
|
||||
$sheet->getStyle($linkColumn.$startingRowNumber.':'.$linkColumn.$rowNumber)->applyFromArray($linkStyle);
|
||||
}
|
||||
|
||||
$objWriter = \PHPExcel_IOFactory::createWriter($phpExcel, 'Excel2007');
|
||||
$objWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpExcel, 'Xlsx');
|
||||
|
||||
if (!$this->getInjection('fileManager')->isDir('data/cache/')) {
|
||||
$this->getInjection('fileManager')->mkdir('data/cache/');
|
||||
|
||||
@@ -36,10 +36,10 @@ class IncludesType 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 \'Array\\Includses\' item is not array.');
|
||||
throw new Error('Value for \'Array\\Includes\' item is not array.');
|
||||
}
|
||||
if (count($item->value) < 2) {
|
||||
throw new Error('Bad arguments passed to \'Array\\Includses\'.');
|
||||
throw new Error('Bad arguments passed to \'Array\\Includes\'.');
|
||||
}
|
||||
$list = $this->evaluate($item->value[0]);
|
||||
$needle = $this->evaluate($item->value[1]);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?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\EntityGroup;
|
||||
|
||||
use \Espo\ORM\Entity;
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
|
||||
class CountRelatedType extends \Espo\Core\Formula\Functions\Base
|
||||
{
|
||||
protected function init()
|
||||
{
|
||||
$this->addDependency('entityManager');
|
||||
$this->addDependency('selectManagerFactory');
|
||||
}
|
||||
|
||||
public function process(\StdClass $item)
|
||||
{
|
||||
if (!property_exists($item, 'value')) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (!is_array($item->value)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (count($item->value) < 1) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$link = $this->evaluate($item->value[0]);
|
||||
|
||||
if (empty($link)) {
|
||||
throw new Error("No link passed to countRelated function.");
|
||||
}
|
||||
|
||||
$filter = null;
|
||||
if (count($item->value) > 1) {
|
||||
$filter = $this->evaluate($item->value[1]);
|
||||
}
|
||||
|
||||
$entity = $this->getEntity();
|
||||
|
||||
$entityManager = $this->getInjection('entityManager');
|
||||
|
||||
$foreignEntityType = $entity->getRelationParam($link, 'entity');
|
||||
|
||||
if (empty($foreignEntityType)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$foreignSelectManager = $this->getInjection('selectManagerFactory')->create($foreignEntityType);
|
||||
|
||||
$selectParams = $foreignSelectManager->getEmptySelectParams();
|
||||
|
||||
if ($filter) {
|
||||
$foreignSelectManager->applyFilter($filter, $selectParams);
|
||||
}
|
||||
|
||||
return $entityManager->getRepository($entity->getEntityType())->countRelated($entity, $link, $selectParams);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?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\EntityGroup;
|
||||
|
||||
use \Espo\ORM\Entity;
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
|
||||
class SumRelatedType extends \Espo\Core\Formula\Functions\Base
|
||||
{
|
||||
protected function init()
|
||||
{
|
||||
$this->addDependency('entityManager');
|
||||
$this->addDependency('selectManagerFactory');
|
||||
}
|
||||
|
||||
public function process(\StdClass $item)
|
||||
{
|
||||
if (!property_exists($item, 'value')) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (!is_array($item->value)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (count($item->value) < 2) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$link = $this->evaluate($item->value[0]);
|
||||
|
||||
if (empty($link)) {
|
||||
throw new Error("No link passed to sumRelated function.");
|
||||
}
|
||||
|
||||
$field = $this->evaluate($item->value[1]);
|
||||
|
||||
if (empty($field)) {
|
||||
throw new Error("No field passed to sumRelated function.");
|
||||
}
|
||||
|
||||
$filter = null;
|
||||
if (count($item->value) > 2) {
|
||||
$filter = $this->evaluate($item->value[2]);
|
||||
}
|
||||
|
||||
$entity = $this->getEntity();
|
||||
|
||||
$entityManager = $this->getInjection('entityManager');
|
||||
|
||||
$foreignEntityType = $entity->getRelationParam($link, 'entity');
|
||||
|
||||
if (empty($foreignEntityType)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$foreignSelectManager = $this->getInjection('selectManagerFactory')->create($foreignEntityType);
|
||||
|
||||
$foreignLink = $entity->getRelationParam($link, 'foreign');
|
||||
|
||||
if (empty($foreignLink)) {
|
||||
throw new Error("No foreign link for link {$link}.");
|
||||
}
|
||||
|
||||
$selectParams = $foreignSelectManager->getEmptySelectParams();
|
||||
|
||||
if ($filter) {
|
||||
$foreignSelectManager->applyFilter($filter, $selectParams);
|
||||
}
|
||||
|
||||
$selectParams['select'] = [[$foreignLink . '.id', 'foreignId'], 'SUM:' . $field];
|
||||
|
||||
$foreignSelectManager->addJoin($foreignLink, $selectParams);
|
||||
|
||||
$selectParams['groupBy'] = [$foreignLink . '.id'];
|
||||
|
||||
$entityManager->getRepository($foreignEntityType)->handleSelectParams($selectParams);
|
||||
|
||||
$sql = $entityManager->getQuery()->createSelectQuery($foreignEntityType, $selectParams);
|
||||
|
||||
$pdo = $entityManager->getPDO();
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute();
|
||||
$rowList = $sth->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($rowList)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $rowList[0]['SUM:' . $field];
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Formula\Functions\StringGroup;
|
||||
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
|
||||
class ContainsType 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\\Contains\' item is not an array.');
|
||||
}
|
||||
if (count($item->value) < 2) {
|
||||
throw new Error('Bad arguments passed to \'String\\Contains\'.');
|
||||
}
|
||||
$string = $this->evaluate($item->value[0]);
|
||||
$needle = $this->evaluate($item->value[1]);
|
||||
|
||||
if (!is_string($string)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strpos($string, $needle) !== false;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
@@ -25,44 +26,33 @@
|
||||
* 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.
|
||||
************************************************************************/
|
||||
(function ($) {
|
||||
|
||||
var root = this;
|
||||
namespace Espo\Core\Formula\Functions\StringGroup;
|
||||
|
||||
root.Espo = {};
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
|
||||
this.EspoTest = {
|
||||
class LengthType extends \Espo\Core\Formula\Functions\Base
|
||||
{
|
||||
public function process(\StdClass $item)
|
||||
{
|
||||
if (!property_exists($item, 'value')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
include: function (path, options) {
|
||||
var thisObject = root;
|
||||
if (options != undefined && 'self' in options) {
|
||||
thisObject = options.self;
|
||||
}
|
||||
if (!is_array($item->value)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
var self = this;
|
||||
if (count($item->value) < 1) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
var ajaxOptions = {
|
||||
url: path,
|
||||
async: false,
|
||||
cache: false,
|
||||
type: 'GET',
|
||||
dataType: 'text',
|
||||
success: function (script) {
|
||||
eval.call(thisObject, script);
|
||||
},
|
||||
error: function () {
|
||||
console.error("Error occured while loading " + path);
|
||||
}
|
||||
};
|
||||
$value = $this->evaluate($item->value[0]);
|
||||
|
||||
if (typeof options != 'undefined') {
|
||||
_.each(options, function (value, key) {
|
||||
ajaxOptions[key] = options[key];
|
||||
});
|
||||
}
|
||||
if (!is_string($value)) {
|
||||
$value = strval($value);
|
||||
}
|
||||
|
||||
$.ajax(ajaxOptions);
|
||||
},
|
||||
|
||||
};
|
||||
}).call(this, $);
|
||||
return mb_strlen($value);
|
||||
}
|
||||
}
|
||||
@@ -50,13 +50,19 @@ class Htmlizer
|
||||
|
||||
protected $entityManager;
|
||||
|
||||
public function __construct(FileManager $fileManager, DateTime $dateTime, NumberUtil $number, $acl = null, $entityManager = null)
|
||||
protected $metadata;
|
||||
|
||||
protected $language;
|
||||
|
||||
public function __construct(FileManager $fileManager, DateTime $dateTime, NumberUtil $number, $acl = null, $entityManager = null, $metadata = null, $language = null)
|
||||
{
|
||||
$this->fileManager = $fileManager;
|
||||
$this->dateTime = $dateTime;
|
||||
$this->number = $number;
|
||||
$this->acl = $acl;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->metadata = $metadata;
|
||||
$this->language = $language;
|
||||
}
|
||||
|
||||
protected function getAcl()
|
||||
@@ -94,10 +100,10 @@ class Htmlizer
|
||||
$forbidenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList($entity->getEntityType(), 'read');
|
||||
}
|
||||
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
if (in_array($field, $forbidenAttributeList)) continue;
|
||||
|
||||
|
||||
$type = $entity->getAttributeType($field);
|
||||
|
||||
if ($type == Entity::DATETIME) {
|
||||
@@ -115,16 +121,16 @@ class Htmlizer
|
||||
foreach ($list as $item) {
|
||||
$v = $item;
|
||||
if ($item instanceof \StdClass) {
|
||||
$v = json_decode(json_encode($v), true);
|
||||
$v = json_decode(json_encode($v, \JSON_PRESERVE_ZERO_FRACTION), true);
|
||||
}
|
||||
if (!is_array($v)) {
|
||||
$v = [];
|
||||
}
|
||||
foreach ($v as $k => $w) {
|
||||
$keyRaw = $k . '_RAW';
|
||||
$v[$keyRaw] = $v[$k];
|
||||
$v[$k] = $this->format($v[$k]);
|
||||
if (is_array($v)) {
|
||||
foreach ($v as $k => $w) {
|
||||
$keyRaw = $k . '_RAW';
|
||||
$v[$keyRaw] = $v[$k];
|
||||
$v[$k] = $this->format($v[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
$newList[] = $v;
|
||||
}
|
||||
$data[$field] = $newList;
|
||||
@@ -133,7 +139,7 @@ class Htmlizer
|
||||
if (!empty($data[$field])) {
|
||||
$value = $data[$field];
|
||||
if ($value instanceof \StdClass) {
|
||||
$data[$field] = json_decode(json_encode($value), true);
|
||||
$data[$field] = json_decode(json_encode($value, \JSON_PRESERVE_ZERO_FRACTION), true);
|
||||
}
|
||||
foreach ($data[$field] as $k => $w) {
|
||||
$keyRaw = $k . '_RAW';
|
||||
@@ -148,6 +154,14 @@ class Htmlizer
|
||||
if (array_key_exists($field, $data)) {
|
||||
$keyRaw = $field . '_RAW';
|
||||
$data[$keyRaw] = $data[$field];
|
||||
|
||||
$fieldType = $this->getFieldType($entity->getEntityType(), $field);
|
||||
if ($fieldType === 'enum') {
|
||||
if ($this->language) {
|
||||
$data[$field] = $this->language->translateOption($data[$field], $field, $entity->getEntityType());
|
||||
}
|
||||
}
|
||||
|
||||
$data[$field] = $this->format($data[$field]);
|
||||
}
|
||||
}
|
||||
@@ -205,6 +219,14 @@ class Htmlizer
|
||||
return number_format($number, $decimals, $decimalPoint, $thousandsSeparator);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
'var' => function ($context, $options) {
|
||||
if ($context && isset($context[0]) && isset($context[1])) {
|
||||
if (isset($context[1][$context[0]])) {
|
||||
return $context[1][$context[0]];
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
],
|
||||
'hbhelpers' => [
|
||||
@@ -246,13 +268,20 @@ class Htmlizer
|
||||
|
||||
$data = $this->getDataFromEntity($entity, $skipLinks);
|
||||
|
||||
if (!array_key_exists('today', $data)) {
|
||||
$data['today'] = $this->dateTime->getTodayString();
|
||||
}
|
||||
|
||||
if (!array_key_exists('now', $data)) {
|
||||
$data['now'] = $this->dateTime->getNowString();
|
||||
}
|
||||
|
||||
foreach ($additionalData as $k => $value) {
|
||||
$data[$k] = $value;
|
||||
}
|
||||
|
||||
$html = $renderer($data);
|
||||
|
||||
|
||||
$html = str_replace('?entryPoint=attachment&', '?entryPoint=attachment&', $html);
|
||||
|
||||
if ($this->getEntityManager()) {
|
||||
@@ -269,4 +298,9 @@ class Htmlizer
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
protected function getFieldType($entityType, $field) {
|
||||
if (!$this->metadata) return;
|
||||
return $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']);
|
||||
}
|
||||
}
|
||||
@@ -154,31 +154,8 @@ class Importer
|
||||
}
|
||||
|
||||
if ($duplicate = $this->findDuplicate($email)) {
|
||||
if ($assignedUserId) {
|
||||
$duplicate->addLinkMultipleId('users', $assignedUserId);
|
||||
$duplicate->addLinkMultipleId('assignedUsers', $assignedUserId);
|
||||
}
|
||||
if (!empty($userIdList)) {
|
||||
foreach ($userIdList as $uId) {
|
||||
$duplicate->addLinkMultipleId('users', $uId);
|
||||
}
|
||||
}
|
||||
|
||||
if ($folderData) {
|
||||
foreach ($folderData as $uId => $folderId) {
|
||||
$email->setLinkMultipleColumn('users', 'folderId', $uId, $folderId);
|
||||
}
|
||||
}
|
||||
|
||||
$duplicate->set('isBeingImported', true);
|
||||
|
||||
$this->getEntityManager()->saveEntity($duplicate);
|
||||
|
||||
if (!empty($teamsIdList)) {
|
||||
foreach ($teamsIdList as $teamId) {
|
||||
$this->getEntityManager()->getRepository('Email')->relate($duplicate, 'teams', $teamId);
|
||||
}
|
||||
}
|
||||
$duplicate = $this->getEntityManager()->getEntity('Email', $duplicate->id);
|
||||
$this->processDuplicate($duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList);
|
||||
return $duplicate;
|
||||
}
|
||||
|
||||
@@ -228,6 +205,10 @@ class Importer
|
||||
))->findOne();
|
||||
if ($replied) {
|
||||
$email->set('repliedId', $replied->id);
|
||||
$repliedTeamIdList = $replied->getLinkMultipleIdList('teams');
|
||||
foreach ($repliedTeamIdList as $repliedTeamId) {
|
||||
$email->addLinkMultipleId('teams', $repliedTeamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +280,43 @@ class Importer
|
||||
}
|
||||
}
|
||||
|
||||
$this->getEntityManager()->getPdo()->query('LOCK TABLES `email` WRITE');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (!$email->get('messageId')) {
|
||||
$email->setDummyMessageId();
|
||||
}
|
||||
|
||||
$this->getEntityManager()->saveEntity($email, [
|
||||
'skipAll' => true,
|
||||
'keepNew' => true
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
|
||||
|
||||
if ($parentFound) {
|
||||
$parentType = $email->get('parentType');
|
||||
$parentId = $email->get('parentId');
|
||||
$emailKeepParentTeamsEntityList = $this->getConfig()->get('emailKeepParentTeamsEntityList', []);
|
||||
if ($parentId && in_array($parentType, $emailKeepParentTeamsEntityList)) {
|
||||
if ($this->getEntityManager()->hasRepository($parentType)) {
|
||||
$parent = $this->getEntityManager()->getEntity($parentType, $parentId);
|
||||
if ($parent) {
|
||||
$parentTeamIdList = $parent->getLinkMultipleIdList('teams');
|
||||
foreach ($parentTeamIdList as $parentTeamId) {
|
||||
$email->addLinkMultipleId('teams', $parentTeamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->getEntityManager()->saveEntity($email);
|
||||
|
||||
foreach ($inlineAttachmentList as $attachment) {
|
||||
@@ -352,12 +370,41 @@ class Importer
|
||||
protected function findDuplicate(Entity $email)
|
||||
{
|
||||
if ($email->get('messageId')) {
|
||||
$duplicate = $this->getEntityManager()->getRepository('Email')->where(array(
|
||||
$duplicate = $this->getEntityManager()->getRepository('Email')->select(['id'])->where(array(
|
||||
'messageId' => $email->get('messageId')
|
||||
))->findOne();
|
||||
))->findOne(['skipAdditionalSelectParams' => true]);
|
||||
if ($duplicate) {
|
||||
return $duplicate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function processDuplicate(Entity $duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList)
|
||||
{
|
||||
if ($assignedUserId) {
|
||||
$duplicate->addLinkMultipleId('users', $assignedUserId);
|
||||
$duplicate->addLinkMultipleId('assignedUsers', $assignedUserId);
|
||||
}
|
||||
if (!empty($userIdList)) {
|
||||
foreach ($userIdList as $uId) {
|
||||
$duplicate->addLinkMultipleId('users', $uId);
|
||||
}
|
||||
}
|
||||
|
||||
if ($folderData) {
|
||||
foreach ($folderData as $uId => $folderId) {
|
||||
$duplicate->setLinkMultipleColumn('users', 'folderId', $uId, $folderId);
|
||||
}
|
||||
}
|
||||
|
||||
$duplicate->set('isBeingImported', true);
|
||||
|
||||
$this->getEntityManager()->saveEntity($duplicate);
|
||||
|
||||
if (!empty($teamsIdList)) {
|
||||
foreach ($teamsIdList as $teamId) {
|
||||
$this->getEntityManager()->getRepository('Email')->relate($duplicate, 'teams', $teamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,9 +179,10 @@ class MailMimeParser
|
||||
} else {
|
||||
$email->set('isHtml', false);
|
||||
$email->set('body', $bodyPlain);
|
||||
$email->set('bodyPlain', $bodyPlain);
|
||||
}
|
||||
|
||||
if (!$email->get('body') && $email->get('bodyPlain')) {
|
||||
if (!$email->get('body') && $email->hasBodyPlain()) {
|
||||
$email->set('body', $email->get('bodyPlain'));
|
||||
}
|
||||
|
||||
|
||||
@@ -165,9 +165,10 @@ class PhpMimeMailParser
|
||||
} else {
|
||||
$email->set('isHtml', false);
|
||||
$email->set('body', $bodyPlain);
|
||||
$email->set('bodyPlain', $bodyPlain);
|
||||
}
|
||||
|
||||
if (!$email->get('body') && $email->get('bodyPlain')) {
|
||||
if (!$email->get('body') && $email->hasBodyPlain()) {
|
||||
$email->set('body', $email->get('bodyPlain'));
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ use \PhpMimeMailParser\Attachment;
|
||||
|
||||
class Parser extends \PhpMimeMailParser\Parser
|
||||
{
|
||||
public function getAttachments()
|
||||
public function getAttachments($include_inline = true)
|
||||
{
|
||||
$attachments = [];
|
||||
$dispositions = ['attachment', 'inline'];
|
||||
|
||||
@@ -139,7 +139,7 @@ class ZendMail
|
||||
$this->importPartDataToEmail($email, $zendMessage, $inlineIds, 'text/plain', $inlineAttachmentList);
|
||||
}
|
||||
|
||||
if (!$email->get('body') && $email->get('bodyPlain')) {
|
||||
if (!$email->get('body') && $email->hasBodyPlain()) {
|
||||
$email->set('body', $email->get('bodyPlain'));
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ class ZendMail
|
||||
$content = $this->getContentFromPart($part);
|
||||
if ($type == 'text/plain') {
|
||||
$bodyPlain = '';
|
||||
if ($email->get('bodyPlain')) {
|
||||
if ($email->hasBodyPlain()) {
|
||||
$bodyPlain .= $email->get('bodyPlain') . "\n";
|
||||
}
|
||||
$bodyPlain .= $content;
|
||||
|
||||
@@ -260,7 +260,7 @@ class Sender
|
||||
$attachment = new MimePart(file_get_contents($fileName));
|
||||
$attachment->disposition = Mime::DISPOSITION_ATTACHMENT;
|
||||
$attachment->encoding = Mime::ENCODING_BASE64;
|
||||
$attachment->filename = $a->get('name');
|
||||
$attachment->filename ='=?utf-8?B?' . base64_encode($a->get('name')) . '?=';
|
||||
if ($a->get('type')) {
|
||||
$attachment->type = $a->get('type');
|
||||
}
|
||||
@@ -349,7 +349,7 @@ class Sender
|
||||
|
||||
try {
|
||||
$messageId = $email->get('messageId');
|
||||
if (empty($messageId) || !is_string($messageId) || strlen($messageId) < 4) {
|
||||
if (empty($messageId) || !is_string($messageId) || strlen($messageId) < 4 || strpos($messageId, 'dummy:') === 0) {
|
||||
$messageId = $this->generateMessageId($email);
|
||||
$email->set('messageId', '<' . $messageId . '>');
|
||||
} else {
|
||||
|
||||
@@ -92,11 +92,27 @@ class Base implements Injectable
|
||||
|
||||
public function process(Entity $entity)
|
||||
{
|
||||
if (!$entity->get('assignedUserId')) return;
|
||||
if (!$entity->isFieldChanged('assignedUserId')) return;
|
||||
if ($entity->hasLinkMultipleField('assignedUsers')) {
|
||||
$userIdList = $entity->getLinkMultipleIdList('assignedUsers');
|
||||
$fetchedAssignedUserIdList = $entity->getFetched('assignedUsersIds');
|
||||
if (!is_array($fetchedAssignedUserIdList)) {
|
||||
$fetchedAssignedUserIdList = [];
|
||||
}
|
||||
|
||||
$assignedUserId = $entity->get('assignedUserId');
|
||||
foreach ($userIdList as $userId) {
|
||||
if (in_array($userId, $fetchedAssignedUserIdList)) continue;
|
||||
$this->processForUser($entity, $userId);
|
||||
}
|
||||
} else {
|
||||
if (!$entity->get('assignedUserId')) return;
|
||||
if (!$entity->isAttributeChanged('assignedUserId')) return;
|
||||
$assignedUserId = $entity->get('assignedUserId');
|
||||
$this->processForUser($entity, $assignedUserId);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processForUser(Entity $entity, $assignedUserId)
|
||||
{
|
||||
if ($entity->hasAttribute('createdById') && $entity->hasAttribute('modifiedById')) {
|
||||
if ($entity->isNew()) {
|
||||
$isNotSelfAssignment = $assignedUserId !== $entity->get('createdById');
|
||||
|
||||
@@ -41,6 +41,29 @@ class Entity extends \Espo\ORM\Entity
|
||||
return $this->hasAttribute($field . 'Id');
|
||||
}
|
||||
|
||||
public function loadParentNameField($field)
|
||||
{
|
||||
if (!$this->hasAttribute($field. 'Id') || !$this->hasAttribute($field . 'Type')) return;
|
||||
|
||||
$parentId = $this->get($field . 'Id');
|
||||
$parentType = $this->get($field . 'Type');
|
||||
|
||||
if ($parentId && $parentType) {
|
||||
if (!$this->entityManager->hasRepository($parentType)) return;
|
||||
$repository = $this->entityManager->getRepository($parentType);
|
||||
|
||||
$select = ['id', 'name'];
|
||||
$foreignEntity = $repository->select($select)->where(['id' => $parentId])->findOne();
|
||||
if ($foreignEntity) {
|
||||
$this->set($field . 'Name', $foreignEntity->get('name'));
|
||||
} else {
|
||||
$this->set($field . 'Name', null);
|
||||
}
|
||||
} else {
|
||||
$this->set($field . 'Name', null);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadLinkMultipleField($field, $columns = null)
|
||||
{
|
||||
if (!$this->hasRelation($field) || !$this->hasAttribute($field . 'Ids')) return;
|
||||
@@ -50,31 +73,50 @@ class Entity extends \Espo\ORM\Entity
|
||||
$defs['additionalColumns'] = $columns;
|
||||
}
|
||||
|
||||
$idsAttribute = $field . 'Ids';
|
||||
|
||||
$foreignEntityType = $this->getRelationParam($field, 'entity');
|
||||
if ($foreignEntityType && $this->entityManager) {
|
||||
$foreignEntityDefs = $this->entityManager->getMetadata()->get($foreignEntityType);
|
||||
if ($foreignEntityDefs && !empty($foreignEntityDefs['collection'])) {
|
||||
$collectionDefs = $foreignEntityDefs['collection'];
|
||||
if (!empty($foreignEntityDefs['collection']['orderBy'])) {
|
||||
$orderBy = $foreignEntityDefs['collection']['orderBy'];
|
||||
$order = 'ASC';
|
||||
if (array_key_exists('order', $foreignEntityDefs['collection'])) {
|
||||
$order = $foreignEntityDefs['collection']['order'];
|
||||
}
|
||||
if (array_key_exists($orderBy, $foreignEntityDefs['fields'])) {
|
||||
$defs['orderBy'] = $orderBy;
|
||||
$defs['order'] = $order;
|
||||
|
||||
if ($this->getAttributeParam($idsAttribute, 'orderBy')) {
|
||||
$defs['orderBy'] = $this->getAttributeParam($idsAttribute, 'orderBy');
|
||||
$defs['order'] = 'ASC';
|
||||
if ($this->getAttributeParam($idsAttribute, 'orderDirection')) {
|
||||
$defs['order'] = $this->getAttributeParam($idsAttribute, 'orderDirection');
|
||||
}
|
||||
} else {
|
||||
if ($foreignEntityType && $this->entityManager) {
|
||||
$foreignEntityDefs = $this->entityManager->getMetadata()->get($foreignEntityType);
|
||||
if ($foreignEntityDefs && !empty($foreignEntityDefs['collection'])) {
|
||||
$collectionDefs = $foreignEntityDefs['collection'];
|
||||
if (!empty($foreignEntityDefs['collection']['orderBy'])) {
|
||||
$orderBy = $foreignEntityDefs['collection']['orderBy'];
|
||||
$order = 'ASC';
|
||||
if (array_key_exists('order', $foreignEntityDefs['collection'])) {
|
||||
$order = $foreignEntityDefs['collection']['order'];
|
||||
}
|
||||
if (array_key_exists($orderBy, $foreignEntityDefs['fields'])) {
|
||||
$defs['orderBy'] = $orderBy;
|
||||
$defs['order'] = $order;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$defs['select'] = ['id', 'name'];
|
||||
|
||||
$hasType = false;
|
||||
if ($this->hasField($field . 'Types')) {
|
||||
$hasType = true;
|
||||
$defs['select'][] = 'type';
|
||||
}
|
||||
|
||||
$collection = $this->get($field, $defs);
|
||||
$ids = array();
|
||||
$names = new \stdClass();
|
||||
$types = new \stdClass();
|
||||
$ids = [];
|
||||
$names = (object) [];
|
||||
$types = (object) [];
|
||||
if (!empty($columns)) {
|
||||
$columnsData = new \stdClass();
|
||||
$columnsData = (object) [];
|
||||
}
|
||||
|
||||
if ($collection) {
|
||||
@@ -82,7 +124,9 @@ class Entity extends \Espo\ORM\Entity
|
||||
$id = $e->id;
|
||||
$ids[] = $id;
|
||||
$names->$id = $e->get('name');
|
||||
$types->$id = $e->get('type');
|
||||
if ($hasType) {
|
||||
$types->$id = $e->get('type');
|
||||
}
|
||||
if (!empty($columns)) {
|
||||
$columnsData->$id = new \stdClass();
|
||||
foreach ($columns as $column => $f) {
|
||||
@@ -92,9 +136,15 @@ class Entity extends \Espo\ORM\Entity
|
||||
}
|
||||
}
|
||||
|
||||
$this->set($field . 'Ids', $ids);
|
||||
$this->set($idsAttribute, $ids);
|
||||
if (!$this->hasFetched($idsAttribute)) {
|
||||
$this->setFetched($idsAttribute, $ids);
|
||||
}
|
||||
|
||||
$this->set($field . 'Names', $names);
|
||||
$this->set($field . 'Types', $types);
|
||||
if ($hasType) {
|
||||
$this->set($field . 'Types', $types);
|
||||
}
|
||||
if (!empty($columns)) {
|
||||
$this->set($field . 'Columns', $columnsData);
|
||||
}
|
||||
@@ -105,7 +155,13 @@ class Entity extends \Espo\ORM\Entity
|
||||
if (!$this->hasRelation($field) || !$this->hasAttribute($field . 'Id')) return;
|
||||
if ($this->getRelationType($field) !== 'hasOne' && $this->getRelationType($field) !== 'belongsTo') return;
|
||||
|
||||
$entity = $this->get($field);
|
||||
$relatedEntityType = $this->getRelationParam($field, 'entity');
|
||||
|
||||
$select = ['id', 'name'];
|
||||
|
||||
$entity = $this->get($field, [
|
||||
'select' => $select
|
||||
]);
|
||||
|
||||
$entityId = null;
|
||||
$entityName = null;
|
||||
|
||||
@@ -40,7 +40,9 @@ use \Espo\Core\Interfaces\Injectable;
|
||||
class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
{
|
||||
protected $dependencies = array(
|
||||
'metadata'
|
||||
'metadata',
|
||||
'config',
|
||||
'fieldManagerUtil'
|
||||
);
|
||||
|
||||
protected $injections = array();
|
||||
@@ -51,6 +53,8 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
|
||||
protected $processFieldsAfterSaveDisabled = false;
|
||||
|
||||
protected $processFieldsBeforeSaveDisabled = false;
|
||||
|
||||
protected function addDependency($name)
|
||||
{
|
||||
$this->dependencies[] = $name;
|
||||
@@ -83,6 +87,16 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
return $this->getInjection('metadata');
|
||||
}
|
||||
|
||||
protected function getConfig()
|
||||
{
|
||||
return $this->getInjection('config');
|
||||
}
|
||||
|
||||
protected function getFieldManagerUtil()
|
||||
{
|
||||
return $this->getInjection('fieldManagerUtil');
|
||||
}
|
||||
|
||||
public function __construct($entityType, EntityManager $entityManager, EntityFactory $entityFactory)
|
||||
{
|
||||
parent::__construct($entityType, $entityManager, $entityFactory);
|
||||
@@ -176,7 +190,7 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
protected function beforeRemove(Entity $entity, array $options = array())
|
||||
{
|
||||
parent::beforeRemove($entity, $options);
|
||||
if (!$this->hooksDisabled) {
|
||||
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
|
||||
$this->getEntityManager()->getHookManager()->process($this->entityType, 'beforeRemove', $entity, $options);
|
||||
}
|
||||
|
||||
@@ -192,14 +206,14 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
protected function afterRemove(Entity $entity, array $options = array())
|
||||
{
|
||||
parent::afterRemove($entity, $options);
|
||||
if (!$this->hooksDisabled) {
|
||||
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
|
||||
$this->getEntityManager()->getHookManager()->process($this->entityType, 'afterRemove', $entity, $options);
|
||||
}
|
||||
}
|
||||
|
||||
protected function afterMassRelate(Entity $entity, $relationName, array $params = array(), array $options = array())
|
||||
{
|
||||
if (!$this->hooksDisabled) {
|
||||
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
|
||||
$hookData = array(
|
||||
'relationName' => $relationName,
|
||||
'relationParams' => $params
|
||||
@@ -220,7 +234,7 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
|
||||
if ($foreign instanceof Entity) {
|
||||
$foreignEntity = $foreign;
|
||||
if (!$this->hooksDisabled) {
|
||||
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
|
||||
$hookData = array(
|
||||
'relationName' => $relationName,
|
||||
'relationData' => $data,
|
||||
@@ -237,7 +251,7 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
|
||||
if ($foreign instanceof Entity) {
|
||||
$foreignEntity = $foreign;
|
||||
if (!$this->hooksDisabled) {
|
||||
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
|
||||
$hookData = array(
|
||||
'relationName' => $relationName,
|
||||
'foreignEntity' => $foreignEntity
|
||||
@@ -251,9 +265,13 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
{
|
||||
parent::beforeSave($entity, $options);
|
||||
|
||||
if (!$this->hooksDisabled) {
|
||||
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
|
||||
$this->getEntityManager()->getHookManager()->process($this->entityType, 'beforeSave', $entity, $options);
|
||||
}
|
||||
|
||||
if (!$this->processFieldsBeforeSaveDisabled) {
|
||||
$this->processCurrencyFieldsBeforeSave($entity);
|
||||
}
|
||||
}
|
||||
|
||||
protected function afterSave(Entity $entity, array $options = array())
|
||||
@@ -271,7 +289,7 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
$this->processFileFieldsSave($entity);
|
||||
}
|
||||
|
||||
if (!$this->hooksDisabled) {
|
||||
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
|
||||
$this->getEntityManager()->getHookManager()->process($this->entityType, 'afterSave', $entity, $options);
|
||||
}
|
||||
}
|
||||
@@ -285,35 +303,40 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
if (!$entity->has('id')) {
|
||||
$entity->set('id', Util::generateId());
|
||||
}
|
||||
}
|
||||
|
||||
if ($entity->hasAttribute('createdAt')) {
|
||||
if (empty($options['import']) || !$entity->has('createdAt')) {
|
||||
$entity->set('createdAt', $nowString);
|
||||
}
|
||||
}
|
||||
if ($entity->hasAttribute('modifiedAt')) {
|
||||
$entity->set('modifiedAt', $nowString);
|
||||
}
|
||||
if ($entity->hasAttribute('createdById')) {
|
||||
if (empty($options['skipCreatedBy']) && (empty($options['import']) || !$entity->has('createdById'))) {
|
||||
if ($this->getEntityManager()->getUser()) {
|
||||
$entity->set('createdById', $this->getEntityManager()->getUser()->id);
|
||||
if (empty($options['skipAll'])) {
|
||||
if ($entity->isNew()) {
|
||||
if ($entity->hasAttribute('createdAt')) {
|
||||
if (empty($options['import']) || !$entity->has('createdAt')) {
|
||||
$entity->set('createdAt', $nowString);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (empty($options['silent']) && empty($options['skipModifiedBy'])) {
|
||||
if ($entity->hasAttribute('modifiedAt')) {
|
||||
$entity->set('modifiedAt', $nowString);
|
||||
}
|
||||
if ($entity->hasAttribute('modifiedById')) {
|
||||
if ($this->getEntityManager()->getUser()) {
|
||||
$entity->set('modifiedById', $this->getEntityManager()->getUser()->id);
|
||||
$entity->set('modifiedByName', $this->getEntityManager()->getUser()->get('name'));
|
||||
if ($entity->hasAttribute('createdById')) {
|
||||
if (empty($options['skipCreatedBy']) && (empty($options['import']) || !$entity->has('createdById'))) {
|
||||
if ($this->getEntityManager()->getUser()) {
|
||||
$entity->set('createdById', $this->getEntityManager()->getUser()->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (empty($options['silent']) && empty($options['skipModifiedBy'])) {
|
||||
if ($entity->hasAttribute('modifiedAt')) {
|
||||
$entity->set('modifiedAt', $nowString);
|
||||
}
|
||||
if ($entity->hasAttribute('modifiedById')) {
|
||||
if ($this->getEntityManager()->getUser()) {
|
||||
$entity->set('modifiedById', $this->getEntityManager()->getUser()->id);
|
||||
$entity->set('modifiedByName', $this->getEntityManager()->getUser()->get('name'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->restoreData = $restoreData;
|
||||
|
||||
$result = parent::save($entity, $options);
|
||||
@@ -321,6 +344,28 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getFieldByTypeList($type)
|
||||
{
|
||||
return $this->getFieldManagerUtil()->getFieldByTypeList($this->entityType, $type);
|
||||
}
|
||||
|
||||
protected function processCurrencyFieldsBeforeSave(Entity $entity)
|
||||
{
|
||||
foreach ($this->getFieldByTypeList('currency') as $field) {
|
||||
$currencyAttribute = $field . 'Currency';
|
||||
$defaultCurrency = $this->getConfig()->get('defaultCurrency');
|
||||
if ($entity->isNew()) {
|
||||
if ($entity->get($field) && !$entity->get($currencyAttribute)) {
|
||||
$entity->set($currencyAttribute, $defaultCurrency);
|
||||
}
|
||||
} else {
|
||||
if ($entity->isAttributeChanged($field) && $entity->has($currencyAttribute) && !$entity->get($currencyAttribute)) {
|
||||
$entity->set($currencyAttribute, $defaultCurrency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function processFileFieldsSave(Entity $entity)
|
||||
{
|
||||
foreach ($entity->getRelations() as $name => $defs) {
|
||||
@@ -363,14 +408,14 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
protected function processEmailAddressSave(Entity $entity)
|
||||
{
|
||||
if ($entity->hasRelation('emailAddresses') && $entity->hasAttribute('emailAddress')) {
|
||||
$emailAddressRepository = $this->getEntityManager()->getRepository('EmailAddress')->storeEntityEmailAddress($entity);
|
||||
$this->getEntityManager()->getRepository('EmailAddress')->storeEntityEmailAddress($entity);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processPhoneNumberSave(Entity $entity)
|
||||
{
|
||||
if ($entity->hasRelation('phoneNumbers') && $entity->hasAttribute('phoneNumber')) {
|
||||
$emailAddressRepository = $this->getEntityManager()->getRepository('PhoneNumber')->storeEntityPhoneNumber($entity);
|
||||
$this->getEntityManager()->getRepository('PhoneNumber')->storeEntityPhoneNumber($entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ class Tcpdf extends \TCPDF
|
||||
|
||||
protected $footerPosition = 15;
|
||||
|
||||
protected $useGroupNumbers = false;
|
||||
|
||||
public function setUseGroupNumbers($value)
|
||||
{
|
||||
$this->useGroupNumbers = $value;
|
||||
}
|
||||
|
||||
public function setFooterHtml($html)
|
||||
{
|
||||
$this->footerHtml = $html;
|
||||
@@ -51,10 +58,26 @@ class Tcpdf extends \TCPDF
|
||||
}
|
||||
|
||||
public function Footer() {
|
||||
$breakMargin = $this->getBreakMargin();
|
||||
$autoPageBreak = $this->AutoPageBreak;
|
||||
|
||||
$this->SetAutoPageBreak(false, 0);
|
||||
|
||||
$this->SetY((-1) * $this->footerPosition);
|
||||
|
||||
$html = str_replace('{pageNumber}', '{:pnp:}', $this->footerHtml);
|
||||
$html = $this->footerHtml;
|
||||
|
||||
if ($this->useGroupNumbers) {
|
||||
$html = str_replace('{pageNumber}', '{:png:}', $html);
|
||||
$html = str_replace('{pageAbsoluteNumber}', '{:pnp:}', $html);
|
||||
} else {
|
||||
$html = str_replace('{pageNumber}', '{:pnp:}', $html);
|
||||
$html = str_replace('{pageAbsoluteNumber}', '{:pnp:}', $html);
|
||||
}
|
||||
|
||||
$this->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, '', 0, false, 'T');
|
||||
|
||||
$this->SetAutoPageBreak($autoPageBreak, $breakMargin);
|
||||
}
|
||||
|
||||
protected function _putpages() {
|
||||
@@ -91,6 +114,9 @@ class Tcpdf extends \TCPDF
|
||||
++$pagegroupnum;
|
||||
$pnga = TCPDF_STATIC::formatPageNumber($pagegroupnum);
|
||||
$pngu = TCPDF_FONTS::UTF8ToUTF16BE($pnga, false, $this->isunicode, $this->CurrentFont);
|
||||
|
||||
$pnga = $pngu;
|
||||
|
||||
$png_num_chars = $this->GetNumChars($pnga);
|
||||
// replace page numbers
|
||||
$replace = array();
|
||||
|
||||
@@ -41,6 +41,8 @@ class AclManager extends \Espo\Core\AclManager
|
||||
|
||||
private $portal = null;
|
||||
|
||||
protected $userAclClassName = '\\Espo\\Core\\Portal\\Acl';
|
||||
|
||||
public function getImplementation($scope)
|
||||
{
|
||||
if (empty($this->implementationHashMap[$scope])) {
|
||||
|
||||
@@ -99,6 +99,6 @@ class Application extends \Espo\Core\Application
|
||||
$this->getContainer()->get('clientManager')->display(null, 'html/portal.html', array(
|
||||
'portalId' => $this->getPortal()->id
|
||||
));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class CategoryTree extends \Espo\Core\ORM\Repositories\RDB
|
||||
$query = $this->getEntityManager()->getQuery();
|
||||
|
||||
$parentId = $entity->get('parentId');
|
||||
$pathsTableName = $query->toDb($entity->getEntityType() . 'Path');
|
||||
$pathsTableName = $query->toDb($query->sanitize($entity->getEntityType()) . 'Path');
|
||||
|
||||
if ($entity->isNew()) {
|
||||
if ($parentId) {
|
||||
@@ -62,7 +62,7 @@ class CategoryTree extends \Espo\Core\ORM\Repositories\RDB
|
||||
}
|
||||
$pdo->query($sql);
|
||||
} else {
|
||||
if ($entity->isFieldChanged('parentId')) {
|
||||
if ($entity->isAttributeChanged('parentId')) {
|
||||
$sql = "
|
||||
DELETE a FROM `".$pathsTableName."` AS a
|
||||
JOIN `".$pathsTableName."` AS d ON a.descendor_id = d.descendor_id
|
||||
@@ -93,7 +93,7 @@ class CategoryTree extends \Espo\Core\ORM\Repositories\RDB
|
||||
$pdo = $this->getEntityManager()->getPDO();
|
||||
$query = $this->getEntityManager()->getQuery();
|
||||
|
||||
$pathsTableName = $query->toDb($entity->getEntityType() . 'Path');
|
||||
$pathsTableName = $query->toDb($query->sanitize($entity->getEntityType()) . 'Path');
|
||||
|
||||
$sql = "DELETE FROM `".$pathsTableName."` WHERE descendor_id = ".$pdo->quote($entity->id)."";
|
||||
$pdo->query($sql);
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Core;
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
|
||||
use \Espo\Core\Utils\Util;
|
||||
use \Espo\Core\InjectableFactory;
|
||||
|
||||
class SelectManagerFactory
|
||||
{
|
||||
@@ -43,7 +44,9 @@ class SelectManagerFactory
|
||||
|
||||
private $metadata;
|
||||
|
||||
public function __construct($entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Utils\Metadata $metadata, Utils\Config $config)
|
||||
private $injectableFactory;
|
||||
|
||||
public function __construct($entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Utils\Metadata $metadata, Utils\Config $config, InjectableFactory $injectableFactory)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->user = $user;
|
||||
@@ -51,9 +54,10 @@ class SelectManagerFactory
|
||||
$this->aclManager = $aclManager;
|
||||
$this->metadata = $metadata;
|
||||
$this->config = $config;
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
}
|
||||
|
||||
public function create($entityType)
|
||||
public function create($entityType, $user = null)
|
||||
{
|
||||
$normalizedName = Util::normilizeClassName($entityType);
|
||||
|
||||
@@ -70,7 +74,14 @@ class SelectManagerFactory
|
||||
}
|
||||
}
|
||||
|
||||
$selectManager = new $className($this->entityManager, $this->user, $this->acl, $this->aclManager, $this->metadata, $this->config);
|
||||
if ($user) {
|
||||
$acl = $this->aclManager->createUserAcl($user);
|
||||
} else {
|
||||
$acl = $this->acl;
|
||||
$user = $this->user;
|
||||
}
|
||||
|
||||
$selectManager = new $className($this->entityManager, $user, $acl, $this->aclManager, $this->metadata, $this->config, $this->injectableFactory);
|
||||
$selectManager->setEntityType($entityType);
|
||||
|
||||
return $selectManager;
|
||||
|
||||
@@ -36,6 +36,7 @@ use \Espo\Core\Acl;
|
||||
use \Espo\Core\AclManager;
|
||||
use \Espo\Core\Utils\Metadata;
|
||||
use \Espo\Core\Utils\Config;
|
||||
use \Espo\Core\InjectableFactory;
|
||||
|
||||
class Base
|
||||
{
|
||||
@@ -63,15 +64,19 @@ class Base
|
||||
|
||||
const MIN_LENGTH_FOR_CONTENT_SEARCH = 4;
|
||||
|
||||
public function __construct($entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Metadata $metadata, Config $config)
|
||||
const MIN_LENGTH_FOR_FULL_TEXT_SEARCH = 4;
|
||||
|
||||
protected $fullTextSearchDataCacheHash = [];
|
||||
|
||||
public function __construct($entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Metadata $metadata, Config $config, InjectableFactory $injectableFactory)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->user = $user;
|
||||
$this->acl = $acl;
|
||||
$this->aclManager = $aclManager;
|
||||
|
||||
$this->metadata = $metadata;
|
||||
$this->config = $config;
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
}
|
||||
|
||||
protected function getEntityManager()
|
||||
@@ -104,6 +109,11 @@ class Base
|
||||
return $this->aclManager;
|
||||
}
|
||||
|
||||
protected function getInjectableFactory()
|
||||
{
|
||||
return $this->injectableFactory;
|
||||
}
|
||||
|
||||
public function setEntityType($entityType)
|
||||
{
|
||||
$this->entityType = $entityType;
|
||||
@@ -140,7 +150,7 @@ class Base
|
||||
} else {
|
||||
$orderPart = 'DESC';
|
||||
}
|
||||
$result['orderBy'] = [[$sortBy . 'Country', $orderPart], [$sortBy . 'City', $orderPart], [$sortBy . 'Street', $orderPart]];
|
||||
$result['orderBy'] = [[$sortBy . 'Country', $orderPart], [$sortBy . 'City', $orderPart], [$sortBy . '_eet', $orderPart]];
|
||||
return;
|
||||
} else if ($type === 'enum') {
|
||||
$list = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'fields', $sortBy, 'options']);
|
||||
@@ -186,7 +196,6 @@ class Base
|
||||
{
|
||||
$this->prepareResult($result);
|
||||
|
||||
$whereClause = array();
|
||||
foreach ($where as $item) {
|
||||
if (!isset($item['type'])) continue;
|
||||
|
||||
@@ -198,8 +207,8 @@ class Base
|
||||
}
|
||||
$this->applyBoolFilter($filter, $result);
|
||||
}
|
||||
} else if ($item['type'] == 'textFilter' && !empty($item['value'])) {
|
||||
if (!empty($item['value'])) {
|
||||
} else if ($item['type'] == 'textFilter') {
|
||||
if (isset($item['value']) || $item['value'] !== '') {
|
||||
$this->textFilter($item['value'], $result);
|
||||
}
|
||||
} else if ($item['type'] == 'primary' && !empty($item['value'])) {
|
||||
@@ -207,10 +216,17 @@ class Base
|
||||
}
|
||||
}
|
||||
|
||||
$whereClause = $this->convertWhere($where, false, $result);
|
||||
|
||||
$result['whereClause'] = array_merge($result['whereClause'], $whereClause);
|
||||
}
|
||||
|
||||
public function convertWhere(array $where, $ignoreAdditionaFilterTypes = false, &$result = null)
|
||||
{
|
||||
$whereClause = [];
|
||||
|
||||
$ignoreTypeList = array_merge(['bool', 'primary'], $this->additionalFilterTypeList);
|
||||
|
||||
$additionalFilters = array();
|
||||
foreach ($where as $item) {
|
||||
if (!isset($item['type'])) continue;
|
||||
|
||||
@@ -221,7 +237,7 @@ class Base
|
||||
$whereClause[] = $part;
|
||||
}
|
||||
} else {
|
||||
if (in_array($type, $this->additionalFilterTypeList)) {
|
||||
if (!$ignoreAdditionaFilterTypes && in_array($type, $this->additionalFilterTypeList)) {
|
||||
if (!empty($item['value'])) {
|
||||
$methodName = 'apply' . ucfirst($type);
|
||||
|
||||
@@ -242,7 +258,7 @@ class Base
|
||||
}
|
||||
}
|
||||
|
||||
$result['whereClause'] = array_merge($result['whereClause'], $whereClause);
|
||||
return $whereClause;
|
||||
}
|
||||
|
||||
protected function applyLinkedWith($link, $idsValue, &$result)
|
||||
@@ -375,7 +391,7 @@ class Base
|
||||
|
||||
protected function q($params, &$result)
|
||||
{
|
||||
if (!empty($params['q'])) {
|
||||
if (isset($params['q']) && $params['q'] !== '') {
|
||||
$this->textFilter($params['q'], $result);
|
||||
}
|
||||
}
|
||||
@@ -389,7 +405,7 @@ class Base
|
||||
public function manageTextFilter($textFilter, &$result)
|
||||
{
|
||||
$this->prepareResult($result);
|
||||
$this->q(array('q' => $textFilter), $result);
|
||||
$this->q(['q' => $textFilter], $result);
|
||||
}
|
||||
|
||||
public function getEmptySelectParams()
|
||||
@@ -472,9 +488,9 @@ class Base
|
||||
{
|
||||
if ($this->hasAssignedUsersField()) {
|
||||
$this->setDistinct(true, $result);
|
||||
$this->addLeftJoin('assignedUsers', $result);
|
||||
$this->addLeftJoin(['assignedUsers', 'assignedUsersAccess'], $result);
|
||||
$result['whereClause'][] = array(
|
||||
'assignedUsers.id' => $this->getUser()->id
|
||||
'assignedUsersAccess.id' => $this->getUser()->id
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -720,7 +736,7 @@ class Base
|
||||
$this->where($params['where'], $result);
|
||||
}
|
||||
|
||||
if (!empty($params['textFilter'])) {
|
||||
if (isset($params['textFilter']) && $params['textFilter'] !== '') {
|
||||
$this->textFilter($params['textFilter'], $result);
|
||||
}
|
||||
|
||||
@@ -995,7 +1011,7 @@ class Base
|
||||
foreach ($item['value'] as $i) {
|
||||
$a = $this->getWherePart($i, $result);
|
||||
foreach ($a as $left => $right) {
|
||||
if (!empty($right) || is_null($right) || $right === '') {
|
||||
if (!empty($right) || is_null($right) || $right === '' || $right === 0 || $right === false) {
|
||||
$arr[] = array($left => $right);
|
||||
}
|
||||
}
|
||||
@@ -1327,6 +1343,20 @@ class Base
|
||||
$method = 'filter' . ucfirst($filterName);
|
||||
if (method_exists($this, $method)) {
|
||||
$this->$method($result);
|
||||
} else {
|
||||
$className = $this->getMetadata()->get(['entityDefs', $this->entityType, 'collection', 'filters', $filterName, 'className']);
|
||||
if ($className) {
|
||||
if (!class_exists($className)) {
|
||||
$GLOBALS['log']->error("Could find class for filter {$filterName}.");
|
||||
return;
|
||||
}
|
||||
$impl = $this->getInjectableFactory()->createByClassName($className);
|
||||
if (!$impl) {
|
||||
$GLOBALS['log']->error("Could not create filter {$filterName} implementation.");
|
||||
return;
|
||||
}
|
||||
$impl->applyFilter($this->entityType, $filterName, $result, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1474,35 +1504,211 @@ class Base
|
||||
);
|
||||
}
|
||||
|
||||
public function getFullTextSearchDataForTextFilter($textFilter, $isAuxiliaryUse = false)
|
||||
{
|
||||
if (array_key_exists($textFilter, $this->fullTextSearchDataCacheHash)) {
|
||||
return $this->fullTextSearchDataCacheHash[$textFilter];
|
||||
}
|
||||
|
||||
if ($this->getConfig()->get('fullTextSearchDisabled')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = null;
|
||||
|
||||
$fieldList = $this->getTextFilterFieldList();
|
||||
|
||||
if ($isAuxiliaryUse) {
|
||||
$textFilter = str_replace('%', '', $textFilter);
|
||||
}
|
||||
|
||||
$fullTextSearchColumnList = $this->getEntityManager()->getOrmMetadata()->get($this->getEntityType(), ['fullTextSearchColumnList']);
|
||||
|
||||
$useFullTextSearch = false;
|
||||
|
||||
if (
|
||||
$this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'collection', 'fullTextSearch'])
|
||||
&&
|
||||
!empty($fullTextSearchColumnList)
|
||||
) {
|
||||
$fullTextSearchMinLength = $this->getConfig()->get('fullTextSearchMinLength', self::MIN_LENGTH_FOR_FULL_TEXT_SEARCH);
|
||||
if (!$fullTextSearchMinLength) {
|
||||
$fullTextSearchMinLength = 0;
|
||||
}
|
||||
if (mb_strlen($textFilter) >= $fullTextSearchMinLength) {
|
||||
$useFullTextSearch = true;
|
||||
}
|
||||
}
|
||||
|
||||
$fullTextSearchFieldList = [];
|
||||
|
||||
if ($useFullTextSearch) {
|
||||
foreach ($fieldList as $field) {
|
||||
$defs = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'fields', $field], []);
|
||||
if (empty($defs['type'])) continue;
|
||||
$fieldType = $defs['type'];
|
||||
if (!empty($defs['notStorable'])) continue;
|
||||
if (!$this->getMetadata()->get(['fields', $fieldType, 'fullTextSearch'])) continue;
|
||||
$fullTextSearchFieldList[] = $field;
|
||||
}
|
||||
if (!count($fullTextSearchFieldList)) {
|
||||
$useFullTextSearch = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($fullTextSearchColumnList)) {
|
||||
$useFullTextSearch = false;
|
||||
}
|
||||
|
||||
if ($useFullTextSearch) {
|
||||
if (
|
||||
$isAuxiliaryUse
|
||||
||
|
||||
mb_strpos($textFilter, ' ') === false
|
||||
&&
|
||||
mb_strpos($textFilter, '+') === false
|
||||
&&
|
||||
mb_strpos($textFilter, '-') === false
|
||||
&&
|
||||
mb_strpos($textFilter, '*') === false
|
||||
) {
|
||||
$function = 'MATCH_NATURAL_LANGUAGE';
|
||||
} else {
|
||||
$function = 'MATCH_BOOLEAN';
|
||||
}
|
||||
|
||||
$fullTextSearchColumnSanitizedList = [];
|
||||
$query = $this->getEntityManager()->getQuery();
|
||||
foreach ($fullTextSearchColumnList as $i => $field) {
|
||||
$fullTextSearchColumnSanitizedList[$i] = $query->sanitize($query->toDb($field));
|
||||
}
|
||||
|
||||
$where = $function . ':' . implode(',', $fullTextSearchColumnSanitizedList) . ':' . $textFilter;
|
||||
|
||||
$result = [
|
||||
'where' => $where,
|
||||
'fieldList' => $fullTextSearchFieldList,
|
||||
'columnList' => $fullTextSearchColumnList
|
||||
];
|
||||
}
|
||||
|
||||
$this->fullTextSearchDataCacheHash[$textFilter] = $result;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function textFilter($textFilter, &$result)
|
||||
{
|
||||
$fieldDefs = $this->getSeed()->getAttributes();
|
||||
$fieldList = $this->getTextFilterFieldList();
|
||||
$d = array();
|
||||
$group = [];
|
||||
|
||||
$textFilterContainsMinLength = $this->getConfig()->get('textFilterContainsMinLength', self::MIN_LENGTH_FOR_CONTENT_SEARCH);
|
||||
|
||||
$fullTextSearchData = null;
|
||||
|
||||
$forceFullTextSearch = false;
|
||||
|
||||
$useFullTextSearch = !empty($result['useFullTextSearch']);
|
||||
|
||||
if (mb_strpos($textFilter, 'ft:') === 0) {
|
||||
$textFilter = mb_substr($textFilter, 3);
|
||||
$useFullTextSearch = true;
|
||||
$forceFullTextSearch = true;
|
||||
}
|
||||
|
||||
$skipWidlcards = false;
|
||||
if (!$useFullTextSearch) {
|
||||
if (mb_strpos($textFilter, '*') !== false) {
|
||||
$skipWidlcards = true;
|
||||
$textFilter = str_replace('*', '%', $textFilter);
|
||||
}
|
||||
}
|
||||
|
||||
$fullTextSearchData = $this->getFullTextSearchDataForTextFilter($textFilter, !$useFullTextSearch);
|
||||
|
||||
$fullTextGroup = [];
|
||||
|
||||
$fullTextSearchFieldList = [];
|
||||
if ($fullTextSearchData) {
|
||||
$fullTextGroup[] = $fullTextSearchData['where'];
|
||||
$fullTextSearchFieldList = $fullTextSearchData['fieldList'];
|
||||
}
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
$expression = $textFilter . '%';
|
||||
if (
|
||||
strlen($textFilter) >= self::MIN_LENGTH_FOR_CONTENT_SEARCH
|
||||
&&
|
||||
(
|
||||
!empty($fieldDefs[$field]['type']) && $fieldDefs[$field]['type'] == 'text'
|
||||
||
|
||||
!empty($this->textFilterUseContainsAttributeList[$field])
|
||||
||
|
||||
!empty($fieldDefs[$field]['type']) && $fieldDefs[$field]['type'] == 'varchar' &&
|
||||
$this->getConfig()->get('textFilterUseContainsForVarchar')
|
||||
)
|
||||
) {
|
||||
$expression = '%' . $textFilter . '%';
|
||||
} else {
|
||||
$expression = $textFilter . '%';
|
||||
if ($useFullTextSearch) {
|
||||
if (in_array($field, $fullTextSearchFieldList)) continue;
|
||||
}
|
||||
$d[$field . '*'] = $expression;
|
||||
if ($forceFullTextSearch) continue;
|
||||
|
||||
$attributeType = null;
|
||||
if (!empty($fieldDefs[$field]['type'])) {
|
||||
$attributeType = $fieldDefs[$field]['type'];
|
||||
}
|
||||
|
||||
if ($attributeType === 'int') {
|
||||
if (is_numeric($textFilter)) {
|
||||
$group[$field] = intval($textFilter);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$skipWidlcards) {
|
||||
if (
|
||||
mb_strlen($textFilter) >= $textFilterContainsMinLength
|
||||
&&
|
||||
(
|
||||
$attributeType == 'text'
|
||||
||
|
||||
in_array($field, $this->textFilterUseContainsAttributeList)
|
||||
||
|
||||
$attributeType == 'varchar' && $this->getConfig()->get('textFilterUseContainsForVarchar')
|
||||
)
|
||||
) {
|
||||
$expression = '%' . $textFilter . '%';
|
||||
} else {
|
||||
$expression = $textFilter . '%';
|
||||
}
|
||||
} else {
|
||||
$expression = $textFilter;
|
||||
}
|
||||
|
||||
if ($fullTextSearchData) {
|
||||
if (!$useFullTextSearch) {
|
||||
if (in_array($field, $fullTextSearchFieldList)) {
|
||||
if (!array_key_exists('OR', $fullTextGroup)) {
|
||||
$fullTextGroup['OR'] = [];
|
||||
}
|
||||
$fullTextGroup['OR'][$field . '*'] = $expression;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$group[$field . '*'] = $expression;
|
||||
}
|
||||
$result['whereClause'][] = array(
|
||||
'OR' => $d
|
||||
);
|
||||
|
||||
if (!$forceFullTextSearch) {
|
||||
$this->applyAdditionalToTextFilterGroup($textFilter, $group);
|
||||
}
|
||||
|
||||
if (!empty($fullTextGroup)) {
|
||||
$group['AND'] = $fullTextGroup;
|
||||
}
|
||||
|
||||
if (count($group) === 0) {
|
||||
$result['whereClause'][] = [
|
||||
'id' => null
|
||||
];
|
||||
}
|
||||
|
||||
$result['whereClause'][] = [
|
||||
'OR' => $group
|
||||
];
|
||||
}
|
||||
|
||||
protected function applyAdditionalToTextFilterGroup($textFilter, &$group)
|
||||
{
|
||||
}
|
||||
|
||||
public function applyAccess(&$result)
|
||||
@@ -1531,19 +1737,25 @@ class Base
|
||||
protected function boolFilterOnlyMy(&$result)
|
||||
{
|
||||
if (!$this->checkIsPortal()) {
|
||||
if ($this->hasAssignedUserField()) {
|
||||
$result['whereClause'][] = array(
|
||||
if ($this->hasAssignedUsersField()) {
|
||||
$this->setDistinct(true, $result);
|
||||
$this->addLeftJoin(['assignedUsers', 'assignedUsersAccess'], $result);
|
||||
$result['whereClause'][] = [
|
||||
'assignedUsersAccess.id' => $this->getUser()->id
|
||||
];
|
||||
} else if ($this->hasAssignedUserField()) {
|
||||
$result['whereClause'][] = [
|
||||
'assignedUserId' => $this->getUser()->id
|
||||
);
|
||||
];
|
||||
} else {
|
||||
$result['whereClause'][] = array(
|
||||
$result['whereClause'][] = [
|
||||
'createdById' => $this->getUser()->id
|
||||
);
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$result['whereClause'][] = array(
|
||||
$result['whereClause'][] = [
|
||||
'createdById' => $this->getUser()->id
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1563,4 +1775,3 @@ class Base
|
||||
$this->filterFollowed($result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"fields": {
|
||||
"name": {
|
||||
"type": "personName"
|
||||
"type": "personName",
|
||||
"isPersonalData": true
|
||||
},
|
||||
"salutationName": {
|
||||
"type": "enum",
|
||||
@@ -24,15 +25,18 @@
|
||||
"type": "text"
|
||||
},
|
||||
"emailAddress": {
|
||||
"type": "email"
|
||||
"type": "email",
|
||||
"isPersonalData": true
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "phone",
|
||||
"typeList": ["Mobile", "Office", "Home", "Fax", "Other"],
|
||||
"defaultType": "Mobile"
|
||||
"defaultType": "Mobile",
|
||||
"isPersonalData": true
|
||||
},
|
||||
"address": {
|
||||
"type": "address"
|
||||
"type": "address",
|
||||
"isPersonalData": true
|
||||
},
|
||||
"addressStreet": {
|
||||
"type": "text",
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"aclPortalLevelList": ["all", "account", "contact", "own", "no"],
|
||||
"customizable": true,
|
||||
"importable": true,
|
||||
"notifications": true
|
||||
"notifications": true,
|
||||
"hasPersonalData": true
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class Person extends \Espo\Services\Record
|
||||
if (
|
||||
($entity->get('emailAddress') || $entity->get('emailAddressData'))
|
||||
&&
|
||||
($entity->isNew() || $entity->isFieldChanged('emailAddress') || $entity->isFieldChanged('emailAddressData'))
|
||||
($entity->isNew() || $entity->isAttributeChanged('emailAddress') || $entity->isAttributeChanged('emailAddressData'))
|
||||
) {
|
||||
if ($entity->get('emailAddress')) {
|
||||
$list = [$entity->get('emailAddress')];
|
||||
|
||||
5
application/Espo/Core/Templates/i18n/hr_HR/Base.json
Normal file
5
application/Espo/Core/Templates/i18n/hr_HR/Base.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Napravi {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
10
application/Espo/Core/Templates/i18n/hr_HR/BasePlus.json
Normal file
10
application/Espo/Core/Templates/i18n/hr_HR/BasePlus.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"links": {
|
||||
"meetings": "Sastanci",
|
||||
"calls": "Pozivi",
|
||||
"tasks": "Zadaci"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Kreiraj {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
15
application/Espo/Core/Templates/i18n/hr_HR/Company.json
Normal file
15
application/Espo/Core/Templates/i18n/hr_HR/Company.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fields": {
|
||||
"billingAddress": "Adresa za naplatu",
|
||||
"shippingAddress": "Adresa za dostavu",
|
||||
"website": "Sajt"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Sastanci",
|
||||
"calls": "Pozivi",
|
||||
"tasks": "Zadaci"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Kreiraj {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
35
application/Espo/Core/Templates/i18n/hr_HR/Event.json
Normal file
35
application/Espo/Core/Templates/i18n/hr_HR/Event.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"fields": {
|
||||
"parent": "Nadređen",
|
||||
"dateStart": "Početni datum",
|
||||
"dateEnd": "Završni datum",
|
||||
"duration": "Trajanje",
|
||||
"reminders": "Podsjetnici"
|
||||
},
|
||||
"links": {
|
||||
"parent": "Nadređen"
|
||||
},
|
||||
"options": {
|
||||
"status": {
|
||||
"Planned": "Planiran",
|
||||
"Held": "Održan",
|
||||
"Not Held": "Nije održan"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Kreiraj {entityTypeTranslated}",
|
||||
"Schedule {entityType}": "Zakaži {entityTypeTranslated}",
|
||||
"Log {entityType}": "Zabilježi {entitiTipeTranslated}",
|
||||
"Set Held": "Postavi kao održano",
|
||||
"Set Not Held": "Postavi kao nije održano"
|
||||
},
|
||||
"massActions": {
|
||||
"setHeld": "Postavi kao održano",
|
||||
"setNotHeld": "Postavi kao nije održano"
|
||||
},
|
||||
"presetFilters": {
|
||||
"planned": "Planiran",
|
||||
"held": "Održan",
|
||||
"todays": "Današnji"
|
||||
}
|
||||
}
|
||||
13
application/Espo/Core/Templates/i18n/hr_HR/Person.json
Normal file
13
application/Espo/Core/Templates/i18n/hr_HR/Person.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"fields": {
|
||||
"address": "Adresa"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Sastanci",
|
||||
"calls": "Pozivi",
|
||||
"tasks": "Zadaci"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Kreiraj {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
5
application/Espo/Core/Templates/i18n/hu_HU/Base.json
Normal file
5
application/Espo/Core/Templates/i18n/hu_HU/Base.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "{EntityTypeTranslated} létrehozása"
|
||||
}
|
||||
}
|
||||
10
application/Espo/Core/Templates/i18n/hu_HU/BasePlus.json
Normal file
10
application/Espo/Core/Templates/i18n/hu_HU/BasePlus.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"links": {
|
||||
"meetings": "találkozók",
|
||||
"calls": "felhívja",
|
||||
"tasks": "feladatok"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "{EntityTypeTranslated} létrehozása"
|
||||
}
|
||||
}
|
||||
15
application/Espo/Core/Templates/i18n/hu_HU/Company.json
Normal file
15
application/Espo/Core/Templates/i18n/hu_HU/Company.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fields": {
|
||||
"billingAddress": "számlázási cím",
|
||||
"shippingAddress": "szállítási cím",
|
||||
"website": "Weboldal"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "találkozók",
|
||||
"calls": "felhívja",
|
||||
"tasks": "feladatok"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "{EntityTypeTranslated} létrehozása"
|
||||
}
|
||||
}
|
||||
27
application/Espo/Core/Templates/i18n/hu_HU/Event.json
Normal file
27
application/Espo/Core/Templates/i18n/hu_HU/Event.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"fields": {
|
||||
"parent": "Szülő",
|
||||
"dateStart": "Dátum kezdete",
|
||||
"dateEnd": "Dátum vége",
|
||||
"duration": "tartam",
|
||||
"status": "Állapot",
|
||||
"reminders": "Emlékeztetők"
|
||||
},
|
||||
"links": {
|
||||
"parent": "Szülő"
|
||||
},
|
||||
"options": {
|
||||
"status": {
|
||||
"Planned": "Tervezett",
|
||||
"Not Held": "Nem tartott"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "{EntityTypeTranslated} létrehozása",
|
||||
"Schedule {entityType}": "{EntityTypeTranslated} ütemezése"
|
||||
},
|
||||
"presetFilters": {
|
||||
"planned": "Tervezett",
|
||||
"todays": "A mai"
|
||||
}
|
||||
}
|
||||
13
application/Espo/Core/Templates/i18n/hu_HU/Person.json
Normal file
13
application/Espo/Core/Templates/i18n/hu_HU/Person.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"fields": {
|
||||
"address": "Cím"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "találkozók",
|
||||
"calls": "felhívja",
|
||||
"tasks": "feladatok"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "{EntityTypeTranslated} létrehozása"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"links": {
|
||||
"meetings": "Susirinkimai",
|
||||
"meetings": "Susitikimai",
|
||||
"calls": "Skambučiai",
|
||||
"tasks": "Užduotys"
|
||||
},
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"fields": {
|
||||
"billingAddress": "Mokėtojo adresas",
|
||||
"shippingAddress": "Siuntimo adresas",
|
||||
"website": "Interntinė Svetainė"
|
||||
"website": "Internetinė Svetainė"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Susirinkimai",
|
||||
"meetings": "Susitikimai",
|
||||
"calls": "Skambučiai",
|
||||
"tasks": "Užduotys"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"address": "Adresas"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Susirinkimai",
|
||||
"meetings": "Susitikimai",
|
||||
"calls": "Skambučiai",
|
||||
"tasks": "Užduotys"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Utwórz {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
"labels": {
|
||||
"Create {entityType}": "Utwórz {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Utwórz {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
"links": {
|
||||
"meetings": "Spotkania",
|
||||
"calls": "Połączenia",
|
||||
"tasks": "Zadania"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Utwórz {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Utwórz {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
"fields": {
|
||||
"billingAddress": "Adres rozliczeniowy",
|
||||
"shippingAddress": "Adres dostawy",
|
||||
"website": "Strona internetowa"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Spotkania",
|
||||
"calls": "Połączenia",
|
||||
"tasks": "Zadania"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Utwórz {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,20 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Utwórz {entityTypeTranslated}"
|
||||
"fields": {
|
||||
"dateStart": "Data rozpoczęcia",
|
||||
"dateEnd": "Data zakończenia",
|
||||
"duration": "Czas trwania",
|
||||
"reminders": "Przypomnienia"
|
||||
},
|
||||
"options": {
|
||||
"status": {
|
||||
"Planned": "Planowane"
|
||||
}
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Utwórz {entityTypeTranslated}",
|
||||
"Schedule {entityType}": "Zaplanuj {entityTypeTranslated} "
|
||||
},
|
||||
"presetFilters": {
|
||||
"planned": "Planowane"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Utwórz {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
"fields": {
|
||||
"address": "Adres"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Spotkania",
|
||||
"calls": "Rozmowy",
|
||||
"tasks": "Zadania"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Utwórz {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Creare {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
"labels": {
|
||||
"Create {entityType}": "Creare {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Creare {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
"labels": {
|
||||
"Create {entityType}": "Creare {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Creare {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
"labels": {
|
||||
"Create {entityType}": "Creare {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Creare {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
"labels": {
|
||||
"Create {entityType}": "Creare {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Creare {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
"labels": {
|
||||
"Create {entityType}": "Creare {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
5
application/Espo/Core/Templates/i18n/sk_SK/Base.json
Normal file
5
application/Espo/Core/Templates/i18n/sk_SK/Base.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Vytvoriť {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
10
application/Espo/Core/Templates/i18n/sk_SK/BasePlus.json
Normal file
10
application/Espo/Core/Templates/i18n/sk_SK/BasePlus.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"links": {
|
||||
"meetings": "Stretnutia",
|
||||
"calls": "Hovory",
|
||||
"tasks": "Úlohy"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Vytvoriť {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
15
application/Espo/Core/Templates/i18n/sk_SK/Company.json
Normal file
15
application/Espo/Core/Templates/i18n/sk_SK/Company.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fields": {
|
||||
"billingAddress": "Fakturačná adresa",
|
||||
"shippingAddress": "Dodacia adresa",
|
||||
"website": "Webová stránka"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Stretnutia",
|
||||
"calls": "Hovory",
|
||||
"tasks": "Úlohy"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Vytvoriť {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
36
application/Espo/Core/Templates/i18n/sk_SK/Event.json
Normal file
36
application/Espo/Core/Templates/i18n/sk_SK/Event.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"fields": {
|
||||
"parent": "Rodič",
|
||||
"dateStart": "Dátum začiatku",
|
||||
"dateEnd": "Dátum konca",
|
||||
"duration": "Trvanie",
|
||||
"status": "Stav",
|
||||
"reminders": "Pripomienky"
|
||||
},
|
||||
"links": {
|
||||
"parent": "Rodič"
|
||||
},
|
||||
"options": {
|
||||
"status": {
|
||||
"Planned": "Plánovaný",
|
||||
"Held": "Pozdržaný",
|
||||
"Not Held": "Nepozdržaný"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Vytvoriť {entityTypeTranslated}",
|
||||
"Schedule {entityType}": "Naplánovať {entityTypeTranslated}",
|
||||
"Log {entityType}": "Protokol {entityTypeTranslated}",
|
||||
"Set Held": "Nastav pozdržaný",
|
||||
"Set Not Held": "Nastav nepozdržaný"
|
||||
},
|
||||
"massActions": {
|
||||
"setHeld": "Nastav pozdržaný",
|
||||
"setNotHeld": "Nastav nepozdržaný"
|
||||
},
|
||||
"presetFilters": {
|
||||
"planned": "Plánovaný",
|
||||
"held": "Pozdržaný",
|
||||
"todays": "Dnešné"
|
||||
}
|
||||
}
|
||||
13
application/Espo/Core/Templates/i18n/sk_SK/Person.json
Normal file
13
application/Espo/Core/Templates/i18n/sk_SK/Person.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"fields": {
|
||||
"address": "Adresa"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Stretnutia",
|
||||
"calls": "Hovory",
|
||||
"tasks": "Úlohy"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Vytvoriť {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ class UpgradeManager extends Upgrades\Base
|
||||
'customDirNames' => array(
|
||||
'before' => 'beforeUpgradeFiles',
|
||||
'after' => 'afterUpgradeFiles',
|
||||
'vendor' => 'vendorFiles',
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Upgrades\Actions;
|
||||
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Core\Utils\System;
|
||||
use Espo\Core\Utils\Json;
|
||||
@@ -83,6 +84,7 @@ abstract class Base
|
||||
*/
|
||||
protected $defaultPackageType = 'extension';
|
||||
|
||||
protected $vendorDirName = 'vendor';
|
||||
|
||||
public function __construct(\Espo\Core\Container $container, \Espo\Core\Upgrades\ActionManager $actionManager)
|
||||
{
|
||||
@@ -315,8 +317,17 @@ abstract class Base
|
||||
{
|
||||
$manifest = $this->getManifest();
|
||||
|
||||
if (isset($manifest[$type])) {
|
||||
return $manifest[$type];
|
||||
switch ($type) {
|
||||
case 'delete':
|
||||
case 'deleteBeforeCopy':
|
||||
if (isset($manifest[$type])) {
|
||||
return $manifest[$type];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'vendor':
|
||||
return $this->getVendorFileList('delete');
|
||||
break;
|
||||
}
|
||||
|
||||
return array();
|
||||
@@ -332,7 +343,7 @@ abstract class Base
|
||||
if (!isset($this->data['deleteFileList'])) {
|
||||
$deleteFileList = array();
|
||||
|
||||
$deleteList = array_merge($this->getDeleteList('delete'), $this->getDeleteList('deleteBeforeCopy'));
|
||||
$deleteList = array_merge($this->getDeleteList('delete'), $this->getDeleteList('deleteBeforeCopy'), $this->getDeleteList('vendor'));
|
||||
foreach ($deleteList as $key => $itemPath) {
|
||||
if (is_dir($itemPath)) {
|
||||
$fileList = $this->getFileManager()->getFileList($itemPath, true, '', true, true);
|
||||
@@ -356,9 +367,9 @@ abstract class Base
|
||||
*
|
||||
* @return boolen
|
||||
*/
|
||||
protected function deleteFiles($withEmptyDirs = false)
|
||||
protected function deleteFiles($type = 'delete', $withEmptyDirs = false)
|
||||
{
|
||||
$deleteList = $this->getDeleteList('delete');
|
||||
$deleteList = $this->getDeleteList($type);
|
||||
|
||||
if (!empty($deleteList)) {
|
||||
return $this->getFileManager()->remove($deleteList, null, $withEmptyDirs);
|
||||
@@ -367,24 +378,6 @@ abstract class Base
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleted file/forder list before coppy the upgrade files
|
||||
*
|
||||
* @param boolean $withEmptyDirs
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function deleteBeforeCopy($withEmptyDirs = false)
|
||||
{
|
||||
$deleteList = $this->getDeleteList('deleteBeforeCopy');
|
||||
|
||||
if (!empty($deleteList)) {
|
||||
$this->getFileManager()->remove($deleteList, null, $withEmptyDirs);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getCopyFileList()
|
||||
{
|
||||
if (!isset($this->data['fileList'])) {
|
||||
@@ -445,6 +438,12 @@ abstract class Base
|
||||
}
|
||||
}
|
||||
|
||||
//vendor file list
|
||||
$vendorFileList = $this->getVendorFileList('copy');
|
||||
if (!empty($vendorFileList)) {
|
||||
$fileList = array_merge($fileList, $vendorFileList);
|
||||
}
|
||||
|
||||
return $fileList;
|
||||
}
|
||||
|
||||
@@ -466,7 +465,7 @@ abstract class Base
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function copyFiles($type = null)
|
||||
protected function copyFiles($type = null, $dest = '')
|
||||
{
|
||||
switch ($type) {
|
||||
case 'before':
|
||||
@@ -475,21 +474,63 @@ abstract class Base
|
||||
$dirPath = $dirNames[$type];
|
||||
break;
|
||||
|
||||
case 'vendor':
|
||||
$dirNames = $this->getParams('customDirNames');
|
||||
if (isset($dirNames['vendor'])) {
|
||||
$dirPath = $dirNames['vendor'];
|
||||
$dest = $this->vendorDirName;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$dirPath = self::FILES;
|
||||
break;
|
||||
}
|
||||
|
||||
$packagePath = $this->getPackagePath();
|
||||
$filesPath = Util::concatPath($packagePath, $dirPath);
|
||||
if (isset($dirPath)) {
|
||||
$packagePath = $this->getPackagePath();
|
||||
$filesPath = Util::concatPath($packagePath, $dirPath);
|
||||
|
||||
if (file_exists($filesPath)) {
|
||||
return $this->copy($filesPath, '', true);
|
||||
if (file_exists($filesPath)) {
|
||||
return $this->copy($filesPath, $dest, true);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getVendorFileList($type = 'copy')
|
||||
{
|
||||
$list = [];
|
||||
|
||||
$packagePath = $this->getPackagePath();
|
||||
$dirNames = $this->getParams('customDirNames');
|
||||
if (!isset($dirNames['vendor'])) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
$filesPath = Util::concatPath($packagePath, $dirNames['vendor']);
|
||||
if (!file_exists($filesPath)) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'copy':
|
||||
$list = $this->getFileManager()->getFileList($filesPath, true, '', true, true);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$list = $this->getFileManager()->getFileList($filesPath, false, '', null, true);
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($list as &$path) {
|
||||
$path = Util::concatPath($this->vendorDirName, $path);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function getManifest()
|
||||
{
|
||||
if (!isset($this->data['manifest'])) {
|
||||
@@ -515,6 +556,11 @@ abstract class Base
|
||||
return $this->data['manifest'];
|
||||
}
|
||||
|
||||
protected function setManifest()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the manifest is correct
|
||||
*
|
||||
|
||||
@@ -73,10 +73,12 @@ class Install extends \Espo\Core\Upgrades\Actions\Base
|
||||
}
|
||||
|
||||
/* run before install script */
|
||||
$this->runScript('before');
|
||||
if (!isset($data['skipBeforeScript']) || !$data['skipBeforeScript']) {
|
||||
$this->runScript('before');
|
||||
}
|
||||
|
||||
/* remove files defined in a manifest "deleteBeforeCopy" */
|
||||
$this->deleteBeforeCopy(true);
|
||||
$this->deleteFiles('deleteBeforeCopy', true);
|
||||
|
||||
/* copy files from directory "Files" to EspoCRM files */
|
||||
if (!$this->copyFiles()) {
|
||||
@@ -84,10 +86,15 @@ class Install extends \Espo\Core\Upgrades\Actions\Base
|
||||
}
|
||||
|
||||
/* remove files defined in a manifest */
|
||||
$this->deleteFiles(true);
|
||||
$this->deleteFiles('delete', true);
|
||||
|
||||
if (!$this->systemRebuild()) {
|
||||
$this->throwErrorAndRemovePackage('Error occurred while EspoCRM rebuild.');
|
||||
$this->deleteFiles('vendor');
|
||||
$this->copyFiles('vendor');
|
||||
|
||||
if (!isset($data['skipSystemRebuild']) || !$data['skipSystemRebuild']) {
|
||||
if (!$this->systemRebuild()) {
|
||||
$this->throwErrorAndRemovePackage('Error occurred while EspoCRM rebuild.');
|
||||
}
|
||||
}
|
||||
|
||||
//afterInstallFiles
|
||||
@@ -96,7 +103,9 @@ class Install extends \Espo\Core\Upgrades\Actions\Base
|
||||
}
|
||||
|
||||
/* run before install script */
|
||||
$this->runScript('after');
|
||||
if (!isset($data['skipAfterScript']) || !$data['skipAfterScript']) {
|
||||
$this->runScript('after');
|
||||
}
|
||||
|
||||
$this->afterRunAction();
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class Uninstall extends \Espo\Core\Upgrades\Actions\Base
|
||||
$this->beforeRunAction();
|
||||
|
||||
/* run before install script */
|
||||
if (!isset($data['isNotRunScriptBefore']) || !$data['isNotRunScriptBefore']) {
|
||||
if (!isset($data['skipBeforeScript']) || !$data['skipBeforeScript']) {
|
||||
$this->runScript('beforeUninstall');
|
||||
}
|
||||
|
||||
@@ -66,17 +66,19 @@ class Uninstall extends \Espo\Core\Upgrades\Actions\Base
|
||||
}
|
||||
|
||||
/* remove extension files, saved in fileList */
|
||||
if (!$this->deleteFiles(true)) {
|
||||
if (!$this->deleteFiles('delete', true)) {
|
||||
$this->throwErrorAndRemovePackage('Permission denied to delete files.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->systemRebuild()) {
|
||||
$this->throwErrorAndRemovePackage('Error occurred while EspoCRM rebuild.');
|
||||
if (!isset($data['skipSystemRebuild']) || !$data['skipSystemRebuild']) {
|
||||
if (!$this->systemRebuild()) {
|
||||
$this->throwErrorAndRemovePackage('Error occurred while EspoCRM rebuild.');
|
||||
}
|
||||
}
|
||||
|
||||
/* run after uninstall script */
|
||||
if (!isset($data['isNotRunScriptAfter']) || !$data['isNotRunScriptAfter']) {
|
||||
if (!isset($data['skipAfterScript']) || !$data['skipAfterScript']) {
|
||||
$this->runScript('afterUninstall');
|
||||
}
|
||||
|
||||
@@ -119,10 +121,10 @@ class Uninstall extends \Espo\Core\Upgrades\Actions\Base
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function copyFiles($type = null)
|
||||
protected function copyFiles($type = null, $dest = '')
|
||||
{
|
||||
$backupPath = $this->getPath('backupPath');
|
||||
$res = $this->copy(array($backupPath, self::FILES), '', true);
|
||||
$res = $this->copy(array($backupPath, self::FILES), $dest, true);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
@@ -149,6 +149,11 @@ class Install extends \Espo\Core\Upgrades\Actions\Base\Install
|
||||
'fileList' => $fileList,
|
||||
'description' => $manifest['description'],
|
||||
);
|
||||
|
||||
if (!empty($manifest['checkVersionUrl'])) {
|
||||
$data['checkVersionUrl'] = $manifest['checkVersionUrl'];
|
||||
}
|
||||
|
||||
$extensionEntity->set($data);
|
||||
|
||||
return $entityManager->saveEntity($extensionEntity);
|
||||
@@ -199,7 +204,8 @@ class Install extends \Espo\Core\Upgrades\Actions\Base\Install
|
||||
|
||||
$this->executeAction(ExtensionManager::UNINSTALL, array(
|
||||
'id' => $extensionEntity->get('id'),
|
||||
'isNotRunScriptAfter' => true,
|
||||
'skipSystemRebuild' => true,
|
||||
'skipAfterScript' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,13 @@ class AdminNotificationManager
|
||||
$extensionsNeedingUpgrade = $this->getExtensionsNeedingUpgrade();
|
||||
if (!empty($extensionsNeedingUpgrade)) {
|
||||
foreach ($extensionsNeedingUpgrade as $extensionName => $extensionDetails) {
|
||||
$message = $this->getLanguage()->translate('newExtensionVersionIsAvailable', 'messages', 'Admin');
|
||||
$label = 'new' . Util::toCamelCase($extensionName, ' ', true) .'VersionIsAvailable';
|
||||
|
||||
$message = $this->getLanguage()->get(['Admin', 'messages', $label]);
|
||||
if (!$message) {
|
||||
$message = $this->getLanguage()->translate('newExtensionVersionIsAvailable', 'messages', 'Admin');
|
||||
}
|
||||
|
||||
$notificationList[] = array(
|
||||
'id' => 'newExtensionVersionIsAvailable' . Util::toCamelCase($extensionName, ' ', true),
|
||||
'type' => 'newExtensionVersionIsAvailable',
|
||||
@@ -120,6 +126,7 @@ class AdminNotificationManager
|
||||
$latestVersion = $config->get('latestVersion');
|
||||
if (isset($latestVersion)) {
|
||||
$currentVersion = $config->get('version');
|
||||
if ($currentVersion === 'dev') return;
|
||||
if (version_compare($latestVersion, $currentVersion, '>')) {
|
||||
return array(
|
||||
'currentVersion' => $currentVersion,
|
||||
@@ -158,8 +165,8 @@ class AdminNotificationManager
|
||||
|
||||
$query = "
|
||||
SELECT version FROM extension
|
||||
WHERE name='". $extensionName ."'
|
||||
AND deleted=0
|
||||
WHERE name = ". $pdo->quote($extensionName) ."
|
||||
AND deleted = 0
|
||||
AND is_installed = 1
|
||||
ORDER BY created_at DESC
|
||||
";
|
||||
@@ -186,11 +193,10 @@ class AdminNotificationManager
|
||||
$notification->set(array(
|
||||
'type' => 'message',
|
||||
'data' => array(
|
||||
'userId' => $this->getUser()->id,
|
||||
'userName' => $this->getUser()->get('name')
|
||||
'userId' => $userId,
|
||||
),
|
||||
'userId' => $user->id,
|
||||
'message' => $actionData['messageTemplate']
|
||||
'userId' => $userId,
|
||||
'message' => $message
|
||||
));
|
||||
$this->getEntityManager()->saveEntity($notification);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class Auth extends \Slim\Middleware
|
||||
|
||||
$espoAuth = $req->headers('HTTP_ESPO_AUTHORIZATION');
|
||||
if (isset($espoAuth)) {
|
||||
list($authUsername, $authPassword) = explode(':', base64_decode($espoAuth));
|
||||
list($authUsername, $authPassword) = explode(':', base64_decode($espoAuth), 2);
|
||||
}
|
||||
|
||||
if (!isset($authUsername)) {
|
||||
@@ -84,7 +84,12 @@ class Auth extends \Slim\Middleware
|
||||
if (isset($routeConditions['auth']) && $routeConditions['auth'] === false) {
|
||||
|
||||
if ($authUsername && $authPassword) {
|
||||
$isAuthenticated = $this->auth->login($authUsername, $authPassword);
|
||||
try {
|
||||
$isAuthenticated = $this->auth->login($authUsername, $authPassword);
|
||||
} catch (\Exception $e) {
|
||||
$this->processException($e);
|
||||
return;
|
||||
}
|
||||
if ($isAuthenticated) {
|
||||
$this->next->call();
|
||||
return;
|
||||
@@ -105,8 +110,12 @@ class Auth extends \Slim\Middleware
|
||||
}
|
||||
|
||||
if ($authUsername && $authPassword) {
|
||||
|
||||
$isAuthenticated = $this->auth->login($authUsername, $authPassword);
|
||||
try {
|
||||
$isAuthenticated = $this->auth->login($authUsername, $authPassword);
|
||||
} catch (\Exception $e) {
|
||||
$this->processException($e);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isAuthenticated) {
|
||||
$this->next->call();
|
||||
@@ -121,23 +130,31 @@ class Auth extends \Slim\Middleware
|
||||
}
|
||||
}
|
||||
|
||||
protected function processException(\Exception $e)
|
||||
{
|
||||
$response = $this->app->response();
|
||||
|
||||
if ($e->getMessage()) {
|
||||
$response->headers->set('X-Status-Reason', $e->getMessage());
|
||||
}
|
||||
$response->setStatus($e->getCode());
|
||||
}
|
||||
|
||||
protected function processUnauthorized()
|
||||
{
|
||||
$res = $this->app->response();
|
||||
$response = $this->app->response();
|
||||
|
||||
if ($this->showDialog) {
|
||||
$res->header('WWW-Authenticate', 'Basic realm=""');
|
||||
} else {
|
||||
$res->header('WWW-Authenticate');
|
||||
$response->headers->set('WWW-Authenticate', 'Basic realm=""');
|
||||
}
|
||||
$res->status(401);
|
||||
$response->setStatus(401);
|
||||
}
|
||||
|
||||
protected function isXMLHttpRequest()
|
||||
{
|
||||
$req = $this->app->request();
|
||||
$request = $this->app->request();
|
||||
|
||||
$httpXRequestedWith = $req->headers('HTTP_X_REQUESTED_WITH');
|
||||
$httpXRequestedWith = $request->headers('HTTP_X_REQUESTED_WITH');
|
||||
|
||||
if (isset($httpXRequestedWith) && strtolower($httpXRequestedWith) == 'xmlhttprequest') {
|
||||
return true;
|
||||
@@ -145,6 +162,4 @@ class Auth extends \Slim\Middleware
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Api;
|
||||
|
||||
class Output
|
||||
{
|
||||
private $slim;
|
||||
@@ -96,8 +97,8 @@ class Output
|
||||
ob_clean();
|
||||
|
||||
if (!empty( $this->slim)) {
|
||||
$this->getSlim()->response()->status($statusCode);
|
||||
$this->getSlim()->response()->header('X-Status-Reason', $text);
|
||||
$this->getSlim()->response()->setStatus($statusCode);
|
||||
$this->getSlim()->response()->headers->set('X-Status-Reason', $text);
|
||||
|
||||
if ($isPrint) {
|
||||
$status = $this->getCodeDesc($statusCode);
|
||||
@@ -139,4 +140,3 @@ class Output
|
||||
return preg_replace('/"(.*?password.*?)":".*?"/i', '"$1":"*****"', $inputData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ class Auth
|
||||
|
||||
const ACCESS_ANY = 3;
|
||||
|
||||
const FAILED_ATTEMPTS_PERIOD = '60 seconds';
|
||||
|
||||
const MAX_FAILED_ATTEMPT_NUMBER = 10;
|
||||
|
||||
private $portal;
|
||||
|
||||
public function __construct(\Espo\Core\Container $container, $allowAnyAccess = false)
|
||||
@@ -117,20 +121,34 @@ class Auth
|
||||
|
||||
public function login($username, $password)
|
||||
{
|
||||
$authToken = $this->getEntityManager()->getRepository('AuthToken')->where(array(
|
||||
'token' => $password,
|
||||
'isActive' => true
|
||||
))->findOne();
|
||||
$isByTokenOnly = false;
|
||||
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();
|
||||
|
||||
$authTokenIsFound = false;
|
||||
|
||||
if ($authToken) {
|
||||
$authTokenIsFound = true;
|
||||
}
|
||||
|
||||
if ($authToken && $authToken->get('isActive')) {
|
||||
if (!$this->allowAnyAccess) {
|
||||
if ($this->isPortal() && $authToken->get('portalId') !== $this->getPortal()->id) {
|
||||
$GLOBALS['log']->debug("AUTH: Trying to login to portal with a token not related to portal.");
|
||||
return false;
|
||||
$GLOBALS['log']->info("AUTH: Trying to login to portal with a token not related to portal.");
|
||||
return;
|
||||
}
|
||||
if (!$this->isPortal() && $authToken->get('portalId')) {
|
||||
$GLOBALS['log']->debug("AUTH: Trying to login to crm with a token related to portal.");
|
||||
return false;
|
||||
$GLOBALS['log']->info("AUTH: Trying to login to crm with a token related to portal.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($this->allowAnyAccess) {
|
||||
@@ -141,77 +159,190 @@ class Auth
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$authToken = null;
|
||||
}
|
||||
|
||||
if ($isByTokenOnly && !$authToken) {
|
||||
$GLOBALS['log']->info("AUTH: Trying to login as user '{$username}' by token but token is not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->authentication->login($username, $password, $authToken);
|
||||
|
||||
if ($user) {
|
||||
if (!$user->isActive()) {
|
||||
$GLOBALS['log']->debug("AUTH: Trying to login as user '".$user->get('userName')."' which is not active.");
|
||||
return false;
|
||||
}
|
||||
$authLogRecord = null;
|
||||
|
||||
if (!$user->isAdmin() && !$this->isPortal() && $user->get('isPortalUser')) {
|
||||
$GLOBALS['log']->debug("AUTH: Trying to login to crm as a portal user '".$user->get('userName')."'.");
|
||||
return false;
|
||||
}
|
||||
if (!$authTokenIsFound) {
|
||||
$authLogRecord = $this->createAuthLogRecord($username, $user);
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && $this->isPortal() && !$user->get('isPortalUser')) {
|
||||
$GLOBALS['log']->debug("AUTH: Trying to login to portal as user '".$user->get('userName')."' which is not portal user.");
|
||||
return false;
|
||||
}
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isPortal()) {
|
||||
if (!$user->isAdmin() && !$this->getEntityManager()->getRepository('Portal')->isRelated($this->getPortal(), 'users', $user)) {
|
||||
$GLOBALS['log']->debug("AUTH: Trying to login to portal as user '".$user->get('userName')."' which is portal user but does not belongs to portal.");
|
||||
return false;
|
||||
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')) {
|
||||
$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')) {
|
||||
$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)) {
|
||||
$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;
|
||||
}
|
||||
$user->set('portalId', $this->getPortal()->id);
|
||||
} else {
|
||||
$user->loadLinkMultipleField('teams');
|
||||
}
|
||||
|
||||
$user->set('ipAddress', $_SERVER['REMOTE_ADDR']);
|
||||
|
||||
$this->getEntityManager()->setUser($user);
|
||||
$this->getContainer()->setUser($user);
|
||||
|
||||
if ($this->request->headers->get('HTTP_ESPO_AUTHORIZATION')) {
|
||||
if (!$authToken) {
|
||||
$authToken = $this->getEntityManager()->getEntity('AuthToken');
|
||||
$token = $this->generateToken();
|
||||
$authToken->set('token', $token);
|
||||
$authToken->set('hash', $user->get('password'));
|
||||
$authToken->set('ipAddress', $_SERVER['REMOTE_ADDR']);
|
||||
$authToken->set('userId', $user->id);
|
||||
if ($this->isPortal()) {
|
||||
$authToken->set('portalId', $this->getPortal()->id);
|
||||
}
|
||||
$user->set('portalId', $this->getPortal()->id);
|
||||
} else {
|
||||
$user->loadLinkMultipleField('teams');
|
||||
}
|
||||
|
||||
$user->set('ipAddress', $_SERVER['REMOTE_ADDR']);
|
||||
|
||||
$this->getEntityManager()->setUser($user);
|
||||
$this->getContainer()->setUser($user);
|
||||
|
||||
if ($this->request->headers->get('HTTP_ESPO_AUTHORIZATION')) {
|
||||
if (!$authToken) {
|
||||
$authToken = $this->getEntityManager()->getEntity('AuthToken');
|
||||
$token = $this->createToken($user);
|
||||
$authToken->set('token', $token);
|
||||
$authToken->set('hash', $user->get('password'));
|
||||
$authToken->set('ipAddress', $_SERVER['REMOTE_ADDR']);
|
||||
$authToken->set('userId', $user->id);
|
||||
if ($this->isPortal()) {
|
||||
$authToken->set('portalId', $this->getPortal()->id);
|
||||
if ($this->getConfig()->get('authTokenPreventConcurrent')) {
|
||||
$concurrentAuthTokenList = $this->getEntityManager()->getRepository('AuthToken')->select(['id'])->where([
|
||||
'userId' => $user->id,
|
||||
'isActive' => true
|
||||
])->find();
|
||||
foreach ($concurrentAuthTokenList as $concurrentAuthToken) {
|
||||
$concurrentAuthToken->set('isActive', false);
|
||||
$this->getEntityManager()->saveEntity($concurrentAuthToken);
|
||||
}
|
||||
}
|
||||
$authToken->set('lastAccess', date('Y-m-d H:i:s'));
|
||||
|
||||
$this->getEntityManager()->saveEntity($authToken);
|
||||
$user->set('token', $authToken->get('token'));
|
||||
$user->set('authTokenId', $authToken->id);
|
||||
}
|
||||
}
|
||||
$authToken->set('lastAccess', date('Y-m-d H:i:s'));
|
||||
|
||||
return true;
|
||||
$this->getEntityManager()->saveEntity($authToken);
|
||||
$user->set('token', $authToken->get('token'));
|
||||
$user->set('authTokenId', $authToken->id);
|
||||
|
||||
if ($authLogRecord) {
|
||||
$authLogRecord->set('authTokenId', $authToken->id);
|
||||
}
|
||||
}
|
||||
|
||||
if ($authLogRecord) {
|
||||
$this->getEntityManager()->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
if ($authToken && !$authLogRecord) {
|
||||
$authLogRecord = $this->getEntityManager()->getRepository('AuthLogRecord')->select(['id'])->where([
|
||||
'authTokenId' => $authToken->id
|
||||
])->order('requestTime', true)->findOne();
|
||||
}
|
||||
|
||||
if ($authLogRecord) {
|
||||
$user->set('authLogRecordId', $authLogRecord->id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function checkFailedAttemptsLimit($username = null)
|
||||
{
|
||||
$failedAttemptsPeriod = $this->getConfig()->get('authFailedAttemptsPeriod', self::FAILED_ATTEMPTS_PERIOD);
|
||||
$maxFailedAttempts = $this->getConfig()->get('authMaxFailedAttemptNumber', self::MAX_FAILED_ATTEMPT_NUMBER);
|
||||
|
||||
$requestTimeFrom = (new \DateTime('@' . intval($_SERVER['REQUEST_TIME_FLOAT'])))->modify('-' . $failedAttemptsPeriod);
|
||||
|
||||
$failAttemptCount = $this->getEntityManager()->getRepository('AuthLogRecord')->where([
|
||||
'requestTime>' => $requestTimeFrom->format('U'),
|
||||
'ipAddress' => $_SERVER['REMOTE_ADDR'],
|
||||
'isDenied' => true
|
||||
])->count();
|
||||
|
||||
if ($failAttemptCount > $maxFailedAttempts) {
|
||||
$GLOBALS['log']->warning("AUTH: Max failed login attempts exceeded for IP '".$_SERVER['REMOTE_ADDR']."'.");
|
||||
throw new Forbidden("Max failed login attempts exceeded.");
|
||||
}
|
||||
}
|
||||
|
||||
protected function createToken($user)
|
||||
protected function generateToken()
|
||||
{
|
||||
return md5(uniqid($user->get('id')));
|
||||
$length = 16;
|
||||
|
||||
if (function_exists('random_bytes')) {
|
||||
return bin2hex(random_bytes($length));
|
||||
}
|
||||
if (function_exists('mcrypt_create_iv')) {
|
||||
return bin2hex(mcrypt_create_iv($length, \MCRYPT_DEV_URANDOM));
|
||||
}
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
return bin2hex(openssl_random_pseudo_bytes($length));
|
||||
}
|
||||
}
|
||||
|
||||
public function destroyAuthToken($token)
|
||||
{
|
||||
$authToken = $this->getEntityManager()->getRepository('AuthToken')->where(array('token' => $token))->findOne();
|
||||
$authToken = $this->getEntityManager()->getRepository('AuthToken')->select(['id', 'isActive'])->where(['token' => $token])->findOne();
|
||||
if ($authToken) {
|
||||
$authToken->set('isActive', false);
|
||||
$this->getEntityManager()->saveEntity($authToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function createAuthLogRecord($username, $user)
|
||||
{
|
||||
if ($username === '**logout') return;
|
||||
|
||||
$authLogRecord = $this->getEntityManager()->getEntity('AuthLogRecord');
|
||||
|
||||
$authLogRecord->set([
|
||||
'username' => $username,
|
||||
'ipAddress' => $_SERVER['REMOTE_ADDR'],
|
||||
'requestTime' => $_SERVER['REQUEST_TIME_FLOAT'],
|
||||
'requestMethod' => $this->request->getMethod(),
|
||||
'requestUrl' => $this->request->getUrl() . $this->request->getPath()
|
||||
]);
|
||||
|
||||
if ($this->isPortal()) {
|
||||
$authLogRecord->set('portalId', $this->getPortal()->id);
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
$authLogRecord->set('userId', $user->id);
|
||||
} else {
|
||||
$authLogRecord->set('isDenied', true);
|
||||
$authLogRecord->set('denialReason', 'CREDENTIALS');
|
||||
$this->getEntityManager()->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
return $authLogRecord;
|
||||
}
|
||||
|
||||
protected function logDenied($authLogRecord, $denialReason)
|
||||
{
|
||||
if (!$authLogRecord) return;
|
||||
|
||||
$authLogRecord->set('denialReason', $denialReason);
|
||||
$this->getEntityManager()->saveEntity($authLogRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class LDAP extends Base
|
||||
|
||||
$ldapClient = $this->getLdapClient();
|
||||
|
||||
//login LDAP system user (ldapUsername, ldapPassword)
|
||||
/* Login LDAP system user (ldapUsername, ldapPassword) */
|
||||
try {
|
||||
$ldapClient->bind();
|
||||
} catch (\Exception $e) {
|
||||
@@ -119,17 +119,30 @@ class LDAP extends Base
|
||||
if (!isset($adminUser)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$GLOBALS['log']->info('LDAP: Administrator ['.$username.'] was logged in by Espo method.');
|
||||
}
|
||||
|
||||
if (!isset($adminUser)) {
|
||||
$userDn = $this->findLdapUserDnByUsername($username);
|
||||
$GLOBALS['log']->debug('Found DN for ['.$username.']: ['.$userDn.'].');
|
||||
try {
|
||||
$userDn = $this->findLdapUserDnByUsername($username);
|
||||
} catch (\Exception $e) {
|
||||
$GLOBALS['log']->error('Error while finding DN for ['.$username.'], details: ' . $e->getMessage() . '.');
|
||||
}
|
||||
|
||||
if (!isset($userDn)) {
|
||||
$GLOBALS['log']->error('LDAP: Authentication failed for user ['.$username.'], details: user is not found.');
|
||||
return;
|
||||
|
||||
$adminUser = $this->adminLogin($username, $password);
|
||||
if (!isset($adminUser)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$GLOBALS['log']->info('LDAP: Administrator ['.$username.'] was logged in by Espo method.');
|
||||
}
|
||||
|
||||
$GLOBALS['log']->debug('User ['.$username.'] is found with this DN ['.$userDn.'].');
|
||||
|
||||
try {
|
||||
$ldapClient->bind($userDn, $password);
|
||||
} catch (\Exception $e) {
|
||||
@@ -179,7 +192,7 @@ class LDAP extends Base
|
||||
$user = $this->getEntityManager()->getRepository('User')->findOne(array(
|
||||
'whereClause' => array(
|
||||
'userName' => $username,
|
||||
),
|
||||
)
|
||||
));
|
||||
|
||||
return $user;
|
||||
@@ -201,7 +214,7 @@ class LDAP extends Base
|
||||
'userName' => $username,
|
||||
'password' => $hash,
|
||||
'isAdmin' => 1
|
||||
),
|
||||
)
|
||||
));
|
||||
|
||||
return $user;
|
||||
@@ -238,6 +251,8 @@ class LDAP extends Base
|
||||
$data[$fieldName] = $fieldValue;
|
||||
}
|
||||
|
||||
$this->getAuth()->useNoAuth();
|
||||
|
||||
$user = $this->getEntityManager()->getEntity('User');
|
||||
$user->set($data);
|
||||
|
||||
|
||||
@@ -109,8 +109,5 @@ class ClientManager
|
||||
}
|
||||
|
||||
echo $html;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,10 +28,12 @@
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Cron;
|
||||
use \PDO;
|
||||
use \Espo\Core\CronManager;
|
||||
use \Espo\Core\Utils\Config;
|
||||
use \Espo\Core\ORM\EntityManager;
|
||||
|
||||
use PDO;
|
||||
use Espo\Core\CronManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\Utils\System;
|
||||
|
||||
class Job
|
||||
{
|
||||
@@ -64,15 +66,17 @@ class Job
|
||||
return $this->cronScheduledJob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Pending Jobs
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function isJobPending($id)
|
||||
{
|
||||
return !!$this->getEntityManager()->getRepository('Job')->select(['id'])->where([
|
||||
'id' => $id,
|
||||
'status' => CronManager::PENDING
|
||||
])->findOne();
|
||||
}
|
||||
|
||||
public function getPendingJobList()
|
||||
{
|
||||
$jobConfigs = $this->getConfig()->get('cron');
|
||||
$limit = !empty($jobConfigs['maxJobNumber']) ? intval($jobConfigs['maxJobNumber']) : 0;
|
||||
$limit = intval($this->getConfig()->get('jobMaxPortion', 0));
|
||||
|
||||
$selectParams = [
|
||||
'select' => [
|
||||
@@ -88,7 +92,7 @@ class Job
|
||||
'data'
|
||||
],
|
||||
'whereClause' => [
|
||||
'status' => 'Pending',
|
||||
'status' => CronManager::PENDING,
|
||||
'executeTime<=' => date('Y-m-d H:i:s')
|
||||
],
|
||||
'orderBy' => 'executeTime'
|
||||
@@ -97,17 +101,21 @@ class Job
|
||||
$selectParams['offset'] = 0;
|
||||
$selectParams['limit'] = $limit;
|
||||
}
|
||||
$jobList = $this->getEntityManager()->getRepository('Job')->find($selectParams);
|
||||
|
||||
$runningScheduledJobIdList = $this->getRunningScheduledJobIdList();
|
||||
return $this->getEntityManager()->getRepository('Job')->find($selectParams);
|
||||
}
|
||||
|
||||
$actualJobList = [];
|
||||
foreach ($jobList as $job) {
|
||||
if ($job->get('scheduledJobId') && in_array($job->get('scheduledJobId'), $runningScheduledJobIdList)) continue;
|
||||
$actualJobList[] = $job;
|
||||
public function isScheduledJobRunning($scheduledJobId, $targetId = null, $targetType = null)
|
||||
{
|
||||
$where = [
|
||||
'scheduledJobId' => $scheduledJobId,
|
||||
'status' => CronManager::RUNNING
|
||||
];
|
||||
if ($targetId && $targetType) {
|
||||
$where['targetId'] = $targetId;
|
||||
$where['targetType'] = $targetType;
|
||||
}
|
||||
|
||||
return $actualJobList;
|
||||
return !!$this->getEntityManager()->getRepository('Job')->select(['id'])->where($where)->findOne();
|
||||
}
|
||||
|
||||
public function getRunningScheduledJobIdList()
|
||||
@@ -175,39 +183,62 @@ class Job
|
||||
*/
|
||||
public function markFailedJobs()
|
||||
{
|
||||
$jobConfigs = $this->getConfig()->get('cron');
|
||||
$this->markFailedJobsByPeriod('jobPeriodForActiveProcess');
|
||||
$this->markFailedJobsByPeriod('jobPeriod');
|
||||
}
|
||||
|
||||
$currentTime = time();
|
||||
$periodTime = $currentTime - intval($jobConfigs['jobPeriod']);
|
||||
protected function markFailedJobsByPeriod($period)
|
||||
{
|
||||
$time = time() - $this->getConfig()->get($period);
|
||||
|
||||
$pdo = $this->getEntityManager()->getPDO();
|
||||
|
||||
$select = "
|
||||
SELECT id, scheduled_job_id, execute_time, target_id, target_type FROM `job`
|
||||
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', $periodTime)."'
|
||||
`status` = '" . CronManager::RUNNING ."' AND execute_time < '".date('Y-m-d H:i:s', $time)."'
|
||||
";
|
||||
$sth = $pdo->prepare($select);
|
||||
$sth->execute();
|
||||
|
||||
$jobData = array();
|
||||
while ($row = $sth->fetch(PDO::FETCH_ASSOC)){
|
||||
$jobData[$row['id']] = $row;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$update = "
|
||||
UPDATE job
|
||||
SET `status` = '". CronManager::FAILED ."'
|
||||
WHERE id IN ('".implode("', '", array_keys($jobData))."')
|
||||
";
|
||||
$sth = $pdo->prepare($update);
|
||||
$sth->execute();
|
||||
if (!empty($jobData)) {
|
||||
$jobQuotedIdList = [];
|
||||
foreach ($jobData as $jobId => $job) {
|
||||
$jobQuotedIdList[] = $pdo->quote($jobId);
|
||||
}
|
||||
|
||||
//add status 'Failed' to SchediledJobLog
|
||||
$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']);
|
||||
$update = "
|
||||
UPDATE job
|
||||
SET `status` = '" . CronManager::FAILED . "', attempts = 0
|
||||
WHERE id IN (".implode(", ", $jobQuotedIdList).")
|
||||
";
|
||||
|
||||
$sth = $pdo->prepare($update);
|
||||
$sth->execute();
|
||||
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,20 +277,29 @@ class Job
|
||||
$query = "
|
||||
SELECT id FROM `job`
|
||||
WHERE
|
||||
scheduled_job_id = '" . $row['scheduled_job_id'] . "' AND
|
||||
`status` = '" . CronManager::PENDING ."'
|
||||
scheduled_job_id = ".$pdo->quote($row['scheduled_job_id'])."
|
||||
AND `status` = '" . CronManager::PENDING ."'
|
||||
ORDER BY execute_time
|
||||
DESC LIMIT 1, 100000
|
||||
";
|
||||
$sth = $pdo->prepare($query);
|
||||
$sth->execute();
|
||||
$jobIds = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
$jobIdList = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (empty($jobIdList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quotedJobIdList = [];
|
||||
foreach ($jobIdList as $jobId) {
|
||||
$quotedJobIdList[] = $pdo->quote($jobId);
|
||||
}
|
||||
|
||||
$update = "
|
||||
UPDATE job
|
||||
SET deleted = 1
|
||||
WHERE
|
||||
id IN ('". implode("', '", $jobIds)."')
|
||||
id IN (".implode(", ", $quotedJobIdList).")
|
||||
";
|
||||
|
||||
$sth = $pdo->prepare($update);
|
||||
@@ -300,13 +340,18 @@ class Job
|
||||
UPDATE job
|
||||
SET
|
||||
`status` = '" . CronManager::PENDING ."',
|
||||
attempts = '".$attempts."',
|
||||
failed_attempts = '".$failedAttempts."'
|
||||
attempts = ".$pdo->quote($attempts).",
|
||||
failed_attempts = ".$pdo->quote($failedAttempts)."
|
||||
WHERE
|
||||
id = '".$row['id']."'
|
||||
id = ".$pdo->quote($row['id'])."
|
||||
";
|
||||
$pdo->prepare($update)->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getPid()
|
||||
{
|
||||
return System::getPid();
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ class ScheduledJob
|
||||
}
|
||||
|
||||
$scheduledJob->set('lastRun', $runTime);
|
||||
$entityManager->saveEntity($scheduledJob);
|
||||
$entityManager->saveEntity($scheduledJob, ['silent' => true]);
|
||||
|
||||
$scheduledJobLog = $entityManager->getEntity('ScheduledJobLogRecord');
|
||||
$scheduledJobLog->set(array(
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
namespace Espo\Core\Utils\Database;
|
||||
|
||||
use Espo\Core\Utils\Util,
|
||||
Espo\ORM\Entity;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class Converter
|
||||
{
|
||||
@@ -38,15 +38,18 @@ class Converter
|
||||
|
||||
private $fileManager;
|
||||
|
||||
private $config;
|
||||
|
||||
private $schemaConverter;
|
||||
|
||||
private $schemaFromMetadata = null;
|
||||
|
||||
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager)
|
||||
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager, \Espo\Core\Utils\Config $config = null)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->fileManager = $fileManager;
|
||||
$this->ormConverter = new Orm\Converter($this->metadata, $this->fileManager);
|
||||
$this->config = $config;
|
||||
$this->ormConverter = new Orm\Converter($this->metadata, $this->fileManager, $this->config);
|
||||
}
|
||||
|
||||
protected function getMetadata()
|
||||
|
||||
@@ -133,7 +133,6 @@ class MySqlPlatform extends \Doctrine\DBAL\Platforms\MySqlPlatform
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return array_merge($sql, $tableSql, $columnSql);
|
||||
}
|
||||
|
||||
@@ -305,10 +304,153 @@ class MySqlPlatform extends \Doctrine\DBAL\Platforms\MySqlPlatform
|
||||
protected function _getCreateTableSQL($tableName, array $columns, array $options = array())
|
||||
{
|
||||
if (!isset($options['engine'])) {
|
||||
$options['engine'] = 'MyISAM';
|
||||
$options['engine'] = 'InnoDB';
|
||||
}
|
||||
|
||||
return parent::_getCreateTableSQL($tableName, $columns, $options);
|
||||
if (!isset($options['charset'])) {
|
||||
$options['charset'] = 'utf8mb4';
|
||||
}
|
||||
|
||||
if (!isset($options['collate'])) {
|
||||
$options['collate'] = 'utf8mb4_unicode_ci';
|
||||
}
|
||||
|
||||
$queryFields = $this->getColumnDeclarationListSQL($columns);
|
||||
|
||||
if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
|
||||
foreach ($options['uniqueConstraints'] as $index => $definition) {
|
||||
$queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($index, $definition);
|
||||
}
|
||||
}
|
||||
|
||||
// add all indexes
|
||||
if (isset($options['indexes']) && ! empty($options['indexes'])) {
|
||||
foreach($options['indexes'] as $index => $definition) {
|
||||
$queryFields .= ', ' . $this->getIndexDeclarationSQL($index, $definition);
|
||||
}
|
||||
}
|
||||
|
||||
// attach all primary keys
|
||||
if (isset($options['primary']) && ! empty($options['primary'])) {
|
||||
$keyColumns = array_unique(array_values($options['primary']));
|
||||
$queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
|
||||
}
|
||||
|
||||
$query = 'CREATE ';
|
||||
|
||||
if (!empty($options['temporary'])) {
|
||||
$query .= 'TEMPORARY ';
|
||||
}
|
||||
|
||||
$query .= 'TABLE ' . $this->espoQuote($tableName) . ' (' . $queryFields . ') ';
|
||||
$query .= $this->buildTableOptions($options);
|
||||
$query .= $this->buildPartitionOptions($options);
|
||||
|
||||
$sql[] = $query;
|
||||
|
||||
if (isset($options['foreignKeys'])) {
|
||||
foreach ((array) $options['foreignKeys'] as $definition) {
|
||||
$sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public function getColumnCollationDeclarationSQL($collation)
|
||||
{
|
||||
return $this->getCollationFieldDeclaration($collation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build SQL for table options
|
||||
*
|
||||
* @param array $options
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function buildTableOptions(array $options)
|
||||
{
|
||||
if (isset($options['table_options'])) {
|
||||
return $options['table_options'];
|
||||
}
|
||||
|
||||
$tableOptions = array();
|
||||
|
||||
// Charset
|
||||
if ( ! isset($options['charset'])) {
|
||||
$options['charset'] = 'utf8';
|
||||
}
|
||||
|
||||
$tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']);
|
||||
|
||||
// Collate
|
||||
if ( ! isset($options['collate'])) {
|
||||
$options['collate'] = 'utf8_unicode_ci';
|
||||
}
|
||||
|
||||
$tableOptions[] = sprintf('COLLATE %s', $options['collate']);
|
||||
|
||||
// Engine
|
||||
if ( ! isset($options['engine'])) {
|
||||
$options['engine'] = 'InnoDB';
|
||||
}
|
||||
|
||||
$tableOptions[] = sprintf('ENGINE = %s', $options['engine']);
|
||||
|
||||
// Auto increment
|
||||
if (isset($options['auto_increment'])) {
|
||||
$tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']);
|
||||
}
|
||||
|
||||
// Comment
|
||||
if (isset($options['comment'])) {
|
||||
$comment = trim($options['comment'], " '");
|
||||
|
||||
$tableOptions[] = sprintf("COMMENT = '%s' ", str_replace("'", "''", $comment));
|
||||
}
|
||||
|
||||
// Row format
|
||||
if (isset($options['row_format'])) {
|
||||
$tableOptions[] = sprintf('ROW_FORMAT = %s', $options['row_format']);
|
||||
}
|
||||
|
||||
return implode(' ', $tableOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build SQL for partition options.
|
||||
*
|
||||
* @param array $options
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function buildPartitionOptions(array $options)
|
||||
{
|
||||
return (isset($options['partition_options']))
|
||||
? ' ' . $options['partition_options']
|
||||
: '';
|
||||
}
|
||||
|
||||
public function getClobTypeDeclarationSQL(array $field)
|
||||
{
|
||||
if ( ! empty($field['length']) && is_numeric($field['length'])) {
|
||||
$length = $field['length'];
|
||||
|
||||
if ($length <= static::LENGTH_LIMIT_TINYTEXT) {
|
||||
return 'TINYTEXT';
|
||||
}
|
||||
|
||||
if ($length <= static::LENGTH_LIMIT_TEXT) {
|
||||
return 'TEXT';
|
||||
}
|
||||
|
||||
if ($length > static::LENGTH_LIMIT_MEDIUMTEXT) {
|
||||
return 'LONGTEXT';
|
||||
}
|
||||
}
|
||||
|
||||
return 'MEDIUMTEXT';
|
||||
}
|
||||
//end: ESPO
|
||||
}
|
||||
188
application/Espo/Core/Utils/Database/Helper.php
Normal file
188
application/Espo/Core/Utils/Database/Helper.php
Normal file
@@ -0,0 +1,188 @@
|
||||
<?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\Database;
|
||||
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class Helper
|
||||
{
|
||||
private $config;
|
||||
|
||||
private $connection;
|
||||
|
||||
protected $drivers = array(
|
||||
'mysqli' => '\Espo\Core\Utils\Database\DBAL\Driver\Mysqli\Driver',
|
||||
'pdo_mysql' => '\Espo\Core\Utils\Database\DBAL\Driver\PDOMySql\Driver',
|
||||
);
|
||||
|
||||
public function __construct(\Espo\Core\Utils\Config $config = null)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
protected function getConfig()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getDbalConnection()
|
||||
{
|
||||
if (!isset($this->connection)) {
|
||||
if (!$this->getConfig()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$connectionParams = $this->getConfig()->get('database');
|
||||
|
||||
if (empty($connectionParams['dbname']) || empty($connectionParams['user'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$connectionParams['driverClass'] = $this->drivers[ $connectionParams['driver'] ];
|
||||
unset($connectionParams['driver']);
|
||||
|
||||
$dbalConfig = new \Doctrine\DBAL\Configuration();
|
||||
$this->connection = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $dbalConfig);
|
||||
}
|
||||
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum index length. If $tableName is empty get a value for all database tables
|
||||
*
|
||||
* @param string|null $tableName
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getMaxIndexLength($tableName = null, $default = 1000)
|
||||
{
|
||||
$mysqlEngine = $this->getMysqlEngine($tableName);
|
||||
if (!$mysqlEngine) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
switch ($mysqlEngine) {
|
||||
case 'InnoDB':
|
||||
$mysqlVersion = $this->getMysqlVersion();
|
||||
|
||||
if (version_compare($mysqlVersion, '10.0.0') >= 0) {
|
||||
return 767; //InnoDB, MariaDB
|
||||
}
|
||||
|
||||
if (version_compare($mysqlVersion, '5.7.0') >= 0) {
|
||||
return 3072; //InnoDB, MySQL 5.7+
|
||||
}
|
||||
|
||||
return 767; //InnoDB
|
||||
break;
|
||||
}
|
||||
|
||||
return 1000; //MyISAM
|
||||
}
|
||||
|
||||
public function getTableMaxIndexLength($tableName, $default = 1000)
|
||||
{
|
||||
return $this->getMaxIndexLength($tableName, $default);
|
||||
}
|
||||
|
||||
protected function getMysqlVersion()
|
||||
{
|
||||
$connection = $this->getDbalConnection();
|
||||
if (!$connection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $connection->fetchColumn("select version()");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table/database tables engine. If $tableName is empty get a value for all database tables
|
||||
*
|
||||
* @param string|null $tableName
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getMysqlEngine($tableName = null, $default = null)
|
||||
{
|
||||
$connection = $this->getDbalConnection();
|
||||
if (!$connection) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$query = "SHOW TABLE STATUS WHERE Engine = 'MyISAM'";
|
||||
if (!empty($tableName)) {
|
||||
$query = "SHOW TABLE STATUS WHERE Engine = 'MyISAM' AND Name = '" . $tableName . "'";
|
||||
}
|
||||
|
||||
$result = $connection->fetchColumn($query);
|
||||
|
||||
if (!empty($result)) {
|
||||
return 'MyISAM';
|
||||
}
|
||||
|
||||
return 'InnoDB';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if full text supports. If $tableName is empty get a value for all database tables
|
||||
*
|
||||
* @param string $tableName
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isSupportsFulltext($tableName = null, $default = false)
|
||||
{
|
||||
$mysqlEngine = $this->getMysqlEngine($tableName);
|
||||
if (!$mysqlEngine) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
switch ($mysqlEngine) {
|
||||
case 'InnoDB':
|
||||
$mysqlVersion = $this->getMysqlVersion();
|
||||
|
||||
if (version_compare($mysqlVersion, '5.6.4') >= 0) {
|
||||
return true; //InnoDB, MySQL 5.6.4+
|
||||
}
|
||||
|
||||
return false; //InnoDB
|
||||
break;
|
||||
}
|
||||
|
||||
return true; //MyISAM
|
||||
}
|
||||
|
||||
public function isTableSupportsFulltext($tableName, $default = false)
|
||||
{
|
||||
return $this->isSupportsFulltext($tableName, $default);
|
||||
}
|
||||
}
|
||||
@@ -28,20 +28,28 @@
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Orm;
|
||||
use Espo\Core\Utils\Util,
|
||||
Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class Converter
|
||||
{
|
||||
private $metadata;
|
||||
|
||||
private $fileManager;
|
||||
|
||||
private $config;
|
||||
|
||||
private $metadataHelper;
|
||||
|
||||
private $databaseHelper;
|
||||
|
||||
private $relationManager;
|
||||
|
||||
private $entityDefs;
|
||||
|
||||
protected $defaultFieldType = 'varchar';
|
||||
|
||||
protected $defaultNaming = 'postfix';
|
||||
|
||||
protected $defaultLength = array(
|
||||
@@ -62,6 +70,7 @@ class Converter
|
||||
'maxLength' => 'len',
|
||||
'len' => 'len',
|
||||
'notNull' => 'notNull',
|
||||
'exportDisabled' => 'notExportable',
|
||||
'autoincrement' => 'autoincrement',
|
||||
'entity' => 'entity',
|
||||
'notStorable' => 'notStorable',
|
||||
@@ -98,14 +107,16 @@ class Converter
|
||||
'additionalTables',
|
||||
);
|
||||
|
||||
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager)
|
||||
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager, \Espo\Core\Utils\Config $config = null)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->fileManager = $fileManager; //need to featue with ormHooks. Ex. isFollowed field
|
||||
$this->config = $config;
|
||||
|
||||
$this->relationManager = new RelationManager($this->metadata);
|
||||
|
||||
$this->metadataHelper = new \Espo\Core\Utils\Metadata\Helper($this->metadata);
|
||||
$this->databaseHelper = new \Espo\Core\Utils\Database\Helper($this->config);
|
||||
}
|
||||
|
||||
protected function getMetadata()
|
||||
@@ -113,6 +124,11 @@ class Converter
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
protected function getConfig()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
protected function getEntityDefs($reload = false)
|
||||
{
|
||||
if (empty($this->entityDefs) || $reload) {
|
||||
@@ -137,6 +153,11 @@ class Converter
|
||||
return $this->metadataHelper;
|
||||
}
|
||||
|
||||
protected function getDatabaseHelper()
|
||||
{
|
||||
return $this->databaseHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orm metadata convertation process
|
||||
*
|
||||
@@ -185,6 +206,8 @@ class Converter
|
||||
|
||||
$ormMetadata = Util::merge($ormMetadata, $convertedLinks);
|
||||
|
||||
$this->applyFullTextSearch($ormMetadata, $entityName);
|
||||
|
||||
if (!empty($entityMetadata['collection']) && is_array($entityMetadata['collection'])) {
|
||||
$collectionDefs = $entityMetadata['collection'];
|
||||
$ormMetadata[$entityName]['collection'] = array();
|
||||
@@ -275,7 +298,8 @@ class Converter
|
||||
)
|
||||
);
|
||||
|
||||
foreach($entityMetadata['fields'] as $fieldName => $fieldParams) {
|
||||
foreach ($entityMetadata['fields'] as $fieldName => $fieldParams) {
|
||||
if (empty($fieldParams['type'])) continue;
|
||||
|
||||
/** check if "fields" option exists in $fieldMeta */
|
||||
$fieldTypeMetadata = $this->getMetadataHelper()->getFieldDefsByType($fieldParams);
|
||||
@@ -465,4 +489,47 @@ class Converter
|
||||
return $values;
|
||||
}
|
||||
|
||||
protected function applyFullTextSearch(&$ormMetadata, $entityType)
|
||||
{
|
||||
if (!$this->getDatabaseHelper()->isTableSupportsFulltext(Util::toUnderScore($entityType))) return;
|
||||
if (!$this->getMetadata()->get(['entityDefs', $entityType, 'collection', 'fullTextSearch'])) return;
|
||||
|
||||
$fieldList = $this->getMetadata()->get(['entityDefs', $entityType, 'collection', 'textFilterFields'], ['name']);
|
||||
|
||||
$fullTextSearchColumnList = [];
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
$defs = $this->getMetadata()->get(['entityDefs', $entityType, 'fields', $field], []);
|
||||
if (empty($defs['type'])) continue;
|
||||
$fieldType = $defs['type'];
|
||||
if (!empty($defs['notStorable'])) continue;
|
||||
if (!$this->getMetadata()->get(['fields', $fieldType, 'fullTextSearch'])) continue;
|
||||
|
||||
$partList = $this->getMetadata()->get(['fields', $fieldType, 'fullTextSearchColumnList']);
|
||||
if ($partList) {
|
||||
if ($this->getMetadata()->get(['fields', $fieldType, 'naming']) === 'prefix') {
|
||||
foreach ($partList as $part) {
|
||||
$fullTextSearchColumnList[] = $part . ucfirst($field);
|
||||
}
|
||||
} else {
|
||||
foreach ($partList as $part) {
|
||||
$fullTextSearchColumnList[] = $field . ucfirst($part);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$fullTextSearchColumnList[] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fullTextSearchColumnList)) {
|
||||
$ormMetadata[$entityType]['fullTextSearchColumnList'] = $fullTextSearchColumnList;
|
||||
if (!array_key_exists('indexes', $ormMetadata[$entityType])) {
|
||||
$ormMetadata[$entityType]['indexes'] = [];
|
||||
}
|
||||
$ormMetadata[$entityType]['indexes']['system_fullTextSearch'] = [
|
||||
'columns' => $fullTextSearchColumnList,
|
||||
'flags' => ['fulltext']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,20 +37,23 @@ class AttachmentMultiple extends Base
|
||||
$entityType => array (
|
||||
'fields' => array(
|
||||
$fieldName.'Ids' => array(
|
||||
'type' => 'varchar',
|
||||
'notStorable' => true
|
||||
'type' => 'jsonArray',
|
||||
'notStorable' => true,
|
||||
'orderBy' => [['createdAt', 'ASC'], ['name', 'ASC']],
|
||||
'isLinkMultipleIdList' => true
|
||||
),
|
||||
$fieldName.'Names' => array(
|
||||
'type' => 'varchar',
|
||||
'notStorable' => true
|
||||
),
|
||||
'type' => 'jsonObject',
|
||||
'notStorable' => true,
|
||||
'isLinkMultipleNameMap' => true
|
||||
)
|
||||
)
|
||||
),
|
||||
'unset' => array(
|
||||
$entityType => array(
|
||||
'fields.'.$fieldName,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return $data;
|
||||
|
||||
@@ -46,45 +46,50 @@ class Email extends Base
|
||||
JOIN email_address ON email_address.id = entity_email_address.email_address_id
|
||||
WHERE
|
||||
entity_email_address.deleted = 0 AND entity_email_address.entity_type = '{$entityName}' AND
|
||||
email_address.deleted = 0 AND email_address.name LIKE {value}
|
||||
email_address.deleted = 0 AND email_address.lower LIKE {value}
|
||||
)",
|
||||
'=' => array(
|
||||
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
|
||||
'sql' => 'emailAddressesMultiple.name = {value}',
|
||||
'sql' => 'emailAddressesMultiple.lower = {value}',
|
||||
'distinct' => true
|
||||
),
|
||||
'<>' => array(
|
||||
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
|
||||
'sql' => 'emailAddressesMultiple.name <> {value}',
|
||||
'sql' => 'emailAddressesMultiple.lower <> {value}',
|
||||
'distinct' => true
|
||||
),
|
||||
'IN' => array(
|
||||
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
|
||||
'sql' => 'emailAddressesMultiple.name IN {value}',
|
||||
'sql' => 'emailAddressesMultiple.lower IN {value}',
|
||||
'distinct' => true
|
||||
),
|
||||
'NOT IN' => array(
|
||||
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
|
||||
'sql' => 'emailAddressesMultiple.name NOT IN {value}',
|
||||
'sql' => 'emailAddressesMultiple.lower NOT IN {value}',
|
||||
'distinct' => true
|
||||
),
|
||||
'IS NULL' => array(
|
||||
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
|
||||
'sql' => 'emailAddressesMultiple.name IS NULL',
|
||||
'sql' => 'emailAddressesMultiple.lower IS NULL',
|
||||
'distinct' => true
|
||||
),
|
||||
'IS NOT NULL' => array(
|
||||
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
|
||||
'sql' => 'emailAddressesMultiple.name IS NOT NULL',
|
||||
'sql' => 'emailAddressesMultiple.lower IS NOT NULL',
|
||||
'distinct' => true
|
||||
)
|
||||
),
|
||||
'orderBy' => 'emailAddresses.name {direction}',
|
||||
'orderBy' => 'emailAddresses.lower {direction}',
|
||||
),
|
||||
$fieldName .'Data' => array(
|
||||
'type' => 'text',
|
||||
'notStorable' => true
|
||||
),
|
||||
$fieldName .'IsOptedOut' => array(
|
||||
'type' => 'bool',
|
||||
'notStorable' => true,
|
||||
'select' => 'emailAddresses.opt_out'
|
||||
)
|
||||
),
|
||||
'relations' => array(
|
||||
'emailAddresses' => array(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user