mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-06 14:37:02 +00:00
Compare commits
944 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99b4205181 | ||
|
|
5ae56454fc | ||
|
|
1e2369bba1 | ||
|
|
c42bfa3aab | ||
|
|
2b01cefb66 | ||
|
|
0b72848e33 | ||
|
|
ecc7f80c80 | ||
|
|
be5fff011b | ||
|
|
d7a29fdc71 | ||
|
|
e1c2203750 | ||
|
|
66cde8c86c | ||
|
|
10fb006fd8 | ||
|
|
b94c5ff869 | ||
|
|
39898bbbe8 | ||
|
|
62a6c2761f | ||
|
|
6fe6a52f47 | ||
|
|
c5d7fa5b0b | ||
|
|
ceaac6c3dc | ||
|
|
490843e371 | ||
|
|
b290fbfeb2 | ||
|
|
ca1e649b11 | ||
|
|
5e74cd8d06 | ||
|
|
63508423cc | ||
|
|
a5961811b4 | ||
|
|
fdb1595cd5 | ||
|
|
4bfedf8db3 | ||
|
|
325429eb52 | ||
|
|
e558a3139c | ||
|
|
e424c26963 | ||
|
|
b2cc1f97c4 | ||
|
|
d48ce0fc58 | ||
|
|
b61462a1cd | ||
|
|
1bb82151da | ||
|
|
9852e25c6e | ||
|
|
90fe358753 | ||
|
|
bd65e62fce | ||
|
|
fff9d7a781 | ||
|
|
8441fa05d2 | ||
|
|
9e86649b1c | ||
|
|
3c50a0e058 | ||
|
|
e571a63a30 | ||
|
|
14d405542b | ||
|
|
429d03504c | ||
|
|
fc24840a7b | ||
|
|
603ca985df | ||
|
|
a12940e36c | ||
|
|
dcab0ad7d0 | ||
|
|
89124b354b | ||
|
|
03c275f495 | ||
|
|
da89e0a321 | ||
|
|
90af51b8b9 | ||
|
|
92b82b9d4c | ||
|
|
c0aabe65d7 | ||
|
|
6e6478bb6a | ||
|
|
d457b461e7 | ||
|
|
2f6673f8d9 | ||
|
|
ee94408394 | ||
|
|
6d343cb564 | ||
|
|
f5c07c9d6b | ||
|
|
f9f2eaeb1f | ||
|
|
16d8015d92 | ||
|
|
2fa330d6f3 | ||
|
|
3bd2c8a813 | ||
|
|
12ba23e8ab | ||
|
|
e7c3314285 | ||
|
|
13ec8dd49f | ||
|
|
80146e8bca | ||
|
|
b974c673d2 | ||
|
|
c7ae23fd01 | ||
|
|
ccd21ebf76 | ||
|
|
f5c1fdebf3 | ||
|
|
863cd03eb3 | ||
|
|
d3eb08b171 | ||
|
|
5a39450e6d | ||
|
|
dd40786b6f | ||
|
|
35e894a391 | ||
|
|
8bd2b32908 | ||
|
|
ce58cc262f | ||
|
|
2dad119c3c | ||
|
|
7e872e845c | ||
|
|
678e39b0e5 | ||
|
|
ce10665bc3 | ||
|
|
d091ebbd23 | ||
|
|
68e495566d | ||
|
|
9943e413c1 | ||
|
|
8f4f4b4ac1 | ||
|
|
4a7f967d4f | ||
|
|
abf5690239 | ||
|
|
bc6946e6be | ||
|
|
06001a4b4c | ||
|
|
d5d913cf12 | ||
|
|
37f7fa8464 | ||
|
|
27c98bdf0f | ||
|
|
2c223a0739 | ||
|
|
3c756e6252 | ||
|
|
e7331efcbe | ||
|
|
62ae30aa8f | ||
|
|
c49f6c8045 | ||
|
|
16ce6eb0b9 | ||
|
|
701422892a | ||
|
|
abd2016444 | ||
|
|
cc68bd640e | ||
|
|
c247a24db4 | ||
|
|
b8a484cb7e | ||
|
|
61d546a649 | ||
|
|
23b9c08752 | ||
|
|
191f834e0f | ||
|
|
9fa9198963 | ||
|
|
f89e8acb79 | ||
|
|
33fbe125fa | ||
|
|
d93dd9f6df | ||
|
|
08841f7da5 | ||
|
|
725222e4c2 | ||
|
|
003f7864b3 | ||
|
|
48efcdb7a8 | ||
|
|
7bc8c2f161 | ||
|
|
4e3cb38477 | ||
|
|
20ac2dd2d7 | ||
|
|
0cabbaab01 | ||
|
|
8cc7adee3f | ||
|
|
9d297d5587 | ||
|
|
6e2409d969 | ||
|
|
b8cdbfb585 | ||
|
|
5c06345424 | ||
|
|
142cbfa4b8 | ||
|
|
04018bba93 | ||
|
|
e77143640b | ||
|
|
2964fff2cd | ||
|
|
0c8cdb61dc | ||
|
|
a9e0dad0dd | ||
|
|
b34d8ebca8 | ||
|
|
ed795d0d49 | ||
|
|
2673f60831 | ||
|
|
09b56fd8c0 | ||
|
|
44175614d4 | ||
|
|
1b7657bf0c | ||
|
|
469fcdb8bc | ||
|
|
fde53deae6 | ||
|
|
e470bf4eb1 | ||
|
|
5011f1e197 | ||
|
|
bc37a83982 | ||
|
|
d97b9be4c9 | ||
|
|
e51a9621f3 | ||
|
|
6dde915b7e | ||
|
|
a109e1353d | ||
|
|
8018478b13 | ||
|
|
9edb8bd59a | ||
|
|
3b3b05286a | ||
|
|
bb534e6c46 | ||
|
|
fce05fd5d2 | ||
|
|
875be06d28 | ||
|
|
0125ad0db9 | ||
|
|
4634c5d30f | ||
|
|
6bd717fe5d | ||
|
|
be56156516 | ||
|
|
3da90da35b | ||
|
|
6b9ebdc731 | ||
|
|
1d0d12649f | ||
|
|
e2aa06f755 | ||
|
|
19e4943410 | ||
|
|
668233a978 | ||
|
|
5a3bfe5f68 | ||
|
|
90ff998e5c | ||
|
|
71b7c1af2b | ||
|
|
997e5cc44c | ||
|
|
3f8e6b2854 | ||
|
|
3bf7ec7e9d | ||
|
|
1d7c47f005 | ||
|
|
b1d77bdca0 | ||
|
|
36a9563e69 | ||
|
|
67e1917426 | ||
|
|
344714fda0 | ||
|
|
836de2d624 | ||
|
|
2c5e548c07 | ||
|
|
f0ac0dc03e | ||
|
|
2bb53c5495 | ||
|
|
e0256018de | ||
|
|
019cea2641 | ||
|
|
d8286a2de6 | ||
|
|
685f034e15 | ||
|
|
3210733bc7 | ||
|
|
38ad6def8c | ||
|
|
b2d31d0230 | ||
|
|
ddd9a1dc88 | ||
|
|
3355ee7192 | ||
|
|
a4e694412c | ||
|
|
7222a5a546 | ||
|
|
423f26cb29 | ||
|
|
fe4679cbc5 | ||
|
|
df0c56bd9b | ||
|
|
269f38e07c | ||
|
|
48c89aec97 | ||
|
|
2cca4fe429 | ||
|
|
8672fd1dff | ||
|
|
755d3b863a | ||
|
|
453e2bd1a0 | ||
|
|
9ab35f457c | ||
|
|
0a60ffefa5 | ||
|
|
47fad3eb07 | ||
|
|
7cca2353d8 | ||
|
|
089c8d56f8 | ||
|
|
17c9379c15 | ||
|
|
d6d83a209f | ||
|
|
86b77266bb | ||
|
|
9339082f9b | ||
|
|
bac385545c | ||
|
|
3e4b4f2df8 | ||
|
|
990406889b | ||
|
|
487d8dc909 | ||
|
|
4945e19fdf | ||
|
|
0eecaf3d5a | ||
|
|
337dd67c36 | ||
|
|
e8f0d38554 | ||
|
|
e7904b976b | ||
|
|
2360b75f97 | ||
|
|
785c3a8545 | ||
|
|
1d1fccaed9 | ||
|
|
fb921bb023 | ||
|
|
0ffe39cec8 | ||
|
|
21034bfeb2 | ||
|
|
94188f4256 | ||
|
|
20f6d67f40 | ||
|
|
a3c289aee2 | ||
|
|
e56121bc18 | ||
|
|
5b90d4c3f3 | ||
|
|
f69eed63d7 | ||
|
|
06c173486e | ||
|
|
32f1bfb1c6 | ||
|
|
68fee2ec9f | ||
|
|
d15ab2df31 | ||
|
|
cb7f87a3f1 | ||
|
|
6783331aab | ||
|
|
88d4b6f27c | ||
|
|
3347b7fba8 | ||
|
|
cff703db05 | ||
|
|
16ba6ce7cf | ||
|
|
d4bda1fa9d | ||
|
|
06b5100e87 | ||
|
|
f1a3cd397a | ||
|
|
2481e1a652 | ||
|
|
e32ad76590 | ||
|
|
81f1374f55 | ||
|
|
d3fd314e35 | ||
|
|
20b1b06d5f | ||
|
|
a364ae1923 | ||
|
|
bb2ce37a38 | ||
|
|
fd092a3eb1 | ||
|
|
fb3bdde2c3 | ||
|
|
66623c02e2 | ||
|
|
22fded93fb | ||
|
|
6da20c6f86 | ||
|
|
34834b2d5c | ||
|
|
c971304b03 | ||
|
|
6c7d424349 | ||
|
|
f8e43a3694 | ||
|
|
17dddbc248 | ||
|
|
2246ab0cfd | ||
|
|
a42f2cbe3a | ||
|
|
ad4c039b4a | ||
|
|
2638fdb884 | ||
|
|
c0cfe8a36f | ||
|
|
b9ab872ccd | ||
|
|
018b0b46aa | ||
|
|
7bcd347f2d | ||
|
|
a6db71957a | ||
|
|
1a5df659cf | ||
|
|
2219f52140 | ||
|
|
78dde9c7a1 | ||
|
|
3f0e2a242d | ||
|
|
64aebdde6b | ||
|
|
24f6db674f | ||
|
|
bfc4fb7ca8 | ||
|
|
d46945dd2e | ||
|
|
51130d1aef | ||
|
|
6eb71a789e | ||
|
|
9c77169e6c | ||
|
|
26f1218240 | ||
|
|
8b09a81237 | ||
|
|
f3ee5c654b | ||
|
|
19c8fe9ac5 | ||
|
|
81f45d5679 | ||
|
|
1a413cb54e | ||
|
|
3ab2ffee3c | ||
|
|
53a481622a | ||
|
|
7d72a7ff71 | ||
|
|
314cc7f4c2 | ||
|
|
df1228f720 | ||
|
|
bbd071221a | ||
|
|
9235a3cf79 | ||
|
|
75c966e4bb | ||
|
|
c298f5ec9a | ||
|
|
94986d8835 | ||
|
|
ece8405b33 | ||
|
|
0d2d708ba8 | ||
|
|
ef31f7ba08 | ||
|
|
41bcaf50c4 | ||
|
|
dcd3fa0fc8 | ||
|
|
b48b9683ab | ||
|
|
2548f396ef | ||
|
|
0122b99a4b | ||
|
|
fb747d3f65 | ||
|
|
41cb0de44e | ||
|
|
f7442be97b | ||
|
|
20ccacddf3 | ||
|
|
1ce56bb522 | ||
|
|
f2330b9a51 | ||
|
|
0346a0023e | ||
|
|
bc7d9443b1 | ||
|
|
8188dc065b | ||
|
|
a2025d0a89 | ||
|
|
c46933f427 | ||
|
|
1d31637c2e | ||
|
|
e26026c9ad | ||
|
|
f9e4d1a953 | ||
|
|
6f00a6b2e7 | ||
|
|
a89860dc52 | ||
|
|
626c23b1b5 | ||
|
|
7513d05451 | ||
|
|
5e7aabc46c | ||
|
|
740baffeb4 | ||
|
|
2ba808c371 | ||
|
|
8c7f9f43e4 | ||
|
|
2df98585f9 | ||
|
|
9d4266bed0 | ||
|
|
cb313cd7ef | ||
|
|
ce54d516e9 | ||
|
|
6bce395daf | ||
|
|
b634daca6e | ||
|
|
338e0bb9d4 | ||
|
|
cc576a6af8 | ||
|
|
cbb0159d27 | ||
|
|
515b43614b | ||
|
|
736d23fa6d | ||
|
|
7a096cdde4 | ||
|
|
5b0787474e | ||
|
|
8c87f20374 | ||
|
|
ba35115a48 | ||
|
|
069010d0fe | ||
|
|
fe5878fd99 | ||
|
|
ee37960259 | ||
|
|
3ce8fae228 | ||
|
|
13ebc558e9 | ||
|
|
3ae342c275 | ||
|
|
a283305c7b | ||
|
|
839ceea142 | ||
|
|
a6c698588c | ||
|
|
3007976506 | ||
|
|
8189f0eb2f | ||
|
|
248063f327 | ||
|
|
3a76807a2b | ||
|
|
6e6d3e15ea | ||
|
|
eee086ef52 | ||
|
|
9cce9d7347 | ||
|
|
69d0dbbf1c | ||
|
|
92695c0c39 | ||
|
|
44346e962c | ||
|
|
68ef9ce4ac | ||
|
|
83a415cb33 | ||
|
|
163cf047e5 | ||
|
|
02efdf11f5 | ||
|
|
cc574afd3d | ||
|
|
26a0c4b108 | ||
|
|
77d76fe0ee | ||
|
|
caeaf46403 | ||
|
|
c254f5cc0d | ||
|
|
e7ab75ec5a | ||
|
|
53c3f4b4c3 | ||
|
|
53a12e17f8 | ||
|
|
2ab5489bec | ||
|
|
a5c87272b1 | ||
|
|
130bae6a88 | ||
|
|
c520a02ca5 | ||
|
|
b8d5612d32 | ||
|
|
6ecf5fcdd0 | ||
|
|
5a93722232 | ||
|
|
d6035523e2 | ||
|
|
ff6983c9f3 | ||
|
|
e2673473c5 | ||
|
|
e8bf70ab8e | ||
|
|
f459d5811d | ||
|
|
8b49e72f2b | ||
|
|
53bf9b024a | ||
|
|
a1292cf933 | ||
|
|
5ccfd77669 | ||
|
|
67a27ec21b | ||
|
|
25aed1a1c4 | ||
|
|
b1de15339a | ||
|
|
3f819500d3 | ||
|
|
435be717c3 | ||
|
|
d2f4fbc59d | ||
|
|
2589801993 | ||
|
|
9e0a77588d | ||
|
|
5f954c22da | ||
|
|
423e2ca544 | ||
|
|
d033b26e57 | ||
|
|
6176b8770f | ||
|
|
cbced73f6e | ||
|
|
9168dd69f2 | ||
|
|
c69fc7f2c9 | ||
|
|
faebc13757 | ||
|
|
8ab7452859 | ||
|
|
bd6e0023c3 | ||
|
|
5f00c85882 | ||
|
|
6c8cffeb2a | ||
|
|
683eb5a491 | ||
|
|
07035bf8bf | ||
|
|
9e8df41174 | ||
|
|
64736349f0 | ||
|
|
71f389703d | ||
|
|
669701c6fd | ||
|
|
02917943b6 | ||
|
|
7221963990 | ||
|
|
062043d5e1 | ||
|
|
acf1833d9f | ||
|
|
9c45902213 | ||
|
|
f9674bd60c | ||
|
|
831d840cc5 | ||
|
|
adc3df5144 | ||
|
|
3f6f718cd9 | ||
|
|
c35b5c5aa5 | ||
|
|
0050f44a8a | ||
|
|
f86479afdc | ||
|
|
f4cbb5e56a | ||
|
|
6c8e9b129a | ||
|
|
c033cb171e | ||
|
|
bdf7d56e0c | ||
|
|
3e7326f605 | ||
|
|
61e51bfb31 | ||
|
|
c35d934ba7 | ||
|
|
80fa391daa | ||
|
|
1092b17a13 | ||
|
|
1de6568918 | ||
|
|
ee7c7046ac | ||
|
|
19638dd649 | ||
|
|
df30678484 | ||
|
|
b05697d874 | ||
|
|
91177fc0d2 | ||
|
|
e56c64dba4 | ||
|
|
7ecc0dc6a9 | ||
|
|
808e2f8788 | ||
|
|
7682539114 | ||
|
|
890649e46b | ||
|
|
33d710d265 | ||
|
|
0558739e67 | ||
|
|
552983ca33 | ||
|
|
d328872479 | ||
|
|
6cee542972 | ||
|
|
afb77424d4 | ||
|
|
c7465970bd | ||
|
|
4f6ae321b9 | ||
|
|
b92970992f | ||
|
|
4923984e2d | ||
|
|
071fb20f7b | ||
|
|
3e7d0f23c8 | ||
|
|
268009e5f6 | ||
|
|
573d810d37 | ||
|
|
b3f3226f55 | ||
|
|
511b7b0d9d | ||
|
|
3e54b58c8c | ||
|
|
92467afad3 | ||
|
|
a1c6004c4f | ||
|
|
0d8943d86a | ||
|
|
07ca2b6ceb | ||
|
|
e019de24cc | ||
|
|
7b417313a2 | ||
|
|
cb6777d19b | ||
|
|
2790834c62 | ||
|
|
9162b61a3f | ||
|
|
8488afb6f6 | ||
|
|
1b518f4b6f | ||
|
|
92bad5240b | ||
|
|
5f70d1b408 | ||
|
|
0941bd440e | ||
|
|
2fabf4bfe1 | ||
|
|
f9e13e8093 | ||
|
|
d1f0fd4fb8 | ||
|
|
7c9ae7caa4 | ||
|
|
2adcab4417 | ||
|
|
2fd367cd7d | ||
|
|
82c74c23f9 | ||
|
|
9de57ba4b2 | ||
|
|
53d544a6d5 | ||
|
|
5f550f38f9 | ||
|
|
63a1c0be66 | ||
|
|
a43903dfd2 | ||
|
|
ce369c774b | ||
|
|
971543ab02 | ||
|
|
bf8b9a7e16 | ||
|
|
d727cea9ee | ||
|
|
373e05d219 | ||
|
|
8fa5cf4af1 | ||
|
|
776ae3ac9d | ||
|
|
f3d9f93564 | ||
|
|
d32995df9b | ||
|
|
6c8e5b2c92 | ||
|
|
b44e536e35 | ||
|
|
8e9045352d | ||
|
|
97499626db | ||
|
|
65cf99faad | ||
|
|
5719f29262 | ||
|
|
06811d9fcc | ||
|
|
9000dba6fd | ||
|
|
b8734f68da | ||
|
|
a6eb683770 | ||
|
|
00a8c7326d | ||
|
|
8faa5b91e2 | ||
|
|
afeeba80f7 | ||
|
|
b6daa5accb | ||
|
|
7bf5c9ba11 | ||
|
|
2433da0db7 | ||
|
|
b820c880a7 | ||
|
|
c7c31ee73a | ||
|
|
18ec05f63e | ||
|
|
cf42c312cf | ||
|
|
d76bb5677b | ||
|
|
9afaf54dea | ||
|
|
99490fdf4c | ||
|
|
244edbbcac | ||
|
|
2fab584c60 | ||
|
|
9d797ae17a | ||
|
|
15860096d3 | ||
|
|
c0ac579b7f | ||
|
|
50d5280d2c | ||
|
|
b1c5526286 | ||
|
|
384902f349 | ||
|
|
86e75a4d47 | ||
|
|
0401f0cce6 | ||
|
|
5b54f1b579 | ||
|
|
62370b6c8d | ||
|
|
edc9d00be3 | ||
|
|
73d4250daa | ||
|
|
aa052560bf | ||
|
|
b35c2ca97f | ||
|
|
d2946cacfa | ||
|
|
eaaceb74bd | ||
|
|
2138591b72 | ||
|
|
3583b586c2 | ||
|
|
965e5149f3 | ||
|
|
be899bc0e2 | ||
|
|
e23e8854d7 | ||
|
|
2b977e5456 | ||
|
|
1c7f65db1a | ||
|
|
cdda1e0514 | ||
|
|
9fc955d7b5 | ||
|
|
0372374e1f | ||
|
|
c615f0d8db | ||
|
|
f4b9b3fd81 | ||
|
|
da06eb9426 | ||
|
|
2a3f3f2d04 | ||
|
|
19a8535dbe | ||
|
|
363a1c1b06 | ||
|
|
5c2b3f707b | ||
|
|
73f88bc097 | ||
|
|
7ebcc6c71a | ||
|
|
af3baef0cb | ||
|
|
14ea8886f6 | ||
|
|
852977d5db | ||
|
|
4178406453 | ||
|
|
149649be09 | ||
|
|
2a17155f96 | ||
|
|
d9488ba71c | ||
|
|
275c3aa492 | ||
|
|
9f5975eb5b | ||
|
|
a1d5174372 | ||
|
|
547b77423a | ||
|
|
a2c81e5891 | ||
|
|
f1070612b4 | ||
|
|
c5828a9feb | ||
|
|
62b2314ff3 | ||
|
|
dce9c437bf | ||
|
|
e5a012cdc7 | ||
|
|
17f18e36fd | ||
|
|
e99fca0252 | ||
|
|
316a865d02 | ||
|
|
0f6049b36f | ||
|
|
63771d37b3 | ||
|
|
9e50367303 | ||
|
|
71b0dfec04 | ||
|
|
90c692648d | ||
|
|
e968424b9d | ||
|
|
ed77c8758c | ||
|
|
caa536f17a | ||
|
|
7463caf8ee | ||
|
|
d4096b8dfb | ||
|
|
f73e4f2b47 | ||
|
|
fc93a3d029 | ||
|
|
0bcbfdd223 | ||
|
|
6b1f8de16a | ||
|
|
64741be18b | ||
|
|
8ad8a42ecd | ||
|
|
ead2d3829b | ||
|
|
55be65d48c | ||
|
|
ecee6e7477 | ||
|
|
c037870e7e | ||
|
|
e86c4a9b6b | ||
|
|
586e28bd26 | ||
|
|
3ca7cdffcc | ||
|
|
d36dc4bc1c | ||
|
|
ede53970bf | ||
|
|
0348778bc1 | ||
|
|
3e2a3597ad | ||
|
|
1c5afb52b3 | ||
|
|
8601c8fc24 | ||
|
|
5b3b7b583d | ||
|
|
9f0827e478 | ||
|
|
a0858acae3 | ||
|
|
49a49d6103 | ||
|
|
7d9d248ab7 | ||
|
|
acc0b71a7a | ||
|
|
1f3a0bb5dd | ||
|
|
c5611a691f | ||
|
|
72927e34e2 | ||
|
|
6c2a114c47 | ||
|
|
edb5f30ddd | ||
|
|
94881de082 | ||
|
|
ce0baf450e | ||
|
|
3854141292 | ||
|
|
152bc76e57 | ||
|
|
6d01717f7e | ||
|
|
b026ff8066 | ||
|
|
f9270a20d4 | ||
|
|
5196343479 | ||
|
|
3606051b91 | ||
|
|
3fc4b487b3 | ||
|
|
692e0b43e3 | ||
|
|
c273e26cf9 | ||
|
|
a26a48fac7 | ||
|
|
2a42ab8db5 | ||
|
|
16d66aa642 | ||
|
|
85d167aad4 | ||
|
|
cbec1cbbe5 | ||
|
|
1509f8d18f | ||
|
|
e6d618f142 | ||
|
|
455c51e806 | ||
|
|
d9f553d47f | ||
|
|
5dc08ecf05 | ||
|
|
2d791010dc | ||
|
|
cc8e2511e0 | ||
|
|
92b0af358e | ||
|
|
9526614a3d | ||
|
|
9fca726329 | ||
|
|
83a9f53fe3 | ||
|
|
da27b68312 | ||
|
|
d2e90f7853 | ||
|
|
0f7cd11ffe | ||
|
|
b006349528 | ||
|
|
f03637ac64 | ||
|
|
e0909af3fe | ||
|
|
1ed38e28cd | ||
|
|
567db6b204 | ||
|
|
04689b8ed0 | ||
|
|
c007010a73 | ||
|
|
498dd5478b | ||
|
|
4e9e71b1bb | ||
|
|
06771f2f3e | ||
|
|
bafe2dd471 | ||
|
|
4591d8f634 | ||
|
|
f1e42f931b | ||
|
|
8cf31f17cb | ||
|
|
40c8d0063d | ||
|
|
157f03b38f | ||
|
|
ffa5cf44ea | ||
|
|
cc88ab9290 | ||
|
|
1a179c2373 | ||
|
|
066f815d0c | ||
|
|
8078043f3c | ||
|
|
06de056bb5 | ||
|
|
630bdd7885 | ||
|
|
847440ad6c | ||
|
|
0271deddde | ||
|
|
65a385dfcf | ||
|
|
71dd872618 | ||
|
|
8025c1e101 | ||
|
|
fe15332531 | ||
|
|
508699d0aa | ||
|
|
0465f826f1 | ||
|
|
f78d38d592 | ||
|
|
db7142e4f9 | ||
|
|
e1b4fd6bdb | ||
|
|
483ffb91fa | ||
|
|
76a5721c72 | ||
|
|
27c5bec755 | ||
|
|
8e3016e301 | ||
|
|
d93f08b18e | ||
|
|
701abe129b | ||
|
|
e585ef0a0d | ||
|
|
beaafccd2a | ||
|
|
cad2e0078a | ||
|
|
3dc35d0f16 | ||
|
|
2d73c62936 | ||
|
|
dad5c3724e | ||
|
|
d0f3373515 | ||
|
|
bbe578ce0a | ||
|
|
51a8a3a302 | ||
|
|
e3d3cae647 | ||
|
|
bfed154feb | ||
|
|
bbe9281cb0 | ||
|
|
b2a6ababd2 | ||
|
|
343ad33a78 | ||
|
|
0a51e27d57 | ||
|
|
dc1dc5de9b | ||
|
|
868aa64e51 | ||
|
|
0a9c0dcb20 | ||
|
|
c1855e84a9 | ||
|
|
4aa4b17c9f | ||
|
|
689d59df2e | ||
|
|
6fd55a6c2d | ||
|
|
ae141c2415 | ||
|
|
ab88abfb5e | ||
|
|
90ab1de10f | ||
|
|
e3bf337a07 | ||
|
|
69329a87db | ||
|
|
663eacb962 | ||
|
|
38ce78fced | ||
|
|
ddf904fa0f | ||
|
|
eba3138d16 | ||
|
|
05227281e7 | ||
|
|
16672722d4 | ||
|
|
7fcc80025c | ||
|
|
da264fcc71 | ||
|
|
2b6bf419a1 | ||
|
|
310f83e9c2 | ||
|
|
45cdae005a | ||
|
|
15b4ce9657 | ||
|
|
665dd2f242 | ||
|
|
ecde690c3a | ||
|
|
90728dfc18 | ||
|
|
d519318804 | ||
|
|
0d8a5ce5a1 | ||
|
|
3dc29b1bae | ||
|
|
0819b69d4a | ||
|
|
8ac3d4e90b | ||
|
|
a577950cc0 | ||
|
|
3cf6edd051 | ||
|
|
5a19b90b13 | ||
|
|
daa3980122 | ||
|
|
bab460c521 | ||
|
|
64b3f55d7a | ||
|
|
ca053b57c5 | ||
|
|
a07f6b032a | ||
|
|
ad0fbb577f | ||
|
|
df55323a04 | ||
|
|
a081237b2e | ||
|
|
b604501eba | ||
|
|
b4368862e0 | ||
|
|
81043740dc | ||
|
|
b6beb39192 | ||
|
|
687f66908d | ||
|
|
a5ef17e06c | ||
|
|
cb8a43da52 | ||
|
|
a575a69704 | ||
|
|
7d6b1d7bcf | ||
|
|
31b875bbdd | ||
|
|
33b0ca8824 | ||
|
|
e7d388f55f | ||
|
|
559ef609e0 | ||
|
|
c382cb0e7b | ||
|
|
3912f937bd | ||
|
|
7222cd6436 | ||
|
|
540c58a564 | ||
|
|
6f7c1f72f6 | ||
|
|
5bf4ab368d | ||
|
|
42645a9bc1 | ||
|
|
5be0f5c71e | ||
|
|
f029675856 | ||
|
|
3a1f5bec0f | ||
|
|
1356e7b9f8 | ||
|
|
ab10b6c036 | ||
|
|
ed0e5112a3 | ||
|
|
46da1bb8a0 | ||
|
|
cd216007af | ||
|
|
bd96cf9e73 | ||
|
|
67c0be5699 | ||
|
|
695dc2eb42 | ||
|
|
516bd037e7 | ||
|
|
f122d3d3bb | ||
|
|
e7b64bcc6f | ||
|
|
3df7349d2b | ||
|
|
47b6471717 | ||
|
|
95280e8f7b | ||
|
|
296f73a407 | ||
|
|
918faa340b | ||
|
|
4526224ae4 | ||
|
|
e35b8e799f | ||
|
|
06fffdf1cf | ||
|
|
ac133fb257 | ||
|
|
2ab3274768 | ||
|
|
b7f22352ed | ||
|
|
7873340616 | ||
|
|
546a0440cd | ||
|
|
9783271a1d | ||
|
|
35ad3177d3 | ||
|
|
06b6be0c9e | ||
|
|
a390d71ef0 | ||
|
|
39dafddb4b | ||
|
|
2706f8681f | ||
|
|
44babbc240 | ||
|
|
2fe1403cdb | ||
|
|
665bfc3649 | ||
|
|
dd3746d435 | ||
|
|
b881c11672 | ||
|
|
2c0370f546 | ||
|
|
6e1b3c64ec | ||
|
|
17c3fe5699 | ||
|
|
8d20ba1ffd | ||
|
|
0d124269f0 | ||
|
|
1fcf326ecc | ||
|
|
76397085a3 | ||
|
|
1b4bdfaa12 | ||
|
|
040e6ddeb7 | ||
|
|
d148b53d52 | ||
|
|
90f125567c | ||
|
|
8cc72d1c7b | ||
|
|
747c1bbf12 | ||
|
|
22b5f89991 | ||
|
|
d19de50f7f | ||
|
|
d1dd39bf5a | ||
|
|
d07f5b8e18 | ||
|
|
9fa9440d7b | ||
|
|
042b3ae66e | ||
|
|
c07004faf4 | ||
|
|
b3905eedf4 | ||
|
|
9cffccbe73 | ||
|
|
43322186b5 | ||
|
|
a0ab01a77f | ||
|
|
d7b9b65990 | ||
|
|
e23a22d259 | ||
|
|
5d946a44f6 | ||
|
|
437f12245e | ||
|
|
b1cefdfbbe | ||
|
|
857ee7fb98 | ||
|
|
3b68d3dd13 | ||
|
|
4125b4f9a0 | ||
|
|
913cad075a | ||
|
|
8a542adb7e | ||
|
|
481f352f5a | ||
|
|
8ce2c7a40b | ||
|
|
8dda7cf2ea | ||
|
|
01eacf875d | ||
|
|
6da9ac6af1 | ||
|
|
418ad2776d | ||
|
|
2a2c07af87 | ||
|
|
ca898cf240 | ||
|
|
05b6f83ba1 | ||
|
|
68ccaf8818 | ||
|
|
162e0ab546 | ||
|
|
4c9df4da1b | ||
|
|
d1c2e82abc | ||
|
|
c054cde199 | ||
|
|
f113905edd | ||
|
|
b4e2b91c31 | ||
|
|
3568345343 | ||
|
|
4f1223e9a0 | ||
|
|
c6a172a6d0 | ||
|
|
1da70019fb | ||
|
|
a84962d96d | ||
|
|
98253fb0a8 | ||
|
|
7887e4c7e3 | ||
|
|
abe383d532 | ||
|
|
a8078c3fc6 | ||
|
|
9fd07d2fab | ||
|
|
f42fbd0098 | ||
|
|
d431699dbe | ||
|
|
9b883d3e59 | ||
|
|
955e53304f | ||
|
|
ed7085e952 | ||
|
|
e274907260 | ||
|
|
96520162d3 | ||
|
|
bb091a0301 | ||
|
|
7253082497 | ||
|
|
0e446c6f08 | ||
|
|
7af208c2a0 | ||
|
|
60e0c03a1e | ||
|
|
89f75bfb37 | ||
|
|
b83a66cf49 | ||
|
|
3cd6deb732 | ||
|
|
59fff4cc80 | ||
|
|
35e7b5c5ee | ||
|
|
e7415a2317 | ||
|
|
69c5af4d97 | ||
|
|
dca1d34685 | ||
|
|
105922b026 | ||
|
|
1dd6ea1bac | ||
|
|
54f1fb27a1 | ||
|
|
27353b1fbf | ||
|
|
afc34a9730 | ||
|
|
5b4e3cbc66 | ||
|
|
5b763b7519 | ||
|
|
fb07cec466 | ||
|
|
04ea13d8e2 | ||
|
|
9208775f34 | ||
|
|
308c480317 | ||
|
|
85af141f9f | ||
|
|
d7a88b382d | ||
|
|
8133682288 | ||
|
|
0b035b89c0 | ||
|
|
d5930542d9 | ||
|
|
831dfc5bfd | ||
|
|
078808604c | ||
|
|
35f8c5652a | ||
|
|
14d8f88985 | ||
|
|
2ae0a118b6 | ||
|
|
1a51759b0f | ||
|
|
acc2eb1a55 | ||
|
|
e513a72504 | ||
|
|
a8fa8bfa00 | ||
|
|
e8473e5ce0 | ||
|
|
5a6583b93b | ||
|
|
977b9adeff | ||
|
|
382ba22cd7 | ||
|
|
03e16b81e5 | ||
|
|
455bb2a298 | ||
|
|
0b19bd4bb5 | ||
|
|
48b0063c69 | ||
|
|
9395fa886e | ||
|
|
a6c95ff0d5 | ||
|
|
0a115a3603 | ||
|
|
fcb3158a06 | ||
|
|
77f60734f6 | ||
|
|
1c10ddd7b8 | ||
|
|
d8dd048a89 | ||
|
|
42812f8bb2 | ||
|
|
b5a9690b63 | ||
|
|
268354f3c6 | ||
|
|
3ca68f2539 | ||
|
|
08f2c5ea72 | ||
|
|
4d1f3d214d | ||
|
|
fab13b3140 | ||
|
|
b741898ce9 | ||
|
|
45ac0f7fee | ||
|
|
406487d50c | ||
|
|
b89c28fe96 | ||
|
|
1c68c805aa | ||
|
|
cef7a919b8 | ||
|
|
3117db022f | ||
|
|
7b642e40b0 | ||
|
|
89ff3dcf15 | ||
|
|
79c8d25a80 | ||
|
|
58d926be82 | ||
|
|
3044f83690 | ||
|
|
f2d5b2685e | ||
|
|
17abe18d01 | ||
|
|
09880ff8f1 | ||
|
|
23f4686577 |
10
.github/CONTRIBUTING.md
vendored
10
.github/CONTRIBUTING.md
vendored
@@ -1,14 +1,18 @@
|
||||
## Issues
|
||||
|
||||
When reporting a possible bug, provide detail steps so that we will be able
|
||||
to reproduce the issue. Try not to use phrases like "very big bug",
|
||||
to reproduce the issue. Steps to reproduce should be clear and unambiguous. Try not to use phrases like "very big bug",
|
||||
"huge issue", "useless feature", etc. No need to use exclamation marks as well.
|
||||
|
||||
Steps to reproduce should be clear and unambiguous.
|
||||
|
||||
Note that we don't provide developer help or any kind of support on GitHub.
|
||||
For this, please use our [forum](https://forum.espocrm.com).
|
||||
|
||||
If you are very new to EspoCRM, it's probable that an issue you ran into is not a bug.
|
||||
Consider creating a topic on our [forum](https://forum.espocrm.com/forum/general) instead.
|
||||
|
||||
The issue tracker is for the benefit of the EspoCRM project. The project maintainers are going to handle issues in the project's best interest.
|
||||
The maintainers have right to close issues without explanation.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
We are open for contributions that are bug fixes and small improvements. If you would like to contribute something that is not a small fix, please reach out to maintainers before submitting your PR (by creating a GitHub issue).
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Feature requests are not desired at the moment. Need to polish the system. For high-level features, consider creating feature requests on the forum. For low-level (framework) – here on GitHub.
|
||||
about: For high-level features, create feature requests on our forum. For low-level (framework) – here on GitHub.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
2
.idea/codeStyles/Project.xml
generated
2
.idea/codeStyles/Project.xml
generated
@@ -17,8 +17,6 @@
|
||||
<codeStyleSettings language="PHP">
|
||||
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
|
||||
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
|
||||
<option name="CATCH_ON_NEW_LINE" value="true" />
|
||||
<option name="FINALLY_ON_NEW_LINE" value="true" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
|
||||
|
||||
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -3,6 +3,7 @@
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ES6ConvertLetToConst" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
|
||||
<inspection_tool class="HtmlUnknownAnchorTarget" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="JSIgnoredPromiseFromCall" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpDocMissingThrowsInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpDocSignatureIsNotCompleteInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
|
||||
60
README.md
60
README.md
@@ -2,37 +2,41 @@
|
||||
|
||||
[](#espocrm)
|
||||
|
||||
[EspoCRM is an Open Source CRM](https://www.espocrm.com) (Customer Relationship Management)
|
||||
software that allows you to see, enter and evaluate all your company relationships regardless
|
||||
of the type. People, companies or opportunities – all in an easy and intuitive interface.
|
||||
|
||||
It's a web application with a frontend designed as a single page application and a REST API
|
||||
backend written in PHP.
|
||||
|
||||
[Download](https://www.espocrm.com/download/) the latest release from our website. Release notes
|
||||
and release packages are available at [Releases](https://github.com/espocrm/espocrm/releases) on GitHub.
|
||||
[EspoCRM](https://www.espocrm.com) is a free, open-source CRM platform designed to help organizations build and maintain strong customer relationships.
|
||||
It provides a wide range of tools to store, organize, and manage leads, contacts, sales opportunities, marketing campaigns,
|
||||
support cases, and more – all business information in a simple and intuitive interface.
|
||||
|
||||

|
||||
|
||||
### Architecture
|
||||
|
||||
EspoCRM is a web application with a frontend designed as a single-page application and a REST API
|
||||
backend written in PHP.
|
||||
|
||||
### Demo
|
||||
|
||||
You can try the CRM on the online [demo](https://www.espocrm.com/demo/).
|
||||
You can try the CRM on an online [demo](https://www.espocrm.com/demo/).
|
||||
|
||||
### Requirements
|
||||
|
||||
* PHP 8.1 - 8.3;
|
||||
* MySQL 5.7 (and later), or MariaDB 10.2 (and later);
|
||||
* PostgreSQL 15 (and later) (yet experimental, officially supported soon).
|
||||
* PostgreSQL 15 (and later) (beta, official support soon).
|
||||
|
||||
For more information about server configuration see [this article](https://docs.espocrm.com/administration/server-configuration/).
|
||||
For more information about server configuration, see [this article](https://docs.espocrm.com/administration/server-configuration/).
|
||||
|
||||
### Documentation
|
||||
### Why EspoCRM?
|
||||
|
||||
See the [documentation](https://docs.espocrm.com) for administrators, users and developers.
|
||||
* **Open-source transparency**. EspoCRM’s source code is open and accessible, so anyone can inspect it and see how data is being managed within the CRM.
|
||||
* **Customization freedom**. You can develop features, create custom entities, fields, relationships, buttons to make the CRM fit your specific needs.
|
||||
* **Clean user interface**. EspoCRM has a uncluttered, minimalist and very fast user inteface that is easy to navigate and has a short learning curve.
|
||||
* **Straightforward REST API**. It can be easily integrated with other applications using a REST API.
|
||||
|
||||
### Bug reporting
|
||||
### Who is EspoCRM for?
|
||||
|
||||
Create a [GitHub issue](https://github.com/espocrm/espocrm/issues/new/choose) or post on our [forum](https://forum.espocrm.com/forum/bug-reports).
|
||||
* **Startups, small & medium-sized businesses**. It’s an affordable solution that is flexible and fully customizable.
|
||||
* **Developers & tech enthusiasts**. You can extend functionalities, build extensions, and create custom integrations.
|
||||
* **Anyone seeking a free CRM**. If you're looking for a user-friendly and secure CRM platform, it can be a good option.
|
||||
|
||||
### Installing stable version
|
||||
|
||||
@@ -43,11 +47,29 @@ See installation instructions:
|
||||
* [Installation with Docker](https://docs.espocrm.com/administration/docker/installation/)
|
||||
* [Installation with Traefik](https://docs.espocrm.com/administration/docker/traefik/)
|
||||
|
||||
### Download
|
||||
|
||||
[Download](https://www.espocrm.com/download/) the latest release from our website. You can also download the latest and previous release packages from GitHub [releases](https://github.com/espocrm/espocrm/releases).
|
||||
|
||||
### Release notes
|
||||
|
||||
Release notes are available at GitHub [releases](https://github.com/espocrm/espocrm/releases).
|
||||
|
||||
### Documentation
|
||||
|
||||
See the [documentation](https://docs.espocrm.com) for administrators, users and developers.
|
||||
|
||||
### Bug reporting
|
||||
|
||||
Create a [GitHub issue](https://github.com/espocrm/espocrm/issues/new/choose) or post on our [forum](https://forum.espocrm.com/forum/bug-reports).
|
||||
|
||||
### Development
|
||||
|
||||
See the [developer documentation](https://docs.espocrm.com/development/).
|
||||
|
||||
We highly recommend using IDE for development. The backend codebase follows SOLID principles, utilizes interfaces, static typing and generics. We recommend to start learning EspoCRM from the Dependency Injection article in the documentation.
|
||||
We highly recommend using an IDE for development. The backend codebase follows SOLID principles, utilizes interfaces, static typing and generics. We recommend to start learning EspoCRM from the Dependency Injection article in the documentation.
|
||||
|
||||
Metadata plays an integral role in the EspoCRM application. All possible parameters are described with a JSON Schema, meaning you will have autocompletion in the IDE. You can also find the full metadata reference in the documentation.
|
||||
|
||||
### Community & Support
|
||||
|
||||
@@ -55,11 +77,11 @@ If you have a question regarding some features, need help or customizations, wan
|
||||
|
||||
### License
|
||||
|
||||
EspoCRM is published under the GNU AGPLv3 [license](https://raw.githubusercontent.com/espocrm/espocrm/master/LICENSE.txt).
|
||||
EspoCRM is an open-source project licensed under [GNU AGPLv3](https://raw.githubusercontent.com/espocrm/espocrm/master/LICENSE.txt).
|
||||
|
||||
### Contributing
|
||||
|
||||
Before we can merge your pull request, you need to accept our CLA [here](https://github.com/espocrm/cla). See [contributing guidelines](https://github.com/espocrm/espocrm/blob/master/.github/CONTRIBUTING.md).
|
||||
Before we can merge your pull request, you need to accept our CLA [here](https://github.com/espocrm/cla). See the [contributing guidelines](https://github.com/espocrm/espocrm/blob/master/.github/CONTRIBUTING.md).
|
||||
|
||||
Branches:
|
||||
|
||||
|
||||
47
application/Espo/Classes/Acl/ImportEml/AccessChecker.php
Normal file
47
application/Espo/Classes/Acl/ImportEml/AccessChecker.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Acl\ImportEml;
|
||||
|
||||
use Espo\Core\Acl\AccessCreateChecker;
|
||||
use Espo\Core\Acl\ScopeData;
|
||||
use Espo\Entities\User;
|
||||
|
||||
class AccessChecker implements AccessCreateChecker
|
||||
{
|
||||
public function check(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $data->isTrue();
|
||||
}
|
||||
|
||||
public function checkCreate(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $data->isTrue();
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\Acl\Note;
|
||||
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\User;
|
||||
@@ -143,7 +144,7 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
}
|
||||
|
||||
if ($entity->getTargetType() === Note::TARGET_PORTALS) {
|
||||
return $this->aclManager->getPermissionLevel($user, 'portal') === Table::LEVEL_YES;
|
||||
return $this->aclManager->getPermissionLevel($user, Permission::PORTAL) === Table::LEVEL_YES;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\Acl\Portal;
|
||||
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Entities\Portal;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\Acl\AccessEntityCREDChecker;
|
||||
@@ -45,18 +46,12 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
{
|
||||
use DefaultAccessCheckerDependency;
|
||||
|
||||
private DefaultAccessChecker $defaultAccessChecker;
|
||||
private AclManager $aclManager;
|
||||
|
||||
public function __construct(DefaultAccessChecker $defaultAccessChecker, AclManager $aclManager)
|
||||
{
|
||||
$this->defaultAccessChecker = $defaultAccessChecker;
|
||||
$this->aclManager = $aclManager;
|
||||
}
|
||||
public function __construct(private DefaultAccessChecker $defaultAccessChecker, private AclManager $aclManager)
|
||||
{}
|
||||
|
||||
public function check(User $user, ScopeData $data): bool
|
||||
{
|
||||
$level = $this->aclManager->getPermissionLevel($user, 'portal');
|
||||
$level = $this->aclManager->getPermissionLevel($user, Permission::PORTAL);
|
||||
|
||||
return $level === Table::LEVEL_YES;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\Acl\User;
|
||||
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Acl\AccessEntityCREDSChecker;
|
||||
@@ -60,8 +61,6 @@ class AccessChecker implements AccessEntityCREDSChecker
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var User $entity */
|
||||
|
||||
if ($entity->isSuperAdmin() && !$user->isSuperAdmin()) {
|
||||
return false;
|
||||
}
|
||||
@@ -71,10 +70,8 @@ class AccessChecker implements AccessEntityCREDSChecker
|
||||
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
/** @var User $entity */
|
||||
|
||||
if ($entity->isPortal()) {
|
||||
if ($this->aclManager->getPermissionLevel($user, 'portal') === Table::LEVEL_YES) {
|
||||
if ($this->aclManager->getPermissionLevel($user, Permission::PORTAL) === Table::LEVEL_YES) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -90,8 +87,6 @@ class AccessChecker implements AccessEntityCREDSChecker
|
||||
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
/** @var User $entity */
|
||||
|
||||
if ($entity->isSystem()) {
|
||||
return false;
|
||||
}
|
||||
@@ -111,8 +106,6 @@ class AccessChecker implements AccessEntityCREDSChecker
|
||||
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
/** @var User $entity */
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
@@ -130,8 +123,7 @@ class AccessChecker implements AccessEntityCREDSChecker
|
||||
|
||||
public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
/** @var User $entity */
|
||||
|
||||
return $this->aclManager->checkUserPermission($user, $entity, 'user');
|
||||
/** @noinspection PhpRedundantOptionalArgumentInspection */
|
||||
return $this->aclManager->checkUserPermission($user, $entity, Permission::USER);
|
||||
}
|
||||
}
|
||||
|
||||
48
application/Espo/Classes/AppParams/AddressCountryData.php
Normal file
48
application/Espo/Classes/AppParams/AddressCountryData.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\AppParams;
|
||||
|
||||
use Espo\Core\Utils\Address\CountryDataProvider;
|
||||
use Espo\Tools\App\AppParam;
|
||||
|
||||
class AddressCountryData implements AppParam
|
||||
{
|
||||
public function __construct(
|
||||
private CountryDataProvider $provider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{list: string[], preferredList: string[]}
|
||||
*/
|
||||
public function get(): array
|
||||
{
|
||||
return $this->provider->get();
|
||||
}
|
||||
}
|
||||
65
application/Espo/Classes/Cleanup/AppLog.php
Normal file
65
application/Espo/Classes/Cleanup/AppLog.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Cleanup;
|
||||
|
||||
use Espo\Core\Cleanup\Cleanup;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\AppLogRecord;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\DeleteBuilder;
|
||||
|
||||
class AppLog implements Cleanup
|
||||
{
|
||||
private const PERIOD = '30 days';
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$query = DeleteBuilder::create()
|
||||
->from(AppLogRecord::ENTITY_TYPE)
|
||||
->where(['createdAt<' => $this->getBefore()->toString()])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($query);
|
||||
}
|
||||
|
||||
private function getBefore(): DateTime
|
||||
{
|
||||
/** @var string $period */
|
||||
$period = $this->config->get('cleanupAppLogPeriod') ?? self::PERIOD;
|
||||
|
||||
return DateTime::createNow()->modify('-' . $period);
|
||||
}
|
||||
}
|
||||
137
application/Espo/Classes/Cleanup/Stars.php
Normal file
137
application/Espo/Classes/Cleanup/Stars.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Cleanup;
|
||||
|
||||
use Espo\Core\Cleanup\Cleanup;
|
||||
use Espo\Core\Utils\Acl\UserAclManagerProvider;
|
||||
use Espo\Entities\StarSubscription;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\DeleteBuilder;
|
||||
use Espo\Tools\Stars\StarService;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class Stars implements Cleanup
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private UserAclManagerProvider $userAclManagerProvider,
|
||||
private StarService $service
|
||||
) {}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
foreach ($this->getEntityTypeList() as $entityType) {
|
||||
$this->processEntityType($entityType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getEntityTypeList(): array
|
||||
{
|
||||
$groups = $this->entityManager->getRDBRepositoryByClass(StarSubscription::class)
|
||||
->group('entityType')
|
||||
->select('entityType')
|
||||
->find();
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$list[] = $group->get('entityType');
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function processEntityType(string $entityType): void
|
||||
{
|
||||
if (
|
||||
!$this->service->isEnabled($entityType) ||
|
||||
!$this->entityManager->hasRepository($entityType)
|
||||
) {
|
||||
$deleteQuery = DeleteBuilder::create()
|
||||
->from(StarSubscription::ENTITY_TYPE)
|
||||
->where(['entityType' => $entityType])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($deleteQuery);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$stars = $this->entityManager
|
||||
->getRDBRepositoryByClass(StarSubscription::class)
|
||||
->where(['entityType' => $entityType])
|
||||
->sth()
|
||||
->find();
|
||||
|
||||
foreach ($stars as $star) {
|
||||
$entityId = $star->get('entityId');
|
||||
$userId = $star->get('userId');
|
||||
|
||||
if ($userId === null || $entityId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityType, $entityId);
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
|
||||
|
||||
if (!$entity || !$user) {
|
||||
$this->unstar($userId, $entityType, $entityId);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$aclManager = $this->userAclManagerProvider->get($user);
|
||||
|
||||
if (!$aclManager->checkEntityRead($user, $entity)) {
|
||||
$this->unstar($userId, $entityType, $entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function unstar(string $userId, string $entityType, string $entityId): void
|
||||
{
|
||||
$deleteQuery = DeleteBuilder::create()
|
||||
->from(StarSubscription::ENTITY_TYPE)
|
||||
->where([
|
||||
'userId' => $userId,
|
||||
'entityType' => $entityType,
|
||||
'entityId' => $entityId,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($deleteQuery);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ use Espo\Core\Cleanup\Cleanup;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\Subscription;
|
||||
use Espo\Entities\StreamSubscription;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
|
||||
@@ -41,19 +41,11 @@ class Subscribers implements Cleanup
|
||||
{
|
||||
private const PERIOD = '2 months';
|
||||
|
||||
private Metadata $metadata;
|
||||
private EntityManager $entityManager;
|
||||
private Config $config;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
EntityManager $entityManager,
|
||||
Config $config
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->config = $config;
|
||||
}
|
||||
private Metadata $metadata,
|
||||
private EntityManager $entityManager,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
@@ -105,7 +97,7 @@ class Subscribers implements Cleanup
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from(Subscription::ENTITY_TYPE, 'subscription')
|
||||
->from(StreamSubscription::ENTITY_TYPE, 'subscription')
|
||||
->join(
|
||||
$entityType,
|
||||
'entity',
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldProcessing\Email;
|
||||
|
||||
use Espo\Core\FieldProcessing\Loader;
|
||||
use Espo\Core\FieldProcessing\Loader\Params;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\EmailFolder;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @implements Loader<Email>
|
||||
*/
|
||||
class FolderDataLoader implements Loader
|
||||
{
|
||||
public function __construct(private EntityManager $entityManager) {}
|
||||
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
$folderId = $entity->get(Email::USERS_COLUMN_FOLDER_ID);
|
||||
|
||||
if (!$folderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$folder = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailFolder::class)
|
||||
->select(['id', 'name'])
|
||||
->where(['id' => $folderId])
|
||||
->findOne();
|
||||
|
||||
if (!$folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->set('folderName', $folder->getName());
|
||||
}
|
||||
}
|
||||
@@ -41,14 +41,10 @@ use Espo\Entities\User;
|
||||
*/
|
||||
class UserColumnsLoader implements Loader
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private User $user;
|
||||
|
||||
public function __construct(EntityManager $entityManager, User $user)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->user = $user;
|
||||
}
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
@@ -58,6 +54,7 @@ class UserColumnsLoader implements Loader
|
||||
Email::USERS_COLUMN_IS_READ,
|
||||
Email::USERS_COLUMN_IS_IMPORTANT,
|
||||
Email::USERS_COLUMN_IN_TRASH,
|
||||
Email::USERS_COLUMN_IN_ARCHIVE,
|
||||
])
|
||||
->where([
|
||||
'deleted' => false,
|
||||
@@ -70,6 +67,7 @@ class UserColumnsLoader implements Loader
|
||||
$entity->set(Email::USERS_COLUMN_IS_READ, null);
|
||||
$entity->clear(Email::USERS_COLUMN_IS_IMPORTANT);
|
||||
$entity->clear(Email::USERS_COLUMN_IN_TRASH);
|
||||
$entity->clear(Email::USERS_COLUMN_IN_ARCHIVE);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -78,6 +76,8 @@ class UserColumnsLoader implements Loader
|
||||
Email::USERS_COLUMN_IS_READ => $emailUser->get(Email::USERS_COLUMN_IS_READ),
|
||||
Email::USERS_COLUMN_IS_IMPORTANT => $emailUser->get(Email::USERS_COLUMN_IS_IMPORTANT),
|
||||
Email::USERS_COLUMN_IN_TRASH => $emailUser->get(Email::USERS_COLUMN_IN_TRASH),
|
||||
Email::USERS_COLUMN_IN_ARCHIVE => $emailUser->get(Email::USERS_COLUMN_IN_ARCHIVE),
|
||||
'isUsersSent' => $entity->getSentBy()?->getId() === $this->user->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**LICENSE**/
|
||||
|
||||
namespace Espo\Classes\FieldProcessing\InboundEmail;
|
||||
|
||||
use Espo\Core\FieldProcessing\Loader;
|
||||
use Espo\Core\FieldProcessing\Loader\Params;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @implements Loader<InboundEmail>
|
||||
*/
|
||||
class IsSystemLoader implements Loader
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
$isSystem = $entity->getEmailAddress() === $this->config->get('outboundEmailFromAddress');
|
||||
|
||||
$entity->set('isSystem', $isSystem);
|
||||
}
|
||||
}
|
||||
56
application/Espo/Classes/FieldSanitizers/StringLowerCase.php
Normal file
56
application/Espo/Classes/FieldSanitizers/StringLowerCase.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldSanitizers;
|
||||
|
||||
use Espo\Core\FieldSanitize\Sanitizer;
|
||||
use Espo\Core\FieldSanitize\Sanitizer\Data;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class StringLowerCase implements Sanitizer
|
||||
{
|
||||
public function sanitize(Data $data, string $field): void
|
||||
{
|
||||
if (!$data->has($field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$value = $data->get($field);
|
||||
|
||||
if (!is_string($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$value = mb_strtolower($value);
|
||||
|
||||
$data->set($field, $value);
|
||||
}
|
||||
}
|
||||
56
application/Espo/Classes/FieldSanitizers/StringUpperCase.php
Normal file
56
application/Espo/Classes/FieldSanitizers/StringUpperCase.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldSanitizers;
|
||||
|
||||
use Espo\Core\FieldSanitize\Sanitizer;
|
||||
use Espo\Core\FieldSanitize\Sanitizer\Data;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class StringUpperCase implements Sanitizer
|
||||
{
|
||||
public function sanitize(Data $data, string $field): void
|
||||
{
|
||||
if (!$data->has($field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$value = $data->get($field);
|
||||
|
||||
if (!is_string($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$value = mb_strtoupper($value);
|
||||
|
||||
$data->set($field, $value);
|
||||
}
|
||||
}
|
||||
@@ -29,16 +29,54 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
use stdClass;
|
||||
|
||||
class IntType
|
||||
{
|
||||
public function __construct(
|
||||
private Defs $defs,
|
||||
) {}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->isNotEmpty($entity, $field);
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
public function checkRangeInternal(Entity $entity, string $field): bool
|
||||
{
|
||||
$value = $entity->get($field);
|
||||
|
||||
if ($value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$dbType = $this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->tryGetAttribute($field)
|
||||
?->getParam('dbType') ?? Types::INTEGER;
|
||||
|
||||
$ranges = [
|
||||
Types::INTEGER => [-2147483648, 2147483647],
|
||||
Types::SMALLINT => [-32768, 32767],
|
||||
];
|
||||
|
||||
$range = $ranges[$dbType] ?? null;
|
||||
|
||||
if (!$range) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($value < $range[0] || $value > $range[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $validationValue
|
||||
* @noinspection PhpUnused
|
||||
|
||||
@@ -36,20 +36,17 @@ use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class LinkMultipleType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
private Defs $defs;
|
||||
|
||||
private const COLUMN_TYPE_ENUM = 'enum';
|
||||
private const COLUMN_TYPE_VARCHAR = 'varchar';
|
||||
private const COLUMN_TYPE_BOOL = 'bool';
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
public function __construct(private Metadata $metadata, private Defs $defs)
|
||||
{}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
@@ -63,6 +60,7 @@ class LinkMultipleType
|
||||
return count($idList) > 0;
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
public function checkPattern(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?mixed[] $idList */
|
||||
@@ -93,6 +91,27 @@ class LinkMultipleType
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
public function checkMaxCount(Entity $entity, string $field, ?int $maxCount): bool
|
||||
{
|
||||
if ($maxCount === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$list = $entity->get($field . 'Ids');
|
||||
|
||||
if (!is_array($list)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (count($list) > $maxCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
public function checkColumnsValid(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Brick\PhoneNumber\PhoneNumber;
|
||||
use Brick\PhoneNumber\PhoneNumberParseException;
|
||||
use Espo\Core\PhoneNumber\Util;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
@@ -203,6 +204,22 @@ class PhoneType
|
||||
return true;
|
||||
}
|
||||
|
||||
$ext = null;
|
||||
|
||||
if ($this->config->get('phoneNumberExtensions')) {
|
||||
[$number, $ext] = Util::splitExtension($number);
|
||||
}
|
||||
|
||||
if ($ext) {
|
||||
if (!preg_match('/[0-9]+/', $ext)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($ext) > 6) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$numberObj = PhoneNumber::parse($number);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators\Settings\AuthIpAddressWhitelist;
|
||||
|
||||
use Espo\Core\FieldValidation\Validator;
|
||||
use Espo\Core\FieldValidation\Validator\Data;
|
||||
use Espo\Core\FieldValidation\Validator\Failure;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @implements Validator<Entity>
|
||||
*/
|
||||
class Valid implements Validator
|
||||
{
|
||||
public function validate(Entity $entity, string $field, Data $data): ?Failure
|
||||
{
|
||||
$list = $entity->get($field);
|
||||
|
||||
if (!is_array($list)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($list as $item) {
|
||||
if (!is_string($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->isValid($item)) {
|
||||
return Failure::create();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isValid(string $item): bool
|
||||
{
|
||||
$address = $item;
|
||||
|
||||
if (count(explode('/', $item)) > 1) {
|
||||
[$address, $mask] = explode('/', $item, 2);
|
||||
|
||||
if (!is_numeric($mask)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mask = (int) $mask;
|
||||
|
||||
if ($mask < 0 || $mask > 128) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false &&
|
||||
filter_var($address, FILTER_VALIDATE_IP) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators\User\DefaultTeam;
|
||||
|
||||
use Espo\Core\FieldValidation\Validator;
|
||||
use Espo\Core\FieldValidation\Validator\Data;
|
||||
use Espo\Core\FieldValidation\Validator\Failure;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @implements Validator<User>
|
||||
*/
|
||||
class IsUserTeam implements Validator
|
||||
{
|
||||
public function validate(Entity $entity, string $field, Data $data): ?Failure
|
||||
{
|
||||
if (!$entity->getDefaultTeam()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (in_array($entity->getDefaultTeam()->getId(), $entity->getTeamIdList())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Failure::create();
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Classes\Jobs;
|
||||
|
||||
use Espo\Entities\AuthToken;
|
||||
use Espo\Entities\Portal;
|
||||
use Espo\Core\Job\JobDataLess;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
@@ -50,27 +51,69 @@ class AuthTokenControl implements JobDataLess
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$authTokenLifetime = (int) ($this->config->get('authTokenLifetime', 0) * 60);
|
||||
$authTokenMaxIdleTime = (int) ($this->config->get('authTokenMaxIdleTime', 0) * 60);
|
||||
$lifetime = (int) $this->config->get('authTokenLifetime', 0) * 60;
|
||||
$maxIdleTime = (int) $this->config->get('authTokenMaxIdleTime', 0) * 60;
|
||||
|
||||
if (!$authTokenLifetime && !$authTokenMaxIdleTime) {
|
||||
$portalIds = [];
|
||||
|
||||
/** @var iterable<Portal> $portals */
|
||||
$portals = $this->entityManager
|
||||
->getRDBRepositoryByClass(Portal::class)
|
||||
->find();
|
||||
|
||||
foreach ($portals as $portal) {
|
||||
$portalIds[] = $portal->getId();
|
||||
}
|
||||
|
||||
$this->process(null, $lifetime, $maxIdleTime, $portalIds);
|
||||
|
||||
foreach ($portals as $portal) {
|
||||
$itemLifetime = $portal->get('authTokenLifetime') !== null ?
|
||||
(int) $portal->get('authTokenLifetime') * 60 :
|
||||
$lifetime;
|
||||
|
||||
$itemMaxIdleTime = $portal->get('authTokenMaxIdleTime') !== null ?
|
||||
(int) $portal->get('authTokenMaxIdleTime') * 60 :
|
||||
$maxIdleTime;
|
||||
|
||||
$this->process($portal->getId(), $itemLifetime, $itemMaxIdleTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $ignorePortalIds
|
||||
*/
|
||||
private function process(?string $portalId, int $lifetime, int $maxIdleTime, array $ignorePortalIds = []): void
|
||||
{
|
||||
if (!$lifetime && !$maxIdleTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
$whereClause = [
|
||||
'isActive' => true,
|
||||
];
|
||||
$whereClause = ['isActive' => true];
|
||||
|
||||
if ($authTokenLifetime) {
|
||||
if ($portalId) {
|
||||
$whereClause['portalId'] = $portalId;
|
||||
}
|
||||
|
||||
if (!$portalId && $ignorePortalIds !== []) {
|
||||
$whereClause[] = [
|
||||
'OR' => [
|
||||
['portalId' => null],
|
||||
['portalId!=' => $ignorePortalIds],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
if ($lifetime) {
|
||||
$dt = new DateTime();
|
||||
$dt->modify("-$authTokenLifetime minutes");
|
||||
$dt->modify("-$lifetime minutes");
|
||||
|
||||
$whereClause['createdAt<'] = $dt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
|
||||
}
|
||||
|
||||
if ($authTokenMaxIdleTime) {
|
||||
if ($maxIdleTime) {
|
||||
$dt = new DateTime();
|
||||
$dt->modify("-$authTokenMaxIdleTime minutes");
|
||||
$dt->modify("-$maxIdleTime minutes");
|
||||
|
||||
$whereClause['lastAccess<'] = $dt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
|
||||
}
|
||||
|
||||
@@ -29,39 +29,31 @@
|
||||
|
||||
namespace Espo\Classes\Jobs;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\Mail\Account\PersonalAccount\Service;
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class CheckEmailAccounts implements Job
|
||||
{
|
||||
private $service;
|
||||
|
||||
public function __construct(Service $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
public function __construct(private Service $service)
|
||||
{}
|
||||
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$targetId = $data->getTargetId();
|
||||
|
||||
if (!$targetId) {
|
||||
throw new Error("No target.");
|
||||
throw new RuntimeException("No target.");
|
||||
}
|
||||
|
||||
try {
|
||||
$this->service->fetch($targetId);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
throw new Error(
|
||||
'Job CheckEmailAccounts ' . $targetId . ': [' . $e->getCode() . '] ' . $e->getMessage() . ' ' .
|
||||
$e->getFile() . ':' . $e->getLine()
|
||||
);
|
||||
throw new RuntimeException("CheckInboundEmails job failed, $targetId; {$e->getMessage()}", 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,37 +29,31 @@
|
||||
|
||||
namespace Espo\Classes\Jobs;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Mail\Account\GroupAccount\Service;
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class CheckInboundEmails implements Job
|
||||
{
|
||||
private $service;
|
||||
|
||||
public function __construct(Service $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
public function __construct(private Service $service)
|
||||
{}
|
||||
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$targetId = $data->getTargetId();
|
||||
|
||||
if (!$targetId) {
|
||||
throw new Error("No target.");
|
||||
throw new RuntimeException("No target.");
|
||||
}
|
||||
|
||||
try {
|
||||
$this->service->fetch($targetId);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
throw new Error(
|
||||
'Job CheckInboundEmails ' . $targetId . ': [' . $e->getCode() . '] ' .$e->getMessage()
|
||||
);
|
||||
throw new RuntimeException("CheckInboundEmails job failed, $targetId; {$e->getMessage()}", 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Classes\MassAction\Email;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\MassAction\Data;
|
||||
use Espo\Core\MassAction\MassAction;
|
||||
@@ -44,11 +45,10 @@ use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Email\Folder;
|
||||
use Espo\Tools\Email\InboxService as EmailService;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
class MoveToFolder implements MassAction
|
||||
{
|
||||
private const FOLDER_INBOX = Folder::INBOX;
|
||||
|
||||
public function __construct(
|
||||
private QueryBuilder $queryBuilder,
|
||||
private EntityManager $entityManager,
|
||||
@@ -68,7 +68,11 @@ class MoveToFolder implements MassAction
|
||||
throw new BadRequest("No folder ID.");
|
||||
}
|
||||
|
||||
if ($folderId !== self::FOLDER_INBOX && !str_starts_with($folderId, 'group:')) {
|
||||
if (
|
||||
$folderId !== Folder::INBOX &&
|
||||
$folderId !== Folder::ARCHIVE &&
|
||||
!str_starts_with($folderId, 'group:')
|
||||
) {
|
||||
$folder = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailFolder::class)
|
||||
->where([
|
||||
@@ -93,7 +97,12 @@ class MoveToFolder implements MassAction
|
||||
}
|
||||
}
|
||||
|
||||
$query = $this->queryBuilder->build($params);
|
||||
try {
|
||||
$query = $this->queryBuilder->build($params);
|
||||
}
|
||||
catch (BadRequest|Forbidden $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepositoryByClass(Email::class)
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Classes\MassAction\User;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\MassAction\Actions\MassDelete as MassDeleteOriginal;
|
||||
use Espo\Core\MassAction\Data;
|
||||
@@ -59,18 +60,19 @@ class MassDelete implements MassAction
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function process(Params $params, Data $data): Result
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
|
||||
if (!$this->acl->check($entityType, Acl\Table::ACTION_DELETE)) {
|
||||
throw new Forbidden("No delete access for '{$entityType}'.");
|
||||
throw new Forbidden("No delete access for '$entityType'.");
|
||||
}
|
||||
|
||||
if (
|
||||
!$params->hasIds() &&
|
||||
$this->acl->getPermissionLevel('massUpdatePermission') !== Acl\Table::LEVEL_YES
|
||||
$this->acl->getPermissionLevel(Acl\Permission::MASS_UPDATE) !== Acl\Table::LEVEL_YES
|
||||
) {
|
||||
throw new Forbidden("No mass-update permission.");
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ use Espo\Tools\MassUpdate\Data as MassUpdateData;
|
||||
|
||||
class MassUpdate implements MassAction
|
||||
{
|
||||
private const PERMISSION = 'massUpdatePermission';
|
||||
private const PERMISSION = Acl\Permission::MASS_UPDATE;
|
||||
|
||||
/** @var string[] */
|
||||
private array $notAllowedAttributeList = [
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\RecordHooks\AddressCountry;
|
||||
|
||||
use Espo\Core\Exceptions\ConflictSilent;
|
||||
use Espo\Core\Exceptions\Error\Body;
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Entities\AddressCountry;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @implements SaveHook<AddressCountry>
|
||||
*/
|
||||
class BeforeSave implements SaveHook
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
$where = ['name' => $entity->getName()];
|
||||
|
||||
if (!$entity->isNew()) {
|
||||
$where['id!='] = $entity->getId();
|
||||
}
|
||||
|
||||
$one = $this->entityManager
|
||||
->getRDBRepositoryByClass(AddressCountry::class)
|
||||
->where($where)
|
||||
->findOne();
|
||||
|
||||
if (!$one) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ConflictSilent::createWithBody(
|
||||
'duplicateError',
|
||||
Body::create()->withMessageTranslation('duplicateConflict')
|
||||
);
|
||||
}
|
||||
}
|
||||
101
application/Espo/Classes/RecordHooks/Email/CheckFromAddress.php
Normal file
101
application/Espo/Classes/RecordHooks/Email/CheckFromAddress.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\RecordHooks\Email;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Mail\Account\SendingAccountProvider;
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @implements SaveHook<Email>
|
||||
*/
|
||||
class CheckFromAddress implements SaveHook
|
||||
{
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private SendingAccountProvider $sendingAccountProvider,
|
||||
private Config $config,
|
||||
private Acl $acl,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
if ($this->user->isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fromAddress = $entity->getFromAddress();
|
||||
|
||||
// Should be after 'getFromAddress'.
|
||||
if (!$entity->isAttributeChanged('from')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$fromAddress) {
|
||||
throw new BadRequest("No 'from' address");
|
||||
}
|
||||
|
||||
if ($this->acl->checkScope('Import')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fromAddress = strtolower($fromAddress);
|
||||
|
||||
foreach ($this->user->getEmailAddressGroup()->getAddressList() as $address) {
|
||||
if ($fromAddress === strtolower($address)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->sendingAccountProvider->getShared($this->user, $fromAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$system = $this->sendingAccountProvider->getSystem();
|
||||
|
||||
if (
|
||||
$system &&
|
||||
$this->config->get('outboundEmailIsShared') &&
|
||||
$system->getEmailAddress()
|
||||
) {
|
||||
if ($fromAddress === strtolower($system->getEmailAddress())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Forbidden("Not allowed 'from' address.");
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Classes\RecordHooks\Note;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Core\Acl\Table as AclTable;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
@@ -90,7 +91,7 @@ class AssignmentCheck implements SaveHook
|
||||
}
|
||||
}
|
||||
|
||||
$messagePermission = $this->acl->getPermissionLevel('message');
|
||||
$messagePermission = $this->acl->getPermissionLevel(Permission::MESSAGE);
|
||||
|
||||
if ($messagePermission === AclTable::LEVEL_NO) {
|
||||
if (
|
||||
@@ -126,14 +127,14 @@ class AssignmentCheck implements SaveHook
|
||||
throw new BadRequest("No portal IDs.");
|
||||
}
|
||||
|
||||
if ($this->acl->getPermissionLevel('portal') !== AclTable::LEVEL_YES) {
|
||||
if ($this->acl->getPermissionLevel(Permission::PORTAL) !== AclTable::LEVEL_YES) {
|
||||
throw new Forbidden('Not permitted to post to portal users.');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$targetType === Note::TARGET_USERS &&
|
||||
$this->acl->getPermissionLevel('portal') !== AclTable::LEVEL_YES
|
||||
$this->acl->getPermissionLevel(Permission::PORTAL) !== AclTable::LEVEL_YES
|
||||
) {
|
||||
if ($hasPortalTargetUser) {
|
||||
throw new Forbidden('Not permitted to post to portal users.');
|
||||
|
||||
@@ -70,6 +70,7 @@ class BeforeCreate implements SaveHook
|
||||
|
||||
$targetType = $entity->getTargetType();
|
||||
|
||||
$entity->clear('isPinned');
|
||||
$entity->clear('isGlobal');
|
||||
|
||||
switch ($targetType) {
|
||||
@@ -87,7 +88,7 @@ class BeforeCreate implements SaveHook
|
||||
$entity->clear('usersIds');
|
||||
$entity->clear('teamsIds');
|
||||
$entity->clear('portalsIds');
|
||||
$entity->set('usersIds', [$this->user->getId()]);
|
||||
$entity->setUsersIds([$this->user->getId()]);
|
||||
$entity->set('isForSelf', true);
|
||||
|
||||
break;
|
||||
|
||||
@@ -32,7 +32,6 @@ namespace Espo\Classes\RecordHooks\Note;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\Stream\NoteUtil;
|
||||
|
||||
@@ -43,18 +42,27 @@ use Espo\Tools\Stream\NoteUtil;
|
||||
class BeforeUpdate implements SaveHook
|
||||
{
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private NoteUtil $noteUtil
|
||||
private NoteUtil $noteUtil,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
if (!$this->isEditableType($entity)) {
|
||||
throw new ForbiddenSilent("Note is not editable.");
|
||||
}
|
||||
|
||||
if ($entity->isPost()) {
|
||||
$this->noteUtil->handlePostText($entity);
|
||||
}
|
||||
|
||||
if (!$entity->isPost() && !$this->user->isAdmin()) {
|
||||
throw new ForbiddenSilent("Only 'Post' type allowed.");
|
||||
if (!$entity->isPost()) {
|
||||
$entity->clear('post');
|
||||
$entity->clear('attachmentsIds');
|
||||
}
|
||||
}
|
||||
|
||||
private function isEditableType(Note $entity): bool
|
||||
{
|
||||
return $entity->getType() == Note::TYPE_POST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\AddressCountry;
|
||||
|
||||
use Espo\Core\Select\Order\Item;
|
||||
use Espo\Core\Select\Order\Orderer;
|
||||
use Espo\ORM\Query\Part\Order;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
class PreferredNameOrderer implements Orderer
|
||||
{
|
||||
public function apply(SelectBuilder $queryBuilder, Item $item): void
|
||||
{
|
||||
$queryBuilder
|
||||
->order('isPreferred', $item->getOrder() === Order::ASC ? Order::DESC : Order::ASC)
|
||||
->order('name', $item->getOrder());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\AppLogRecord\PrimaryFilters;
|
||||
|
||||
use Espo\Core\Select\Primary\Filter;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
class Errors implements Filter
|
||||
{
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
{
|
||||
$queryBuilder->where([
|
||||
'level' => [
|
||||
ucfirst(LogLevel::ERROR),
|
||||
ucfirst(LogLevel::EMERGENCY),
|
||||
ucfirst(LogLevel::CRITICAL),
|
||||
ucfirst(LogLevel::ALERT),
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\AccessControlFilters;
|
||||
use Espo\Core\Select\AccessControl\Filter;
|
||||
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
@@ -46,6 +47,6 @@ class OnlyOwn implements Filter
|
||||
{
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
$queryBuilder->where(['emailUser.userId' => $this->user->getId()]);
|
||||
$queryBuilder->where([Email::ALIAS_INBOX . '.userId' => $this->user->getId()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,15 +52,15 @@ class OnlyTeam implements Filter
|
||||
'entityTeam.entityType' => Email::ENTITY_TYPE,
|
||||
'entityTeam.deleted' => false,
|
||||
])
|
||||
->leftJoin(Email::RELATIONSHIP_EMAIL_USER, 'emailUser', [
|
||||
'emailUser.emailId:' => 'id',
|
||||
'emailUser.deleted' => false,
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
->leftJoin(Email::RELATIONSHIP_EMAIL_USER, Email::ALIAS_INBOX, [
|
||||
Email::ALIAS_INBOX . '.emailId:' => 'id',
|
||||
Email::ALIAS_INBOX . '.deleted' => false,
|
||||
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
|
||||
])
|
||||
->where([
|
||||
'OR' => [
|
||||
'entityTeam.teamId' => $this->user->getTeamIdList(),
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
|
||||
]
|
||||
])
|
||||
->build();
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Classes\Select\Email\AccessControlFilters;
|
||||
|
||||
use Espo\Core\Select\AccessControl\Filter;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
@@ -46,10 +47,9 @@ class PortalOnlyAccount implements Filter
|
||||
$queryBuilder->distinct();
|
||||
|
||||
$orGroup = [
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
|
||||
];
|
||||
|
||||
/** @var string[] $accountIdList */
|
||||
$accountIdList = $this->user->getLinkMultipleIdList('accounts');
|
||||
|
||||
if (count($accountIdList)) {
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Classes\Select\Email\AccessControlFilters;
|
||||
|
||||
use Espo\Core\Select\AccessControl\Filter;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
@@ -46,7 +47,7 @@ class PortalOnlyContact implements Filter
|
||||
$queryBuilder->distinct();
|
||||
|
||||
$orGroup = [
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
|
||||
];
|
||||
|
||||
$contactId = $this->user->get('contactId');
|
||||
|
||||
@@ -76,9 +76,9 @@ class Main implements AdditionalApplier
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->checkApplyDateSentIndex($queryBuilder, $searchParams)) {
|
||||
/*if ($this->checkApplyDateSentIndex($queryBuilder, $searchParams)) {
|
||||
$queryBuilder->useIndex('dateSent');
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
private function joinEmailUser(SelectBuilder $queryBuilder): void
|
||||
@@ -93,11 +93,12 @@ class Main implements AdditionalApplier
|
||||
Email::USERS_COLUMN_IS_READ,
|
||||
Email::USERS_COLUMN_IS_IMPORTANT,
|
||||
Email::USERS_COLUMN_IN_TRASH,
|
||||
Email::USERS_COLUMN_IN_ARCHIVE,
|
||||
Email::USERS_COLUMN_FOLDER_ID,
|
||||
];
|
||||
|
||||
foreach ($itemList as $item) {
|
||||
$queryBuilder->select('emailUser.' . $item, $item);
|
||||
$queryBuilder->select(Email::ALIAS_INBOX . '.' . $item, $item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +117,7 @@ class Main implements AdditionalApplier
|
||||
return null;
|
||||
}
|
||||
|
||||
private function checkApplyDateSentIndex(SelectBuilder $queryBuilder, SearchParams $searchParams): bool
|
||||
/*private function checkApplyDateSentIndex(SelectBuilder $queryBuilder, SearchParams $searchParams): bool
|
||||
{
|
||||
if ($searchParams->getTextFilter()) {
|
||||
return false;
|
||||
@@ -149,5 +150,5 @@ class Main implements AdditionalApplier
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Classes\Select\Email\BoolFilters;
|
||||
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Core\Select\Bool\Filter;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\Where\OrGroupBuilder;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
@@ -46,7 +47,7 @@ class OnlyMy implements Filter
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
$item = WhereClause::fromRaw([
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
|
||||
]);
|
||||
|
||||
$orGroupBuilder->add($item);
|
||||
|
||||
@@ -36,14 +36,14 @@ class JoinHelper
|
||||
{
|
||||
public function joinEmailUser(QueryBuilder $queryBuilder, string $userId): void
|
||||
{
|
||||
if ($queryBuilder->hasLeftJoinAlias('emailUser')) {
|
||||
if ($queryBuilder->hasLeftJoinAlias(Email::ALIAS_INBOX)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder->leftJoin(Email::RELATIONSHIP_EMAIL_USER, 'emailUser', [
|
||||
'emailUser.emailId:' => 'id',
|
||||
'emailUser.deleted' => false,
|
||||
'emailUser.userId' => $userId,
|
||||
$queryBuilder->leftJoin(Email::RELATIONSHIP_EMAIL_USER, Email::ALIAS_INBOX, [
|
||||
Email::ALIAS_INBOX . '.emailId:' => 'id',
|
||||
Email::ALIAS_INBOX . '.deleted' => false,
|
||||
Email::ALIAS_INBOX . '.userId' => $userId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\Email\Where\ItemConverters;
|
||||
|
||||
use Espo\Core\Select\Helpers\RandomStringGenerator;
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Classes\Select\Email\Helpers\EmailAddressHelper;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
class CcEquals implements ItemConverter
|
||||
{
|
||||
public function __construct(
|
||||
private EmailAddressHelper $emailAddressHelper,
|
||||
private RandomStringGenerator $randomStringGenerator
|
||||
) {}
|
||||
|
||||
public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem
|
||||
{
|
||||
$value = $item->getValue();
|
||||
|
||||
if (!$value) {
|
||||
return WhereClause::fromRaw([
|
||||
'id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$emailAddressId = $this->emailAddressHelper->getEmailAddressIdByValue($value);
|
||||
|
||||
if (!$emailAddressId) {
|
||||
return WhereClause::fromRaw([
|
||||
'id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$queryBuilder->distinct();
|
||||
|
||||
$alias = 'emailEmailAddress' . $this->randomStringGenerator->generate();
|
||||
|
||||
$queryBuilder->leftJoin(
|
||||
'EmailEmailAddress',
|
||||
$alias,
|
||||
[
|
||||
'emailId:' => 'id',
|
||||
'deleted' => false,
|
||||
]
|
||||
);
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
$alias . '.emailAddressId' => $emailAddressId,
|
||||
$alias . '.addressType' => 'cc',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\Email\Where\ItemConverters;
|
||||
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
class InArchiveIsFalse implements ItemConverter
|
||||
{
|
||||
public function __construct(private User $user, private JoinHelper $joinHelper)
|
||||
{}
|
||||
|
||||
public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem
|
||||
{
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
Email::ALIAS_INBOX . '.inArchive' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\Email\Where\ItemConverters;
|
||||
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
class InArchiveIsTrue implements ItemConverter
|
||||
{
|
||||
public function __construct(private User $user, private JoinHelper $joinHelper)
|
||||
{}
|
||||
|
||||
public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem
|
||||
{
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
Email::ALIAS_INBOX . '.inArchive' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,9 @@ use Espo\Entities\User;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Tools\Email\Folder;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class InFolder implements ItemConverter
|
||||
{
|
||||
public function __construct(
|
||||
@@ -59,19 +62,21 @@ class InFolder implements ItemConverter
|
||||
Folder::IMPORTANT => $this->convertImportant($queryBuilder),
|
||||
Folder::SENT => $this->convertSent($queryBuilder),
|
||||
Folder::TRASH => $this->convertTrash($queryBuilder),
|
||||
Folder::DRAFTS => $this->convertDraft($queryBuilder),
|
||||
Folder::ARCHIVE => $this->convertArchive($queryBuilder),
|
||||
Folder::DRAFTS => $this->convertDraft(),
|
||||
default => $this->convertFolderId($queryBuilder, $folderId),
|
||||
};
|
||||
}
|
||||
|
||||
protected function convertInbox(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
private function convertInbox(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
{
|
||||
$this->joinEmailUser($queryBuilder);
|
||||
|
||||
$whereClause = [
|
||||
'emailUser.inTrash' => false,
|
||||
'emailUser.folderId' => null,
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
Email::ALIAS_INBOX . '.inTrash' => false,
|
||||
Email::ALIAS_INBOX . '.inArchive' => false,
|
||||
Email::ALIAS_INBOX . '.folderId' => null,
|
||||
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
|
||||
[
|
||||
'status' => [
|
||||
Email::STATUS_ARCHIVED,
|
||||
@@ -83,7 +88,7 @@ class InFolder implements ItemConverter
|
||||
|
||||
$emailAddressIdList = $this->getEmailAddressIdList();
|
||||
|
||||
if (!empty($emailAddressIdList)) {
|
||||
if ($emailAddressIdList !== []) {
|
||||
$whereClause['fromEmailAddressId!='] = $emailAddressIdList;
|
||||
|
||||
$whereClause[] = [
|
||||
@@ -92,8 +97,7 @@ class InFolder implements ItemConverter
|
||||
'createdById!=' => $this->user->getId(),
|
||||
],
|
||||
];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$whereClause[] = [
|
||||
'status' => Email::STATUS_ARCHIVED,
|
||||
'createdById!=' => $this->user->getId(),
|
||||
@@ -103,7 +107,7 @@ class InFolder implements ItemConverter
|
||||
return WhereClause::fromRaw($whereClause);
|
||||
}
|
||||
|
||||
protected function convertSent(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
private function convertSent(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
{
|
||||
$this->joinEmailUser($queryBuilder);
|
||||
|
||||
@@ -118,31 +122,41 @@ class InFolder implements ItemConverter
|
||||
[
|
||||
'status!=' => Email::STATUS_DRAFT,
|
||||
],
|
||||
'emailUser.inTrash' => false,
|
||||
Email::ALIAS_INBOX . '.inTrash' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function convertImportant(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
private function convertImportant(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
{
|
||||
$this->joinEmailUser($queryBuilder);
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
'emailUser.isImportant' => true,
|
||||
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
|
||||
Email::ALIAS_INBOX . '.isImportant' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function convertTrash(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
private function convertTrash(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
{
|
||||
$this->joinEmailUser($queryBuilder);
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
'emailUser.inTrash' => true,
|
||||
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
|
||||
Email::ALIAS_INBOX . '.inTrash' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function convertDraft(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
private function convertArchive(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
{
|
||||
$this->joinEmailUser($queryBuilder);
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
|
||||
Email::ALIAS_INBOX . '.inArchive' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function convertDraft(): WhereClauseItem
|
||||
{
|
||||
return WhereClause::fromRaw([
|
||||
'status' => Email::STATUS_DRAFT,
|
||||
@@ -150,7 +164,7 @@ class InFolder implements ItemConverter
|
||||
]);
|
||||
}
|
||||
|
||||
protected function convertFolderId(QueryBuilder $queryBuilder, string $folderId): WhereClauseItem
|
||||
private function convertFolderId(QueryBuilder $queryBuilder, string $folderId): WhereClauseItem
|
||||
{
|
||||
$this->joinEmailUser($queryBuilder);
|
||||
|
||||
@@ -164,15 +178,17 @@ class InFolder implements ItemConverter
|
||||
return WhereClause::fromRaw([
|
||||
'groupFolderId' => $groupFolderId,
|
||||
'OR' => [
|
||||
'emailUser.id' => null,
|
||||
'emailUser.inTrash' => false,
|
||||
Email::ALIAS_INBOX . '.id' => null,
|
||||
Email::ALIAS_INBOX . '.inTrash' => false,
|
||||
Email::ALIAS_INBOX . '.inArchive' => false,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
'emailUser.inTrash' => false,
|
||||
'emailUser.folderId' => $folderId,
|
||||
Email::ALIAS_INBOX . '.inTrash' => false,
|
||||
Email::ALIAS_INBOX . '.inArchive' => false,
|
||||
Email::ALIAS_INBOX . '.folderId' => $folderId,
|
||||
'groupFolderId' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
@@ -47,7 +48,7 @@ class InTrashIsFalse implements ItemConverter
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
'emailUser.inTrash' => false,
|
||||
Email::ALIAS_INBOX . '.inTrash' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
@@ -47,7 +48,7 @@ class InTrashIsTrue implements ItemConverter
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
'emailUser.inTrash' => true,
|
||||
Email::ALIAS_INBOX . '.inTrash' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
@@ -47,7 +48,7 @@ class IsImportantIsFalse implements ItemConverter
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
'emailUser.isImportant' => false,
|
||||
Email::ALIAS_INBOX . '.isImportant' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
@@ -47,7 +48,7 @@ class IsImportantIsTrue implements ItemConverter
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
'emailUser.isImportant' => true,
|
||||
Email::ALIAS_INBOX . '.isImportant' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
@@ -47,7 +48,7 @@ class IsNotReadIsFalse implements ItemConverter
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
'emailUser.isRead' => true,
|
||||
Email::ALIAS_INBOX . '.isRead' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
@@ -47,7 +48,7 @@ class IsNotReadIsTrue implements ItemConverter
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
'emailUser.isRead' => false,
|
||||
Email::ALIAS_INBOX . '.isRead' => false,
|
||||
'OR' => [
|
||||
'sentById' => null,
|
||||
'sentById!=' => $this->user->getId()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\EmailAccount\PrimaryFilters;
|
||||
|
||||
use Espo\Core\Select\Primary\Filter;
|
||||
use Espo\Entities\EmailFilter;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
class Active implements Filter
|
||||
{
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
{
|
||||
$queryBuilder->where(['status' => EmailFilter::STATUS_ACTIVE]);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\Select\User\AccessControlFilters;
|
||||
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\AclManager;
|
||||
@@ -51,7 +52,7 @@ class Mandatory implements Filter
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->aclManager->getPermissionLevel($this->user, 'portalPermission') !== Table::LEVEL_YES) {
|
||||
if ($this->aclManager->getPermissionLevel($this->user, Permission::PORTAL) !== Table::LEVEL_YES) {
|
||||
$queryBuilder->where([
|
||||
'OR' => [
|
||||
'type!=' => User::TYPE_PORTAL,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\Select\User\AccessControlFilters;
|
||||
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\AclManager;
|
||||
@@ -43,7 +44,7 @@ class OnlyOwn implements Filter
|
||||
|
||||
public function apply(SelectBuilder $queryBuilder): void
|
||||
{
|
||||
if ($this->aclManager->getPermissionLevel($this->user, 'portalPermission') === Table::LEVEL_YES) {
|
||||
if ($this->aclManager->getPermissionLevel($this->user, Permission::PORTAL) === Table::LEVEL_YES) {
|
||||
$queryBuilder->where([
|
||||
'OR' => [
|
||||
'id' => $this->user->getId(),
|
||||
|
||||
@@ -51,7 +51,7 @@ class OnlyTeam implements Filter
|
||||
'id' => $this->user->getId(),
|
||||
];
|
||||
|
||||
if ($this->aclManager->getPermissionLevel($this->user, 'portalPermission') === Table::LEVEL_YES) {
|
||||
if ($this->aclManager->getPermissionLevel($this->user, 'portal') === Table::LEVEL_YES) {
|
||||
$orGroup['type'] = User::TYPE_PORTAL;
|
||||
}
|
||||
|
||||
|
||||
47
application/Espo/Classes/Select/User/BoolFilters/OnlyMe.php
Normal file
47
application/Espo/Classes/Select/User/BoolFilters/OnlyMe.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\User\BoolFilters;
|
||||
|
||||
use Espo\Core\Select\Bool\Filter;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\Where\OrGroupBuilder;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
class OnlyMe implements Filter
|
||||
{
|
||||
public function __construct(
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, OrGroupBuilder $orGroupBuilder): void
|
||||
{
|
||||
$queryBuilder->where(['id' => $this->user->getId()]);
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,7 @@ class GoogleMaps implements Helper
|
||||
|
||||
$url = "https://maps.googleapis.com/maps/api/staticmap?" .
|
||||
'center=' . $addressEncoded .
|
||||
'format=' . $format .
|
||||
'&format=' . $format .
|
||||
'&size=' . $size .
|
||||
'&key=' . $apiKey;
|
||||
|
||||
|
||||
50
application/Espo/Controllers/AddressCountry.php
Normal file
50
application/Espo/Controllers/AddressCountry.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Controllers\RecordBase;
|
||||
use Espo\Tools\Address\CountryDefaultsPopulator;
|
||||
|
||||
class AddressCountry extends RecordBase
|
||||
{
|
||||
protected function checkAccess(): bool
|
||||
{
|
||||
return $this->user->isAdmin();
|
||||
}
|
||||
|
||||
public function postActionPopulateDefaults(): bool
|
||||
{
|
||||
$populate = $this->injectableFactory->create(CountryDefaultsPopulator::class);
|
||||
|
||||
$populate->populate();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ use Espo\Core\Container;
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\AdminNotificationManager;
|
||||
use Espo\Tools\AdminNotifications\Manager;
|
||||
use Espo\Core\Utils\SystemRequirements;
|
||||
use Espo\Core\Utils\ScheduledJob;
|
||||
use Espo\Core\Upgrades\UpgradeManager;
|
||||
@@ -51,7 +51,7 @@ class Admin
|
||||
private Container $container,
|
||||
private Config $config,
|
||||
private User $user,
|
||||
private AdminNotificationManager $adminNotificationManager,
|
||||
private Manager $adminNotificationManager,
|
||||
private SystemRequirements $systemRequirements,
|
||||
private ScheduledJob $scheduledJob,
|
||||
private DataManager $dataManager
|
||||
|
||||
@@ -29,10 +29,14 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
class ApiIndex
|
||||
{
|
||||
public function getActionIndex(): string
|
||||
public function getActionIndex(Request $request, Response $response): void
|
||||
{
|
||||
return "EspoCRM REST API";
|
||||
$response->writeBody(Json::encode("EspoCRM REST API"));
|
||||
}
|
||||
}
|
||||
|
||||
76
application/Espo/Controllers/AppLogRecord.php
Normal file
76
application/Espo/Controllers/AppLogRecord.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero 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\Controllers\Record;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use stdClass;
|
||||
|
||||
class AppLogRecord extends Record
|
||||
{
|
||||
protected function checkAccess(): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->config->get('restrictedMode')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->config->get('appLogAdminAllowed')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->user->isSuperAdmin();
|
||||
}
|
||||
|
||||
public function postActionCreate(Request $request, Response $response): stdClass
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
public function putActionUpdate(Request $request, Response $response): stdClass
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
public function postActionCreateLink(Request $request): bool
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
public function deleteActionRemoveLink(Request $request): bool
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
@@ -36,17 +36,26 @@ use Espo\Core\Acl;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Tools\DataPrivacy\Erasor;
|
||||
|
||||
class DataPrivacy
|
||||
{
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function __construct(private Erasor $erasor, private Acl $acl)
|
||||
{
|
||||
if ($this->acl->getPermissionLevel('dataPrivacyPermission') === Acl\Table::LEVEL_NO) {
|
||||
if ($this->acl->getPermissionLevel(Acl\Permission::DATA_PRIVACY) === Acl\Table::LEVEL_NO) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionErase(Request $request, Response $response): void
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
@@ -35,6 +35,7 @@ use Espo\Core\Mail\Account\Storage\Params as StorageParams;
|
||||
|
||||
use Espo\Core\Controllers\Record;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Mail\Exceptions\ImapError;
|
||||
|
||||
class InboundEmail extends Record
|
||||
{
|
||||
@@ -45,7 +46,8 @@ class InboundEmail extends Record
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
* @throws Error
|
||||
* @throws ImapError
|
||||
*/
|
||||
public function postActionGetFolders(Request $request): array
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Controllers\Record;
|
||||
|
||||
@@ -36,7 +37,7 @@ class Portal extends Record
|
||||
{
|
||||
protected function checkAccess(): bool
|
||||
{
|
||||
$level = $this->acl->getPermissionLevel('portal');
|
||||
$level = $this->acl->getPermissionLevel(Permission::PORTAL);
|
||||
|
||||
return $level === Table::LEVEL_YES;
|
||||
}
|
||||
|
||||
@@ -68,19 +68,28 @@ class Stream
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if ($id === null && $scope !== UserEntity::ENTITY_TYPE) {
|
||||
throw new BadRequest("No ID.");
|
||||
}
|
||||
|
||||
$searchParams = $this->fetchSearchParams($request);
|
||||
|
||||
$result = $scope === UserEntity::ENTITY_TYPE ?
|
||||
$this->userRecordService->find($id, $searchParams) :
|
||||
$this->service->find($scope, $id ?? '', $searchParams);
|
||||
if ($scope === UserEntity::ENTITY_TYPE) {
|
||||
$collection = $this->userRecordService->find($id, $searchParams);
|
||||
|
||||
return (object) [
|
||||
'total' => $collection->getTotal(),
|
||||
'list' => $collection->getValueMapList(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($id === null) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$collection = $this->service->find($scope, $id, $searchParams);
|
||||
$pinnedCollection = $this->service->getPinned($scope, $id);
|
||||
|
||||
return (object) [
|
||||
'total' => $result->getTotal(),
|
||||
'list' => $result->getValueMapList(),
|
||||
'total' => $collection->getTotal(),
|
||||
'list' => $collection->getValueMapList(),
|
||||
'pinnedList' => $pinnedCollection->getValueMapList(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Core;
|
||||
|
||||
use Espo\Core\Acl\Exceptions\NotImplemented;
|
||||
use Espo\Core\Acl\GlobalRestriction;
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Core\Acl\Table;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
@@ -283,7 +284,7 @@ class Acl
|
||||
*
|
||||
* @param User|string $target User entity or user ID.
|
||||
*/
|
||||
public function checkUserPermission($target, string $permissionType = 'user'): bool
|
||||
public function checkUserPermission($target, string $permissionType = Permission::USER): bool
|
||||
{
|
||||
return $this->aclManager->checkUserPermission($this->user, $target, $permissionType);
|
||||
}
|
||||
|
||||
@@ -30,11 +30,8 @@
|
||||
namespace Espo\Core\Acl\AccessChecker\AccessCheckers;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use Espo\Core\Acl\DefaultAccessChecker;
|
||||
use Espo\Core\Acl\Traits\DefaultAccessCheckerDependency;
|
||||
use Espo\Core\Acl\AccessEntityCreateChecker;
|
||||
@@ -67,17 +64,12 @@ class Foreign implements
|
||||
{
|
||||
use DefaultAccessCheckerDependency;
|
||||
|
||||
private Metadata $metadata;
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
private Metadata $metadata,
|
||||
DefaultAccessChecker $defaultAccessChecker,
|
||||
EntityManager $entityManager
|
||||
private EntityManager $entityManager
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->defaultAccessChecker = $defaultAccessChecker;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
private function getForeignEntity(Entity $entity): ?Entity
|
||||
@@ -121,7 +113,9 @@ class Foreign implements
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityCreate($user, $entity, $data);
|
||||
// @todo Check parent 'edit' access.
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityCreate($user, $foreign, $data);
|
||||
}
|
||||
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
@@ -132,7 +126,7 @@ class Foreign implements
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityRead($user, $entity, $data);
|
||||
return $this->defaultAccessChecker->checkEntityRead($user, $foreign, $data);
|
||||
}
|
||||
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
@@ -143,7 +137,7 @@ class Foreign implements
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityEdit($user, $entity, $data);
|
||||
return $this->defaultAccessChecker->checkEntityEdit($user, $foreign, $data);
|
||||
}
|
||||
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
@@ -151,10 +145,14 @@ class Foreign implements
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityDelete($user, $entity, $data);
|
||||
return $this->defaultAccessChecker->checkEntityDelete($user, $foreign, $data);
|
||||
}
|
||||
|
||||
public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool
|
||||
@@ -165,6 +163,6 @@ class Foreign implements
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityStream($user, $entity, $data);
|
||||
return $this->defaultAccessChecker->checkEntityStream($user, $foreign, $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class DefaultAccessChecker implements
|
||||
return $this->scopeChecker->check($data, $action, $checkerData);
|
||||
}
|
||||
|
||||
private function checkScope(User $user, ScopeData $data, ?string $action = null): bool
|
||||
private function checkScope(ScopeData $data, ?string $action = null): bool
|
||||
{
|
||||
$checkerData = ScopeCheckerData
|
||||
::createBuilder()
|
||||
@@ -100,27 +100,27 @@ class DefaultAccessChecker implements
|
||||
|
||||
public function check(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkScope($user, $data);
|
||||
return $this->checkScope($data);
|
||||
}
|
||||
|
||||
public function checkCreate(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkScope($user, $data, Table::ACTION_CREATE);
|
||||
return $this->checkScope($data, Table::ACTION_CREATE);
|
||||
}
|
||||
|
||||
public function checkRead(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkScope($user, $data, Table::ACTION_READ);
|
||||
return $this->checkScope($data, Table::ACTION_READ);
|
||||
}
|
||||
|
||||
public function checkEdit(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkScope($user, $data, Table::ACTION_EDIT);
|
||||
return $this->checkScope($data, Table::ACTION_EDIT);
|
||||
}
|
||||
|
||||
public function checkDelete(User $user, ScopeData $data): bool
|
||||
{
|
||||
if ($this->checkScope($user, $data, Table::ACTION_DELETE)) {
|
||||
if ($this->checkScope($data, Table::ACTION_DELETE)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ class DefaultAccessChecker implements
|
||||
|
||||
public function checkStream(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkScope($user, $data, Table::ACTION_STREAM);
|
||||
return $this->checkScope($data, Table::ACTION_STREAM);
|
||||
}
|
||||
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
|
||||
@@ -99,7 +99,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
return false;
|
||||
}
|
||||
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission');
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
|
||||
|
||||
if (
|
||||
$assignmentPermission === Table::LEVEL_YES ||
|
||||
@@ -157,7 +157,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
|
||||
protected function isPermittedTeams(User $user, Entity $entity): bool
|
||||
{
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission');
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
|
||||
|
||||
if (!in_array($assignmentPermission, [Table::LEVEL_TEAM, Table::LEVEL_NO])) {
|
||||
return true;
|
||||
@@ -219,7 +219,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
|
||||
private function isPermittedTeamsEmpty(User $user, CoreEntity $entity): bool
|
||||
{
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission');
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
|
||||
|
||||
if ($assignmentPermission !== Table::LEVEL_TEAM) {
|
||||
return true;
|
||||
@@ -259,7 +259,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
return false;
|
||||
}
|
||||
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission');
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
|
||||
|
||||
if (
|
||||
$assignmentPermission === Table::LEVEL_YES ||
|
||||
|
||||
@@ -81,7 +81,6 @@ class DefaultOwnershipChecker implements OwnershipOwnChecker, OwnershipTeamCheck
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var string[] $userTeamIdList */
|
||||
$userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
if (
|
||||
|
||||
@@ -53,7 +53,7 @@ class OwnerUserFieldProvider
|
||||
|
||||
/**
|
||||
* Get an entity field that stores an owner-user (or multiple users).
|
||||
* Must be link or linkMulitple field. NULL means no owner.
|
||||
* Must be a link or linkMultiple field. NULL means no owner.
|
||||
*/
|
||||
public function get(string $entityType): ?string
|
||||
{
|
||||
|
||||
43
application/Espo/Core/Acl/Permission.php
Normal file
43
application/Espo/Core/Acl/Permission.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Acl;
|
||||
|
||||
class Permission
|
||||
{
|
||||
public const ASSIGNMENT = 'assignment';
|
||||
public const USER = 'user';
|
||||
public const PORTAL = 'portal';
|
||||
public const MASS_UPDATE = 'massUpdate';
|
||||
public const EXPORT = 'export';
|
||||
public const AUDIT = 'audit';
|
||||
public const DATA_PRIVACY = 'dataPrivacy';
|
||||
public const MESSAGE = 'message';
|
||||
public const MENTION = 'mention';
|
||||
}
|
||||
@@ -52,9 +52,7 @@ class DefaultTable implements Table
|
||||
protected string $type = 'acl';
|
||||
protected string $defaultAclType = 'recordAllTeamOwnNo';
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
/** @var string[] */
|
||||
private $actionList = [
|
||||
self::ACTION_READ,
|
||||
self::ACTION_STREAM,
|
||||
@@ -63,16 +61,12 @@ class DefaultTable implements Table
|
||||
self::ACTION_CREATE,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
/** @var string[] */
|
||||
private $booleanActionList = [
|
||||
self::ACTION_CREATE,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
/** @var string[] */
|
||||
protected $levelList = [
|
||||
self::LEVEL_YES,
|
||||
self::LEVEL_ALL,
|
||||
@@ -81,30 +75,23 @@ class DefaultTable implements Table
|
||||
self::LEVEL_NO,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
/** @var string[] */
|
||||
private $fieldActionList = [
|
||||
self::ACTION_READ,
|
||||
self::ACTION_EDIT,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
/** @var string[] */
|
||||
protected $fieldLevelList = [
|
||||
self::LEVEL_YES,
|
||||
self::LEVEL_NO,
|
||||
];
|
||||
|
||||
private stdClass $data;
|
||||
|
||||
private string $cacheKey;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
/** @var string[] */
|
||||
private $valuePermissionList = [];
|
||||
private ScopeDataResolver $scopeDataResolver;
|
||||
|
||||
public function __construct(
|
||||
private RoleListProvider $roleListProvider,
|
||||
@@ -112,7 +99,7 @@ class DefaultTable implements Table
|
||||
protected User $user,
|
||||
Config $config,
|
||||
protected Metadata $metadata,
|
||||
DataCache $dataCache
|
||||
DataCache $dataCache,
|
||||
) {
|
||||
|
||||
$this->data = (object) [
|
||||
@@ -135,14 +122,15 @@ class DefaultTable implements Table
|
||||
$cachedData = $dataCache->get($this->cacheKey);
|
||||
|
||||
$this->data = $cachedData;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$this->load();
|
||||
|
||||
if ($config->get('useCache')) {
|
||||
$dataCache->store($this->cacheKey, $this->data);
|
||||
}
|
||||
}
|
||||
|
||||
$this->scopeDataResolver = new ScopeDataResolver($this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,11 +144,7 @@ class DefaultTable implements Table
|
||||
|
||||
$data = $this->data->scopes->$scope;
|
||||
|
||||
if (is_string($data)) {
|
||||
return $this->getScopeData($data);
|
||||
}
|
||||
|
||||
return ScopeData::fromRaw($data);
|
||||
return $this->scopeDataResolver->resolve($data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
66
application/Espo/Core/Acl/Table/ScopeDataResolver.php
Normal file
66
application/Espo/Core/Acl/Table/ScopeDataResolver.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Acl\Table;
|
||||
|
||||
use Espo\Core\Acl\ScopeData;
|
||||
use Espo\Core\Acl\Table;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ScopeDataResolver
|
||||
{
|
||||
public function __construct(
|
||||
private Table $table,
|
||||
) {}
|
||||
|
||||
public function resolve(mixed $data): ScopeData
|
||||
{
|
||||
if (!is_string($data)) {
|
||||
return ScopeData::fromRaw($data);
|
||||
}
|
||||
|
||||
$foreignScope = $data;
|
||||
$isBoolean = false;
|
||||
|
||||
if (str_starts_with($data, 'boolean:')) {
|
||||
[, $foreignScope] = explode(':', $data, 2);
|
||||
$isBoolean = true;
|
||||
}
|
||||
|
||||
$scopeData = $this->table->getScopeData($foreignScope);
|
||||
|
||||
if ($isBoolean && !$scopeData->isBoolean()) {
|
||||
return ScopeData::fromRaw(true);
|
||||
}
|
||||
|
||||
return $scopeData;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core;
|
||||
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\User;
|
||||
@@ -69,7 +70,7 @@ use InvalidArgumentException;
|
||||
*/
|
||||
class AclManager
|
||||
{
|
||||
protected const PERMISSION_ASSIGNMENT = 'assignment';
|
||||
protected const PERMISSION_ASSIGNMENT = Permission::ASSIGNMENT;
|
||||
|
||||
/** @var array<string, AccessChecker> */
|
||||
private $accessCheckerHashMap = [];
|
||||
@@ -572,7 +573,7 @@ class AclManager
|
||||
*
|
||||
* @param User|string $target User entity or user ID.
|
||||
*/
|
||||
public function checkUserPermission(User $user, $target, string $permissionType = 'user'): bool
|
||||
public function checkUserPermission(User $user, $target, string $permissionType = Permission::USER): bool
|
||||
{
|
||||
$permission = $this->getPermissionLevel($user, $permissionType);
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Authentication\HeaderKey;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\ServiceUnavailable;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
@@ -47,8 +48,6 @@ use Exception;
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
private const HEADER_ESPO_AUTHORIZATION = 'Espo-Authorization';
|
||||
|
||||
public function __construct(
|
||||
private Log $log,
|
||||
private Authentication $authentication,
|
||||
@@ -195,8 +194,12 @@ class Auth
|
||||
throw new BadRequest("Auth: Bad authorization string provided.");
|
||||
}
|
||||
|
||||
/** @var array{string, string} */
|
||||
return explode(':', $stringDecoded, 2);
|
||||
[$username, $password] = explode(':', $stringDecoded, 2);
|
||||
|
||||
$username = trim($username);
|
||||
$password = trim($password);
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
private function handleSecondStepRequired(Response $response, Result $result): void
|
||||
@@ -237,7 +240,9 @@ class Auth
|
||||
$response->writeBody($e->getBody());
|
||||
}
|
||||
|
||||
$this->log->notice("Auth: " . $e->getMessage());
|
||||
if ($e->getMessage()) {
|
||||
$this->log->notice("Auth exception: {message}", ['message' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -269,7 +274,7 @@ class Auth
|
||||
|
||||
private function obtainAuthenticationMethodFromRequest(Request $request): ?string
|
||||
{
|
||||
if ($request->hasHeader(self::HEADER_ESPO_AUTHORIZATION)) {
|
||||
if ($request->hasHeader(HeaderKey::AUTHORIZATION)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -299,12 +304,10 @@ class Auth
|
||||
*/
|
||||
private function obtainUsernamePasswordFromRequest(Request $request): array
|
||||
{
|
||||
if ($request->hasHeader(self::HEADER_ESPO_AUTHORIZATION)) {
|
||||
[$username, $password] = $this->decodeAuthorizationString(
|
||||
$request->getHeader(self::HEADER_ESPO_AUTHORIZATION) ?? ''
|
||||
);
|
||||
if ($request->hasHeader(HeaderKey::AUTHORIZATION)) {
|
||||
$headerValue = $request->getHeader(HeaderKey::AUTHORIZATION) ?? '';
|
||||
|
||||
return [$username, $password];
|
||||
return $this->decodeAuthorizationString($headerValue);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -314,6 +317,14 @@ class Auth
|
||||
$username = $request->getServerParam('PHP_AUTH_USER');
|
||||
$password = $request->getServerParam('PHP_AUTH_PW');
|
||||
|
||||
if (is_string($username)) {
|
||||
$username = trim($username);
|
||||
}
|
||||
|
||||
if (is_string($password)) {
|
||||
$password = trim($password);
|
||||
}
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,11 @@ use Espo\Core\Exceptions\HasBody;
|
||||
use Espo\Core\Exceptions\HasLogLevel;
|
||||
use Espo\Core\Exceptions\HasLogMessage;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
use LogicException;
|
||||
use Psr\Log\LogLevel;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@@ -80,7 +82,7 @@ class ErrorOutput
|
||||
NotFound::class,
|
||||
];
|
||||
|
||||
public function __construct(private Log $log, private Config $config)
|
||||
public function __construct(private Log $log)
|
||||
{}
|
||||
|
||||
public function process(
|
||||
@@ -112,6 +114,11 @@ class ErrorOutput
|
||||
): void {
|
||||
|
||||
$message = $exception->getMessage();
|
||||
|
||||
if ($exception->getPrevious() && $exception->getPrevious()->getMessage()) {
|
||||
$message .= " " . $exception->getPrevious()->getMessage();
|
||||
}
|
||||
|
||||
$statusCode = $exception->getCode();
|
||||
|
||||
if ($exception instanceof HasLogMessage) {
|
||||
@@ -122,30 +129,12 @@ class ErrorOutput
|
||||
$this->processRoute($route, $request, $exception);
|
||||
}
|
||||
|
||||
$logLevel = $exception instanceof HasLogLevel ?
|
||||
$exception->getLogLevel() :
|
||||
Log::LEVEL_ERROR;
|
||||
$level = $this->getLevel($exception);
|
||||
|
||||
$messageLineFile =
|
||||
'line: ' . $exception->getLine() . ', ' .
|
||||
'file: ' . $exception->getFile();
|
||||
|
||||
$logMessageItemList = [];
|
||||
|
||||
if ($message) {
|
||||
$logMessageItemList[] = "$message";
|
||||
}
|
||||
|
||||
$logMessageItemList[] = $request->getMethod() . ' ' . $request->getResourcePath();
|
||||
$logMessageItemList[] = $messageLineFile;
|
||||
|
||||
$logMessage = "($statusCode) " . implode("; ", $logMessageItemList);
|
||||
|
||||
if ($this->toPrintTrace()) {
|
||||
$logMessage .= " :: " . $exception->getTraceAsString();
|
||||
}
|
||||
|
||||
$this->log->log($logLevel, $logMessage);
|
||||
$this->log->log($level, $message, [
|
||||
'exception' => $exception,
|
||||
'request' => $request,
|
||||
]);
|
||||
|
||||
if (!in_array($statusCode, $this->allowedStatusCodeList)) {
|
||||
$statusCode = 500;
|
||||
@@ -224,6 +213,11 @@ class ErrorOutput
|
||||
$requestBodyString = $this->clearPasswords($request->getBodyContents() ?? '');
|
||||
|
||||
$message = $exception->getMessage();
|
||||
|
||||
if ($exception->getPrevious() && $exception->getPrevious()->getMessage()) {
|
||||
$message .= " " . $exception->getPrevious()->getMessage();
|
||||
}
|
||||
|
||||
$statusCode = $exception->getCode();
|
||||
|
||||
$routeParams = $request->getRouteParams();
|
||||
@@ -250,12 +244,7 @@ class ErrorOutput
|
||||
|
||||
$logMessage .= implode("; ", $logMessageItemList);
|
||||
|
||||
$this->log->log('debug', $logMessage);
|
||||
}
|
||||
|
||||
private function toPrintTrace(): bool
|
||||
{
|
||||
return (bool) $this->config->get('logger.printTrace');
|
||||
$this->log->debug($logMessage);
|
||||
}
|
||||
|
||||
private function toPrintExceptionStatusReason(Throwable $exception): bool
|
||||
@@ -269,4 +258,21 @@ class ErrorOutput
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getLevel(Throwable $exception): string
|
||||
{
|
||||
if ($exception instanceof HasLogLevel) {
|
||||
return $exception->getLogLevel();
|
||||
}
|
||||
|
||||
if ($exception instanceof LogicException) {
|
||||
return LogLevel::ALERT;
|
||||
}
|
||||
|
||||
if ($exception instanceof RuntimeException) {
|
||||
return LogLevel::CRITICAL;
|
||||
}
|
||||
|
||||
return LogLevel::ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ use Exception;
|
||||
class Command implements Runner
|
||||
{
|
||||
use Cli;
|
||||
use SetupSystemUser;
|
||||
|
||||
public function __construct(private ConsoleCommandManager $commandManager)
|
||||
{}
|
||||
|
||||
@@ -67,9 +67,7 @@ class Authentication
|
||||
{
|
||||
private const LOGOUT_USERNAME = '**logout';
|
||||
|
||||
private const HEADER_ESPO_AUTHORIZATION = 'Espo-Authorization';
|
||||
private const HEADER_CREATE_TOKEN_SECRET = 'Espo-Authorization-Create-Token-Secret';
|
||||
private const HEADER_BY_TOKEN = 'Espo-Authorization-By-Token';
|
||||
private const HEADER_ANOTHER_USER = 'X-Another-User';
|
||||
private const HEADER_LOGOUT_REDIRECT_URL = 'X-Logout-Redirect-Url';
|
||||
|
||||
@@ -95,6 +93,7 @@ class Authentication
|
||||
* Process logging in.
|
||||
*
|
||||
* @throws ServiceUnavailable
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function login(AuthenticationData $data, Request $request, Response $response): Result
|
||||
{
|
||||
@@ -107,16 +106,21 @@ class Authentication
|
||||
$method &&
|
||||
!$this->configDataProvider->authenticationMethodIsApi($method)
|
||||
) {
|
||||
$this->log
|
||||
->warning("AUTH: Trying to use not allowed authentication method '$method'.");
|
||||
$this->log->warning("Auth: Trying to use not allowed authentication method '{method}'.", [
|
||||
'method' => $method,
|
||||
]);
|
||||
|
||||
return $this->processFail(Result::fail(FailReason::METHOD_NOT_ALLOWED), $data, $request);
|
||||
}
|
||||
|
||||
$this->hookManager->processBeforeLogin($data, $request);
|
||||
try {
|
||||
$this->hookManager->processBeforeLogin($data, $request);
|
||||
} catch (Forbidden $e) {
|
||||
$this->processForbidden($e);
|
||||
}
|
||||
|
||||
if (!$method && $password === null) {
|
||||
$this->log->error("AUTH: Trying to login w/o password.");
|
||||
$this->log->error("Auth: Trying to login w/o password.");
|
||||
|
||||
return Result::fail(FailReason::NO_PASSWORD);
|
||||
}
|
||||
@@ -149,7 +153,7 @@ class Authentication
|
||||
}
|
||||
}
|
||||
|
||||
$byTokenAndUsername = $request->getHeader(self::HEADER_BY_TOKEN) === 'true';
|
||||
$byTokenAndUsername = $request->getHeader(HeaderKey::AUTHORIZATION_BY_TOKEN) === 'true';
|
||||
|
||||
if ($method && $byTokenAndUsername) {
|
||||
return Result::fail(FailReason::DISCREPANT_DATA);
|
||||
@@ -157,7 +161,9 @@ class Authentication
|
||||
|
||||
if (($byTokenAndUsername || $byTokenOnly) && !$authToken) {
|
||||
if ($username) {
|
||||
$this->log->info("AUTH: Trying to login as user '$username' by token but token is not found.");
|
||||
$this->log->info("Auth: Trying to login as user '{username}' by token but token is not found.", [
|
||||
'username' => $username,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->processFail(Result::fail(FailReason::TOKEN_NOT_FOUND), $data, $request);
|
||||
@@ -213,15 +219,7 @@ class Authentication
|
||||
return $this->processFail(Result::fail(FailReason::DENIED), $data, $request);
|
||||
}
|
||||
|
||||
if ($this->isPortal()) {
|
||||
$user->set('portalId', $this->getPortal()->getId());
|
||||
}
|
||||
|
||||
if (!$this->isPortal()) {
|
||||
$user->loadLinkMultipleField('teams');
|
||||
}
|
||||
|
||||
$user->set('ipAddress', $this->util->obtainIpFromRequest($request));
|
||||
$this->prepareUser($user, $request);
|
||||
|
||||
[$loggedUser, $anotherUserFailReason] = $this->getLoggedUser($request, $user);
|
||||
|
||||
@@ -234,6 +232,7 @@ class Authentication
|
||||
$this->applicationUser->setUser($loggedUser);
|
||||
|
||||
if (
|
||||
!$result->bypassSecondStep() &&
|
||||
!$result->isSecondStepRequired() &&
|
||||
!$authToken &&
|
||||
$this->configDataProvider->isTwoFactorEnabled()
|
||||
@@ -241,13 +240,19 @@ class Authentication
|
||||
$result = $this->processTwoFactor($result, $request);
|
||||
|
||||
if ($result->isFail()) {
|
||||
return $this->processFail($result, $data, $request);
|
||||
return $this->processTwoFactorFail($result, $data, $request, $authLogRecord);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->hookManager->processOnLogin($result, $data, $request);
|
||||
} catch (Forbidden $e) {
|
||||
$this->processForbidden($e, $authLogRecord);
|
||||
}
|
||||
|
||||
if (
|
||||
!$result->isSecondStepRequired() &&
|
||||
$request->getHeader(self::HEADER_ESPO_AUTHORIZATION)
|
||||
$request->getHeader(HeaderKey::AUTHORIZATION)
|
||||
) {
|
||||
$authToken = $this->processAuthTokenFinal(
|
||||
$authToken,
|
||||
@@ -344,13 +349,13 @@ class Authentication
|
||||
private function processAuthTokenCheck(AuthToken $authToken): bool
|
||||
{
|
||||
if ($this->isPortal() && $authToken->getPortalId() !== $this->getPortal()->getId()) {
|
||||
$this->log->info("AUTH: Trying to login to portal with a token not related to portal.");
|
||||
$this->log->info("Auth: Trying to login to portal with a token not related to portal.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isPortal() && $authToken->getPortalId()) {
|
||||
$this->log->info("AUTH: Trying to login to crm with a token related to portal.");
|
||||
$this->log->info("Auth: Trying to login to crm with a token related to portal.");
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -361,8 +366,9 @@ class Authentication
|
||||
private function processUserCheck(User $user, ?AuthLogRecord $authLogRecord): bool
|
||||
{
|
||||
if (!$user->isActive()) {
|
||||
$this->log
|
||||
->info("AUTH: Trying to login as user '" . $user->getUserName() . "' which is not active.");
|
||||
$this->log->info("Auth: Trying to login as user '{username}' which is not active.", [
|
||||
'username' => $user->getUserName(),
|
||||
]);
|
||||
|
||||
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_INACTIVE_USER);
|
||||
|
||||
@@ -370,8 +376,9 @@ class Authentication
|
||||
}
|
||||
|
||||
if ($user->isSystem()) {
|
||||
$this->log
|
||||
->info("AUTH: Trying to login to crm as a system user '{$user->getUserName()}'.");
|
||||
$this->log->info("Auth: Trying to login to crm as a system user '{username}'.", [
|
||||
'username' => $user->getUserName(),
|
||||
]);
|
||||
|
||||
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_IS_SYSTEM_USER);
|
||||
|
||||
@@ -379,8 +386,9 @@ class Authentication
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && !$this->isPortal() && $user->isPortal()) {
|
||||
$this->log
|
||||
->info("AUTH: Trying to login to crm as a portal user '" . $user->getUserName() . "'.");
|
||||
$this->log->info("Auth: Trying to login to crm as a portal user '{username}'.", [
|
||||
'username' => $user->getUserName(),
|
||||
]);
|
||||
|
||||
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_IS_PORTAL_USER);
|
||||
|
||||
@@ -388,8 +396,9 @@ class Authentication
|
||||
}
|
||||
|
||||
if ($this->isPortal() && !$user->isPortal()) {
|
||||
$this->log->info(
|
||||
"AUTH: Trying to login to portal as user '" . $user->getUserName() . "' which is not portal user.");
|
||||
$this->log->info("Auth: Trying to login to portal as user '{username}' which is not portal user.", [
|
||||
'username' => $user->getUserName(),
|
||||
]);
|
||||
|
||||
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_IS_NOT_PORTAL_USER);
|
||||
|
||||
@@ -403,9 +412,12 @@ class Authentication
|
||||
->isRelated($user);
|
||||
|
||||
if (!$isPortalRelatedToUser) {
|
||||
$this->log->info(
|
||||
"AUTH: Trying to login to portal as user '" . $user->getUserName() . "' ".
|
||||
"which is portal user but does not belong to portal.");
|
||||
$msg = "Auth: Trying to login to portal as user '{username}' " .
|
||||
"which is portal user but does not belong to portal.";
|
||||
|
||||
$this->log->info($msg, [
|
||||
'username' => $user->getUserName(),
|
||||
]);
|
||||
|
||||
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_USER_IS_NOT_IN_PORTAL);
|
||||
|
||||
@@ -771,4 +783,56 @@ class Authentication
|
||||
|
||||
return [$loggedUser, null];
|
||||
}
|
||||
|
||||
private function prepareUser(User $user, Request $request): void
|
||||
{
|
||||
if ($this->isPortal()) {
|
||||
$user->set('portalId', $this->getPortal()->getId());
|
||||
}
|
||||
|
||||
if (!$this->isPortal()) {
|
||||
$user->loadLinkMultipleField('teams');
|
||||
}
|
||||
|
||||
$user->set('ipAddress', $this->util->obtainIpFromRequest($request));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processForbidden(Forbidden $exception, ?AuthLogRecord $authLogRecord = null): never
|
||||
{
|
||||
$this->log->warning('Auth: Forbidden. {message}', [
|
||||
'message' => $exception->getMessage(),
|
||||
'exception' => $exception,
|
||||
]);
|
||||
|
||||
if ($authLogRecord) {
|
||||
$authLogRecord
|
||||
->setIsDenied()
|
||||
->setDenialReason(AuthLogRecord::DENIAL_REASON_FORBIDDEN);
|
||||
|
||||
$this->entityManager->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
private function processTwoFactorFail(
|
||||
Result $result,
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
?AuthLogRecord $authLogRecord
|
||||
): Result {
|
||||
|
||||
if ($authLogRecord) {
|
||||
$authLogRecord
|
||||
->setIsDenied()
|
||||
->setDenialReason(AuthLogRecord::DENIAL_REASON_WRONG_CODE);
|
||||
|
||||
$this->entityManager->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
return $this->processFail($result, $data, $request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ use Espo\Core\Utils\Metadata;
|
||||
class ConfigDataProvider
|
||||
{
|
||||
private const FAILED_ATTEMPTS_PERIOD = '60 seconds';
|
||||
private const FAILED_CODE_ATTEMPTS_PERIOD = '5 minutes';
|
||||
private const MAX_FAILED_ATTEMPT_NUMBER = 10;
|
||||
|
||||
public function __construct(private Config $config, private Metadata $metadata)
|
||||
@@ -50,6 +51,14 @@ class ConfigDataProvider
|
||||
return $this->config->get('authFailedAttemptsPeriod', self::FAILED_ATTEMPTS_PERIOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* A period for max failed 2FA code attempts checking.
|
||||
*/
|
||||
public function getFailedCodeAttemptsPeriod(): string
|
||||
{
|
||||
return $this->config->get('authFailedCodeAttemptsPeriod', self::FAILED_CODE_ATTEMPTS_PERIOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Max failed log in attempts.
|
||||
*/
|
||||
@@ -147,4 +156,25 @@ class ConfigDataProvider
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function ipAddressCheck(): bool
|
||||
{
|
||||
return (bool) $this->config->get('authIpAddressCheck');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getIpAddressWhitelist(): array
|
||||
{
|
||||
return $this->config->get('authIpAddressWhitelist') ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getIpAddressCheckExcludedUserIdList(): array
|
||||
{
|
||||
return $this->config->get('authIpAddressCheckExcludedUsersIds') ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
37
application/Espo/Core/Authentication/HeaderKey.php
Normal file
37
application/Espo/Core/Authentication/HeaderKey.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication;
|
||||
|
||||
class HeaderKey
|
||||
{
|
||||
public const AUTHORIZATION_BY_TOKEN = 'Espo-Authorization-By-Token';
|
||||
public const AUTHORIZATION_CODE = 'Espo-Authorization-Code';
|
||||
public const AUTHORIZATION = 'Espo-Authorization';
|
||||
}
|
||||
@@ -36,11 +36,12 @@ use Espo\Core\Exceptions\ServiceUnavailable;
|
||||
|
||||
/**
|
||||
* Before logging in, before credentials are checked.
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws ServiceUnavailable
|
||||
*/
|
||||
interface BeforeLogin
|
||||
{
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws ServiceUnavailable
|
||||
*/
|
||||
public function process(AuthenticationData $data, Request $request): void;
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@
|
||||
namespace Espo\Core\Authentication\Hook\Hooks;
|
||||
|
||||
use Espo\Core\Api\Util;
|
||||
use Espo\Core\Authentication\HeaderKey;
|
||||
use Espo\Core\Authentication\Hook\BeforeLogin;
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Authentication\ConfigDataProvider;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\AuthLogRecord;
|
||||
|
||||
@@ -51,7 +51,6 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
public function __construct(
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private EntityManager $entityManager,
|
||||
private Log $log,
|
||||
private Util $util
|
||||
) {}
|
||||
|
||||
@@ -60,34 +59,20 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
*/
|
||||
public function process(AuthenticationData $data, Request $request): void
|
||||
{
|
||||
$isByTokenOnly = !$data->getMethod() && $request->getHeader('Espo-Authorization-By-Token') === 'true';
|
||||
$isByTokenOnly = !$data->getMethod() && $request->getHeader(HeaderKey::AUTHORIZATION_BY_TOKEN) === 'true';
|
||||
|
||||
if ($isByTokenOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->configDataProvider->isAuthLogDisabled()) {
|
||||
if ($isByTokenOnly || $this->configDataProvider->isAuthLogDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$failedAttemptsPeriod = $this->configDataProvider->getFailedAttemptsPeriod();
|
||||
$maxFailedAttempts = $this->configDataProvider->getMaxFailedAttemptNumber();
|
||||
|
||||
$requestTime = intval($request->getServerParam('REQUEST_TIME_FLOAT'));
|
||||
|
||||
try {
|
||||
$requestTimeFrom = (new DateTime('@' . $requestTime))->modify('-' . $failedAttemptsPeriod);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$ip = $this->util->obtainIpFromRequest($request);
|
||||
$ipAddress = $this->util->obtainIpFromRequest($request);
|
||||
|
||||
$where = [
|
||||
'requestTime>' => $requestTimeFrom->format('U'),
|
||||
'ipAddress' => $ip,
|
||||
'requestTime>' => $this->getTimeFrom($request, $failedAttemptsPeriod)->format('U'),
|
||||
'isDenied' => true,
|
||||
'ipAddress' => $ipAddress,
|
||||
];
|
||||
|
||||
$wasFailed = (bool) $this->entityManager
|
||||
@@ -105,12 +90,24 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
if ($failAttemptCount <= $maxFailedAttempts) {
|
||||
if ($failAttemptCount <= $this->configDataProvider->getMaxFailedAttemptNumber()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->log->warning("AUTH: Max failed login attempts exceeded for IP '$ip'.");
|
||||
throw new Forbidden("Max failed login attempts exceeded for IP address $ipAddress.");
|
||||
}
|
||||
|
||||
throw new Forbidden("Max failed login attempts exceeded.");
|
||||
private function getTimeFrom(Request $request, string $failedAttemptsPeriod): DateTime
|
||||
{
|
||||
$requestTime = intval($request->getServerParam('REQUEST_TIME_FLOAT'));
|
||||
|
||||
try {
|
||||
$requestTimeFrom = (new DateTime('@' . $requestTime))->modify('-' . $failedAttemptsPeriod);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
return $requestTimeFrom;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\Hook\Hooks;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
use Espo\Core\Authentication\ConfigDataProvider;
|
||||
use Espo\Core\Authentication\Hook\BeforeLogin;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Entities\AuthLogRecord;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class FailedCodeAttemptsLimit implements BeforeLogin
|
||||
{
|
||||
public function __construct(
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function process(AuthenticationData $data, Request $request): void
|
||||
{
|
||||
if (
|
||||
$request->getHeader('Espo-Authorization-Code') === null ||
|
||||
$this->configDataProvider->isAuthLogDisabled()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isByTokenOnly = !$data->getMethod() && $request->getHeader('Espo-Authorization-By-Token') === 'true';
|
||||
|
||||
if ($isByTokenOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
$failedAttemptsPeriod = $this->configDataProvider->getFailedCodeAttemptsPeriod();
|
||||
|
||||
$where = [
|
||||
'requestTime>' => $this->getTimeFrom($request, $failedAttemptsPeriod)->format('U'),
|
||||
'isDenied' => true,
|
||||
'username' => $data->getUsername(),
|
||||
'denialReason' => AuthLogRecord::DENIAL_REASON_WRONG_CODE,
|
||||
];
|
||||
|
||||
$wasFailed = (bool) $this->entityManager
|
||||
->getRDBRepository(AuthLogRecord::ENTITY_TYPE)
|
||||
->select(['id'])
|
||||
->where($where)
|
||||
->findOne();
|
||||
|
||||
if (!$wasFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$failAttemptCount = $this->entityManager
|
||||
->getRDBRepository(AuthLogRecord::ENTITY_TYPE)
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
if ($failAttemptCount <= $this->configDataProvider->getMaxFailedAttemptNumber()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$username = $data->getUsername() ?? '';
|
||||
|
||||
throw new Forbidden("Max failed 2FA login attempts exceeded for username '$username'.");
|
||||
}
|
||||
|
||||
private function getTimeFrom(Request $request, string $failedAttemptsPeriod): DateTime
|
||||
{
|
||||
$requestTime = intval($request->getServerParam('REQUEST_TIME_FLOAT'));
|
||||
|
||||
try {
|
||||
$requestTimeFrom = (new DateTime('@' . $requestTime))->modify('-' . $failedAttemptsPeriod);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
return $requestTimeFrom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\Hook\Hooks;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Util;
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
use Espo\Core\Authentication\ConfigDataProvider;
|
||||
use Espo\Core\Authentication\Hook\OnLogin;
|
||||
use Espo\Core\Authentication\Result;
|
||||
use Espo\Core\Authentication\Util\IpAddressUtil;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
class IpAddressWhitelist implements OnLogin
|
||||
{
|
||||
public function __construct(
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private Util $util,
|
||||
private Config $config,
|
||||
private IpAddressUtil $ipAddressUtil
|
||||
) {}
|
||||
|
||||
public function process(Result $result, AuthenticationData $data, Request $request): void
|
||||
{
|
||||
if (!$this->configDataProvider->ipAddressCheck()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ipAddress = $this->util->obtainIpFromRequest($request);
|
||||
|
||||
if (
|
||||
$ipAddress &&
|
||||
$this->ipAddressUtil->isInWhitelist($ipAddress, $this->configDataProvider->getIpAddressWhitelist())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $result->getUser();
|
||||
|
||||
if ($user && $user->isPortal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user && $user->isSuperAdmin() && $this->config->get('restrictedMode')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$user &&
|
||||
in_array($user->getId(), $this->configDataProvider->getIpAddressCheckExcludedUserIdList())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$username = $user ? $user->getUserName() : '?';
|
||||
|
||||
throw new Forbidden("Not allowed IP address $ipAddress, user: $username.");
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
namespace Espo\Core\Authentication\Hook;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ServiceUnavailable;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
@@ -41,6 +43,10 @@ class Manager
|
||||
public function __construct(private Metadata $metadata, private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @throws ServiceUnavailable
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processBeforeLogin(AuthenticationData $data, Request $request): void
|
||||
{
|
||||
foreach ($this->getBeforeLoginHookList() as $hook) {
|
||||
@@ -48,6 +54,16 @@ class Manager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processOnLogin(Result $result, AuthenticationData $data, Request $request): void
|
||||
{
|
||||
foreach ($this->getOnLoginHookList() as $hook) {
|
||||
$hook->process($result, $data, $request);
|
||||
}
|
||||
}
|
||||
|
||||
public function processOnFail(Result $result, AuthenticationData $data, Request $request): void
|
||||
{
|
||||
foreach ($this->getOnFailHookList() as $hook) {
|
||||
@@ -102,6 +118,21 @@ class Manager
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OnLogin[]
|
||||
*/
|
||||
private function getOnLoginHookList(): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($this->getHookClassNameList('onLogin') as $className) {
|
||||
/** @var class-string<OnLogin> $className */
|
||||
$list[] = $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OnResult[]
|
||||
*/
|
||||
|
||||
46
application/Espo/Core/Authentication/Hook/OnLogin.php
Normal file
46
application/Espo/Core/Authentication/Hook/OnLogin.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\Hook;
|
||||
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Authentication\Result;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
/**
|
||||
* Once a login result is ready.
|
||||
*/
|
||||
interface OnLogin
|
||||
{
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function process(Result $result, AuthenticationData $data, Request $request): void;
|
||||
}
|
||||
@@ -132,9 +132,8 @@ class LdapLogin implements Login
|
||||
if ($user) {
|
||||
return Result::success($user);
|
||||
}
|
||||
else {
|
||||
return Result::fail(FailReason::WRONG_CREDENTIALS);
|
||||
}
|
||||
|
||||
return Result::fail(FailReason::WRONG_CREDENTIALS);
|
||||
}
|
||||
|
||||
if (!$password || $username == '**logout') {
|
||||
@@ -158,9 +157,10 @@ class LdapLogin implements Login
|
||||
catch (Exception $e) {
|
||||
$options = $this->utils->getLdapClientOptions();
|
||||
|
||||
$this->log->error(
|
||||
'LDAP: Could not connect to LDAP server [' . $options['host'] . '], details: ' . $e->getMessage()
|
||||
);
|
||||
$this->log->error("LDAP: Could not connect to LDAP server host. {message}", [
|
||||
'host' => $options['host'],
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
|
||||
/** @var string $username */
|
||||
|
||||
@@ -170,7 +170,9 @@ class LdapLogin implements Login
|
||||
return Result::fail();
|
||||
}
|
||||
|
||||
$this->log->info('LDAP: Administrator [' . $username . '] was logged in by Espo method.');
|
||||
$this->log->info("LDAP: Administrator '{username}' was logged in by Espo method.", [
|
||||
'username' => $username,
|
||||
]);
|
||||
}
|
||||
|
||||
$userDn = null;
|
||||
@@ -182,15 +184,16 @@ class LdapLogin implements Login
|
||||
$userDn = $this->findLdapUserDnByUsername($username);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->log->error(
|
||||
'Error while finding DN for [' . $username . '], details: ' . $e->getMessage() . '.'
|
||||
);
|
||||
$this->log->error("Error while finding DN for '{username}'. {message}", [
|
||||
'username' => $username,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!isset($userDn)) {
|
||||
$this->log->error(
|
||||
'LDAP: Authentication failed for user [' . $username . '], details: user is not found.'
|
||||
);
|
||||
$this->log->error("LDAP: Authentication failed for '{username}'; user is not found.", [
|
||||
'username' => $username,
|
||||
]);
|
||||
|
||||
$adminUser = $this->adminLogin($username, $password);
|
||||
|
||||
@@ -198,18 +201,24 @@ class LdapLogin implements Login
|
||||
return Result::fail();
|
||||
}
|
||||
|
||||
$this->log->info('LDAP: Administrator [' . $username . '] was logged in by Espo method.');
|
||||
$this->log->info("LDAP: Administrator '{username}' was logged in by Espo method.", [
|
||||
'username' => $username,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->log->debug('User [' . $username . '] is found with this DN ['.$userDn.'].');
|
||||
$this->log->debug("User '{username}' with DN '{dn}' is found .", [
|
||||
'username' => $username,
|
||||
'dn' => $userDn,
|
||||
]);
|
||||
|
||||
try {
|
||||
$ldapClient->bind($userDn, $password);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->log->error(
|
||||
'LDAP: Authentication failed for user [' . $username . '], details: ' . $e->getMessage()
|
||||
);
|
||||
$this->log->error("LDAP: Authentication failed for '{username}'. {message}", [
|
||||
'username' => $username,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return Result::fail();
|
||||
}
|
||||
@@ -229,9 +238,9 @@ class LdapLogin implements Login
|
||||
|
||||
if (!isset($user)) {
|
||||
if (!$this->utils->getOption('createEspoUser')) {
|
||||
$this->log->warning(
|
||||
"LDAP: Authentication success for user $username, but user is not created in EspoCRM."
|
||||
);
|
||||
$this->log->warning("LDAP: '{username}' authenticated, but user is not created in Espo.", [
|
||||
'username' => $username,
|
||||
]);
|
||||
|
||||
return Result::fail(FailReason::USER_NOT_FOUND);
|
||||
}
|
||||
@@ -259,7 +268,7 @@ class LdapLogin implements Login
|
||||
$this->client = $this->clientFactory->create($options);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->log->error('LDAP error: ' . $e->getMessage());
|
||||
$this->log->error("LDAP error. {message}", ['message' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +299,10 @@ class LdapLogin implements Login
|
||||
if (strtolower($username) !== strtolower($tokenUsername)) {
|
||||
$ip = $this->util->obtainIpFromRequest($request);
|
||||
|
||||
$this->log->alert('Unauthorized access attempt for user [' . $username . '] from IP [' . $ip . ']');
|
||||
$this->log->alert("Unauthorized access attempt for user '{username}' from IP '{ip}'.", [
|
||||
'username' => $username,
|
||||
'ip' => $ip,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -325,11 +337,11 @@ class LdapLogin implements Login
|
||||
*/
|
||||
private function createUser(array $userData, bool $isPortal = false): ?User
|
||||
{
|
||||
$this->log->info('Creating new user...');
|
||||
$this->log->info("LDAP: Creating new user.");
|
||||
|
||||
$data = [];
|
||||
|
||||
$this->log->debug('LDAP: user data: ' . print_r($userData, true));
|
||||
$this->log->debug("LDAP: user data: {userData}", ['userData' => print_r($userData, true)]);
|
||||
|
||||
$ldapFields = $this->loadFields('ldap');
|
||||
|
||||
@@ -337,7 +349,10 @@ class LdapLogin implements Login
|
||||
$ldap = strtolower($ldap);
|
||||
|
||||
if (isset($userData[$ldap][0])) {
|
||||
$this->log->debug('LDAP: Create a user with [' . $espo . '] = [' . $userData[$ldap][0] . '].');
|
||||
$this->log->debug("LDAP: Create a user with [{user1}] = [{user2}].", [
|
||||
'user1' => $espo,
|
||||
'user2' => $userData[$ldap][0],
|
||||
]);
|
||||
|
||||
$data[$espo] = $userData[$ldap][0];
|
||||
}
|
||||
@@ -410,7 +425,7 @@ class LdapLogin implements Login
|
||||
/** @noinspection PhpRedundantOptionalArgumentInspection */
|
||||
$result = $ldapClient->search($searchString, null, Ldap::SEARCH_SCOPE_SUB);
|
||||
|
||||
$this->log->debug('LDAP: user search string: "' . $searchString . '"');
|
||||
$this->log->debug("LDAP: user search string: {string}.", ['string' => $searchString]);
|
||||
|
||||
foreach ($result as $item) {
|
||||
return $item["dn"];
|
||||
|
||||
@@ -43,7 +43,7 @@ use Espo\ORM\EntityManager;
|
||||
/**
|
||||
* Compatible only with default Espo auth tokens.
|
||||
*
|
||||
* @todo Use a token-sessionId map to retrieve tokens.
|
||||
* @todo Use a token-sessionId map to retrieve tokens. Send sid claim in id_token.
|
||||
*/
|
||||
class BackchannelLogout
|
||||
{
|
||||
|
||||
@@ -214,7 +214,7 @@ class ConfigDataProvider
|
||||
|
||||
public function getAuthorizationPrompt(): string
|
||||
{
|
||||
return $this->config->get('oidcAuthorizationPrompt') ?? 'consent';
|
||||
return $this->object->get('oidcAuthorizationPrompt') ?? 'consent';
|
||||
}
|
||||
|
||||
public function getAuthorizationMaxAge(): ?int
|
||||
|
||||
@@ -152,7 +152,7 @@ class Login implements LoginInterface
|
||||
return Result::fail(FailReason::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
return Result::success($user);
|
||||
return Result::success($user)->withBypassSecondStep();
|
||||
}
|
||||
|
||||
private function loginFallback(Data $data, Request $request): Result
|
||||
|
||||
@@ -51,6 +51,10 @@ class DefaultUserProvider implements UserProvider
|
||||
{
|
||||
$user = $this->findUser($payload);
|
||||
|
||||
if ($user === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
$this->syncUser($user, $payload);
|
||||
|
||||
@@ -60,7 +64,10 @@ class DefaultUserProvider implements UserProvider
|
||||
return $this->tryToCreateUser($payload);
|
||||
}
|
||||
|
||||
private function findUser(Payload $payload): ?User
|
||||
/**
|
||||
* @return User|false|null
|
||||
*/
|
||||
private function findUser(Payload $payload): User|bool|null
|
||||
{
|
||||
$usernameClaim = $this->configDataProvider->getUsernameClaim();
|
||||
|
||||
@@ -82,24 +89,26 @@ class DefaultUserProvider implements UserProvider
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$user->isActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userId = $user->getId();
|
||||
|
||||
if (!$user->isActive()) {
|
||||
$this->log->info("Oidc: User $userId found but it's not active.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$isPortal = $this->applicationState->isPortal();
|
||||
|
||||
if (!$isPortal && !$user->isRegular() && !$user->isAdmin()) {
|
||||
$this->log->info("Oidc: User $userId found but it's neither regular user not admin.");
|
||||
$this->log->info("Oidc: User $userId found but it's neither regular user nor admin.");
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($isPortal && !$user->isPortal()) {
|
||||
$this->log->info("Oidc: User $userId found but it's not portal user.");
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($isPortal) {
|
||||
@@ -108,20 +117,20 @@ class DefaultUserProvider implements UserProvider
|
||||
if (!$user->getPortals()->hasId($portalId)) {
|
||||
$this->log->info("Oidc: User $userId found but it's not related to current portal.");
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->isSuperAdmin()) {
|
||||
$this->log->info("Oidc: User $userId found but it's super-admin, not allowed.");
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($user->isAdmin() && !$this->configDataProvider->allowAdminUser()) {
|
||||
$this->log->info("Oidc: User $userId found but it's admin, not allowed.");
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user;
|
||||
|
||||
@@ -51,6 +51,7 @@ class Result
|
||||
private ?string $token = null;
|
||||
private ?string $view = null;
|
||||
private ?string $failReason = null;
|
||||
private bool $bypassSecondStep = false;
|
||||
private ?Data $data;
|
||||
|
||||
private function __construct(string $status, ?User $user = null, ?Data $data = null)
|
||||
@@ -105,13 +106,23 @@ class Result
|
||||
}
|
||||
|
||||
/**
|
||||
* Second step is required. E.g. for 2FA.
|
||||
* The second step is required.
|
||||
*/
|
||||
public function isSecondStepRequired(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_SECOND_STEP_REQUIRED;
|
||||
}
|
||||
|
||||
/**
|
||||
* To bypass the second step.
|
||||
*
|
||||
* @since 8.4.0
|
||||
*/
|
||||
public function bypassSecondStep(): bool
|
||||
{
|
||||
return $this->bypassSecondStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login is failed.
|
||||
*/
|
||||
@@ -183,4 +194,17 @@ class Result
|
||||
{
|
||||
return $this->failReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone with bypass second step.
|
||||
*
|
||||
* @since 8.4.0
|
||||
*/
|
||||
public function withBypassSecondStep(bool $bypassSecondStep = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->bypassSecondStep = $bypassSecondStep;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Authentication\TwoFactor\Email;
|
||||
|
||||
use Espo\Core\Authentication\HeaderKey;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Mail\Exceptions\SendingError;
|
||||
use Espo\Core\Utils\Log;
|
||||
@@ -56,7 +57,7 @@ class EmailLogin implements Login
|
||||
|
||||
public function login(Result $result, Request $request): Result
|
||||
{
|
||||
$code = $request->getHeader('Espo-Authorization-Code');
|
||||
$code = $request->getHeader(HeaderKey::AUTHORIZATION_CODE);
|
||||
|
||||
$user = $result->getUser();
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Authentication\TwoFactor\Sms;
|
||||
|
||||
use Espo\Core\Authentication\HeaderKey;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\ORM\EntityManager;
|
||||
@@ -55,7 +56,7 @@ class SmsLogin implements Login
|
||||
|
||||
public function login(Result $result, Request $request): Result
|
||||
{
|
||||
$code = $request->getHeader('Espo-Authorization-Code');
|
||||
$code = $request->getHeader(HeaderKey::AUTHORIZATION_CODE);
|
||||
|
||||
$user = $result->getUser();
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Authentication\TwoFactor\Totp;
|
||||
|
||||
use Espo\Core\Authentication\HeaderKey;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Entities\UserData;
|
||||
@@ -55,7 +56,7 @@ class TotpLogin implements Login
|
||||
|
||||
public function login(Result $result, Request $request): Result
|
||||
{
|
||||
$code = $request->getHeader('Espo-Authorization-Code');
|
||||
$code = $request->getHeader(HeaderKey::AUTHORIZATION_CODE);
|
||||
|
||||
$user = $result->getUser();
|
||||
|
||||
|
||||
59
application/Espo/Core/Authentication/Util/IpAddressUtil.php
Normal file
59
application/Espo/Core/Authentication/Util/IpAddressUtil.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\Util;
|
||||
|
||||
use CIDRmatch\CIDRmatch;
|
||||
|
||||
class IpAddressUtil
|
||||
{
|
||||
/**
|
||||
* @param string $ipAddress An IP address.
|
||||
* @param string[] $whitelist A whitelist. IPs or IP ranges in CIDR notation.
|
||||
*/
|
||||
public function isInWhitelist(string $ipAddress, array $whitelist): bool
|
||||
{
|
||||
$cidrMatch = new CIDRmatch();
|
||||
|
||||
foreach ($whitelist as $whiteIpAddress) {
|
||||
if ($ipAddress === $whiteIpAddress) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($whiteIpAddress, '/') &&
|
||||
$cidrMatch->match($ipAddress, $whiteIpAddress)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
namespace Espo\Core\Console;
|
||||
|
||||
use Espo\Core\ApplicationUser;
|
||||
use Espo\Core\Console\Exceptions\InvalidArgument;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Util;
|
||||
@@ -44,12 +46,14 @@ class CommandManager
|
||||
private const DEFAULT_COMMAND = 'Help';
|
||||
private const DEFAULT_COMMAND_FLAG = 'help';
|
||||
|
||||
public function __construct(private InjectableFactory $injectableFactory, private Metadata $metadata)
|
||||
{}
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Metadata $metadata,
|
||||
private ApplicationUser $applicationUser
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $argv
|
||||
*
|
||||
* @return int<0, 255> Exit-status.
|
||||
*/
|
||||
public function run(array $argv): int
|
||||
@@ -73,8 +77,12 @@ class CommandManager
|
||||
throw new CommandNotSpecified("Command name is not specified.");
|
||||
}
|
||||
|
||||
$this->checkParams($command, $params);
|
||||
|
||||
$io = new IO();
|
||||
|
||||
$this->setupUser($command);
|
||||
|
||||
$commandObj = $this->createCommand($command);
|
||||
|
||||
if (!$commandObj instanceof Command) {
|
||||
@@ -146,4 +154,59 @@ class CommandManager
|
||||
{
|
||||
return Params::fromArgs(array_slice($argv, 1));
|
||||
}
|
||||
|
||||
private function setupUser(string $command): void
|
||||
{
|
||||
$noSystemUser = $this->metadata->get(['app', 'consoleCommands', lcfirst($command), 'noSystemUser']);
|
||||
|
||||
if ($noSystemUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applicationUser->setupSystemUser();
|
||||
}
|
||||
|
||||
private function checkParams(string $command, Params $params): void
|
||||
{
|
||||
$this->checkOptions($command, $params);
|
||||
$this->checkFlags($command, $params);
|
||||
}
|
||||
|
||||
private function checkOptions(string $command, Params $params): void
|
||||
{
|
||||
$allowedOptions = $this->metadata->get(['app', 'consoleCommands', lcfirst($command), 'allowedOptions']);
|
||||
|
||||
if (!is_array($allowedOptions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notAllowedOptions = array_diff(array_keys($params->getOptions()), $allowedOptions);
|
||||
|
||||
if ($notAllowedOptions === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$msg = sprintf("Not allowed options: %s.", implode(', ', $notAllowedOptions));
|
||||
|
||||
throw new InvalidArgument($msg);
|
||||
}
|
||||
|
||||
private function checkFlags(string $command, Params $params): void
|
||||
{
|
||||
$allowedFlags = $this->metadata->get(['app', 'consoleCommands', lcfirst($command), 'allowedFlags']);
|
||||
|
||||
if (!is_array($allowedFlags)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notAllowedFlags = array_diff($params->getFlagList(), $allowedFlags);
|
||||
|
||||
if ($notAllowedFlags === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$msg = sprintf("Not allowed flags: %s.", implode(', ', $notAllowedFlags));
|
||||
|
||||
throw new InvalidArgument($msg);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user