mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-04 03:07:01 +00:00
Compare commits
549 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b0787474e | ||
|
|
8c87f20374 | ||
|
|
ba35115a48 | ||
|
|
069010d0fe | ||
|
|
fe5878fd99 | ||
|
|
839ceea142 | ||
|
|
9cce9d7347 | ||
|
|
69d0dbbf1c | ||
|
|
68ef9ce4ac | ||
|
|
163cf047e5 | ||
|
|
cc574afd3d | ||
|
|
77d76fe0ee | ||
|
|
e7ab75ec5a | ||
|
|
ff6983c9f3 | ||
|
|
e2673473c5 | ||
|
|
e8bf70ab8e | ||
|
|
f459d5811d | ||
|
|
53bf9b024a | ||
|
|
25aed1a1c4 | ||
|
|
2589801993 | ||
|
|
5f954c22da | ||
|
|
423e2ca544 | ||
|
|
6176b8770f | ||
|
|
8ab7452859 | ||
|
|
07035bf8bf | ||
|
|
9e8df41174 | ||
|
|
64736349f0 | ||
|
|
71f389703d | ||
|
|
02917943b6 | ||
|
|
acf1833d9f | ||
|
|
9c45902213 | ||
|
|
f9674bd60c | ||
|
|
831d840cc5 | ||
|
|
3f6f718cd9 | ||
|
|
c35b5c5aa5 | ||
|
|
0050f44a8a | ||
|
|
f86479afdc | ||
|
|
f4cbb5e56a | ||
|
|
6c8e9b129a | ||
|
|
c033cb171e | ||
|
|
bdf7d56e0c | ||
|
|
3e7326f605 | ||
|
|
61e51bfb31 | ||
|
|
c35d934ba7 | ||
|
|
80fa391daa | ||
|
|
1092b17a13 | ||
|
|
1de6568918 | ||
|
|
ee7c7046ac | ||
|
|
19638dd649 | ||
|
|
df30678484 | ||
|
|
b05697d874 | ||
|
|
91177fc0d2 | ||
|
|
e56c64dba4 | ||
|
|
7ecc0dc6a9 | ||
|
|
808e2f8788 | ||
|
|
7682539114 | ||
|
|
0558739e67 | ||
|
|
552983ca33 | ||
|
|
d328872479 | ||
|
|
6cee542972 | ||
|
|
afb77424d4 | ||
|
|
c7465970bd | ||
|
|
4f6ae321b9 | ||
|
|
b92970992f | ||
|
|
4923984e2d | ||
|
|
071fb20f7b | ||
|
|
3e7d0f23c8 | ||
|
|
268009e5f6 | ||
|
|
573d810d37 | ||
|
|
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 | ||
|
|
b741898ce9 | ||
|
|
b89c28fe96 | ||
|
|
1c68c805aa | ||
|
|
cef7a919b8 | ||
|
|
3117db022f | ||
|
|
7b642e40b0 | ||
|
|
89ff3dcf15 | ||
|
|
79c8d25a80 | ||
|
|
58d926be82 | ||
|
|
3044f83690 | ||
|
|
f2d5b2685e | ||
|
|
17abe18d01 | ||
|
|
09880ff8f1 | ||
|
|
23f4686577 |
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, consider creating feature requests on the forum. For low-level (framework) – here on GitHub.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,12 +65,12 @@ class MassDelete implements MassAction
|
||||
$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 = [
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -97,7 +97,7 @@ class Main implements AdditionalApplier
|
||||
];
|
||||
|
||||
foreach ($itemList as $item) {
|
||||
$queryBuilder->select('emailUser.' . $item, $item);
|
||||
$queryBuilder->select(Email::ALIAS_INBOX . '.' . $item, $item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +69,9 @@ class InFolder implements ItemConverter
|
||||
$this->joinEmailUser($queryBuilder);
|
||||
|
||||
$whereClause = [
|
||||
'emailUser.inTrash' => false,
|
||||
'emailUser.folderId' => null,
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
Email::ALIAS_INBOX . '.inTrash' => false,
|
||||
Email::ALIAS_INBOX . '.folderId' => null,
|
||||
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
|
||||
[
|
||||
'status' => [
|
||||
Email::STATUS_ARCHIVED,
|
||||
@@ -118,7 +118,7 @@ class InFolder implements ItemConverter
|
||||
[
|
||||
'status!=' => Email::STATUS_DRAFT,
|
||||
],
|
||||
'emailUser.inTrash' => false,
|
||||
Email::ALIAS_INBOX . '.inTrash' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -127,8 +127,8 @@ class InFolder implements ItemConverter
|
||||
$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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -137,8 +137,8 @@ class InFolder implements ItemConverter
|
||||
$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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -164,15 +164,15 @@ 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,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return WhereClause::fromRaw([
|
||||
'emailUser.inTrash' => false,
|
||||
'emailUser.folderId' => $folderId,
|
||||
Email::ALIAS_INBOX . '.inTrash' => 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -195,8 +195,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 +241,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;
|
||||
}
|
||||
@@ -314,6 +320,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)
|
||||
{}
|
||||
|
||||
@@ -95,6 +95,7 @@ class Authentication
|
||||
* Process logging in.
|
||||
*
|
||||
* @throws ServiceUnavailable
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function login(AuthenticationData $data, Request $request, Response $response): Result
|
||||
{
|
||||
@@ -107,16 +108,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);
|
||||
}
|
||||
@@ -157,7 +163,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 +221,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);
|
||||
|
||||
@@ -241,10 +241,16 @@ 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)
|
||||
@@ -344,13 +350,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 +367,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 +377,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 +387,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 +397,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 +413,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 +784,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,4 +147,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') ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ 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 +50,6 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
public function __construct(
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private EntityManager $entityManager,
|
||||
private Log $log,
|
||||
private Util $util
|
||||
) {}
|
||||
|
||||
@@ -70,6 +68,8 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
return;
|
||||
}
|
||||
|
||||
$isSecondStep = $request->getHeader('Espo-Authorization-Code') !== null;
|
||||
|
||||
$failedAttemptsPeriod = $this->configDataProvider->getFailedAttemptsPeriod();
|
||||
$maxFailedAttempts = $this->configDataProvider->getMaxFailedAttemptNumber();
|
||||
|
||||
@@ -82,14 +82,21 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$ip = $this->util->obtainIpFromRequest($request);
|
||||
$ipAddress = $this->util->obtainIpFromRequest($request);
|
||||
|
||||
$where = [
|
||||
'requestTime>' => $requestTimeFrom->format('U'),
|
||||
'ipAddress' => $ip,
|
||||
'isDenied' => true,
|
||||
];
|
||||
|
||||
if ($isSecondStep) {
|
||||
$where['username'] = $data->getUsername();
|
||||
}
|
||||
|
||||
if (!$isSecondStep) {
|
||||
$where['ipAddress'] = $ipAddress;
|
||||
}
|
||||
|
||||
$wasFailed = (bool) $this->entityManager
|
||||
->getRDBRepository(AuthLogRecord::ENTITY_TYPE)
|
||||
->select(['id'])
|
||||
@@ -109,8 +116,12 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
return;
|
||||
}
|
||||
|
||||
$this->log->warning("AUTH: Max failed login attempts exceeded for IP '$ip'.");
|
||||
if ($isSecondStep) {
|
||||
$username = $data->getUsername() ?? '';
|
||||
|
||||
throw new Forbidden("Max failed login attempts exceeded.");
|
||||
throw new Forbidden("Max failed 2FA login attempts exceeded for username '$username'.");
|
||||
}
|
||||
|
||||
throw new Forbidden("Max failed login attempts exceeded for IP address $ipAddress.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
54
application/Espo/Core/Console/Commands/Migrate.php
Normal file
54
application/Espo/Core/Console/Commands/Migrate.php
Normal file
@@ -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\Core\Console\Commands;
|
||||
|
||||
use Espo\Core\Console\Command;
|
||||
use Espo\Core\Console\Command\Params;
|
||||
use Espo\Core\Console\IO;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Upgrades\Migration\Runner;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class Migrate implements Command
|
||||
{
|
||||
public function __construct(
|
||||
private Runner $runner
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function run(Params $params, IO $io): void
|
||||
{
|
||||
$this->runner->run($io);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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\Console\Commands;
|
||||
|
||||
use Espo\Core\Console\Command;
|
||||
use Espo\Core\Console\Command\Params;
|
||||
use Espo\Core\Console\IO;
|
||||
use Espo\Core\Upgrades\Migration\AfterUpgradeRunner;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class MigrationVersionStep implements Command
|
||||
{
|
||||
public function __construct(
|
||||
private AfterUpgradeRunner $afterUpgradeRunner
|
||||
) {}
|
||||
|
||||
public function run(Params $params, IO $io): void
|
||||
{
|
||||
$step = $params->getOption('step');
|
||||
|
||||
if (!$step) {
|
||||
throw new RuntimeException("No step parameter.");
|
||||
}
|
||||
|
||||
$this->afterUpgradeRunner->run($step);
|
||||
|
||||
$io->writeLine("Done.");
|
||||
$io->setExitStatus(0);
|
||||
}
|
||||
}
|
||||
@@ -63,13 +63,7 @@ class UpgradeStep implements Command
|
||||
$stepName = $options['step'];
|
||||
$upgradeId = $options['id'];
|
||||
|
||||
$result = $this->runUpgradeStep($stepName, ['id' => $upgradeId]);
|
||||
|
||||
if (!$result) {
|
||||
echo "false";
|
||||
|
||||
return;
|
||||
}
|
||||
$this->runUpgradeStep($stepName, ['id' => $upgradeId]);
|
||||
|
||||
echo "true";
|
||||
}
|
||||
@@ -77,7 +71,7 @@ class UpgradeStep implements Command
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
private function runUpgradeStep(string $stepName, array $params): bool
|
||||
private function runUpgradeStep(string $stepName, array $params): void
|
||||
{
|
||||
$app = new Application();
|
||||
|
||||
@@ -86,12 +80,10 @@ class UpgradeStep implements Command
|
||||
$upgradeManager = new UpgradeManager($app->getContainer());
|
||||
|
||||
try {
|
||||
$result = $upgradeManager->runInstallStep($stepName, $params);
|
||||
$upgradeManager->runInstallStep($stepName, $params);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
die("Error: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,9 @@ class Container implements ContainerInterface
|
||||
return $loader;
|
||||
}
|
||||
|
||||
assert($this->configuration !== null);
|
||||
if ($this->configuration === null) {
|
||||
throw new RuntimeException("Container configuration is not ready.");
|
||||
}
|
||||
|
||||
return $this->configuration->getLoaderClassName($id);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ use Espo\Core\Binding\BindingContainer;
|
||||
use Espo\Core\Binding\BindingLoader;
|
||||
use Espo\Core\Binding\EspoBindingLoader;
|
||||
|
||||
use Espo\Core\Loaders\ApplicationState as ApplicationStateLoader;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Core\Utils\Config\ConfigFileManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
@@ -71,6 +72,7 @@ class ContainerBuilder
|
||||
'log' => LogLoader::class,
|
||||
'dataManager' => DataManagerLoader::class,
|
||||
'metadata' => MetadataLoader::class,
|
||||
'applicationState' => ApplicationStateLoader::class,
|
||||
];
|
||||
|
||||
public function withBindingLoader(BindingLoader $bindingLoader): self
|
||||
|
||||
@@ -37,6 +37,16 @@ class ConfigDataProvider
|
||||
public function __construct(private Config $config)
|
||||
{}
|
||||
|
||||
/**
|
||||
* Get decimal places.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*/
|
||||
public function getDecimalPlaces(): ?int
|
||||
{
|
||||
return $this->config->get('currencyDecimalPlaces');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a system default currency.
|
||||
*/
|
||||
|
||||
@@ -220,6 +220,7 @@ class Starter
|
||||
|
||||
$clientManager->setBasePath($this->clientManager->getBasePath());
|
||||
$clientManager->setApiUrl('api/v1/portal-access/' . $portalId);
|
||||
$clientManager->setApplicationId($portalId);
|
||||
|
||||
$params = RunnerParams::fromArray([
|
||||
'entryPoint' => $entryPoint,
|
||||
|
||||
@@ -41,20 +41,13 @@ use Espo\ORM\Defs as OrmDefs;
|
||||
*/
|
||||
class NotJoinedLoader implements LoaderInterface
|
||||
{
|
||||
private OrmDefs $ormDefs;
|
||||
/** @var array<string, string[]> */
|
||||
private array $fieldListCacheMap = [];
|
||||
|
||||
private EntityManager $entityManager;
|
||||
|
||||
/**
|
||||
* @var array<string, string[]>
|
||||
*/
|
||||
private $fieldListCacheMap = [];
|
||||
|
||||
public function __construct(OrmDefs $ormDefs, EntityManager $entityManager)
|
||||
{
|
||||
$this->ormDefs = $ormDefs;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
public function __construct(
|
||||
private OrmDefs $ormDefs,
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
@@ -92,12 +85,25 @@ class NotJoinedLoader implements LoaderInterface
|
||||
->findOne();
|
||||
|
||||
if (!$foreignEntity) {
|
||||
/** @noinspection PhpRedundantOptionalArgumentInspection */
|
||||
$entity->set($nameAttribute, null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->set($nameAttribute, $foreignEntity->get('name'));
|
||||
$name = $foreignEntity->get('name');
|
||||
|
||||
if ($name === null) {
|
||||
$foreignEntity = $this->entityManager
|
||||
->getRDBRepository($foreignEntityType)
|
||||
->getById($id);
|
||||
|
||||
if ($foreignEntity) {
|
||||
$name = $foreignEntity->get('name');
|
||||
}
|
||||
}
|
||||
|
||||
$entity->set($nameAttribute, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,9 +124,10 @@ class NotJoinedLoader implements LoaderInterface
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$relationDefs->getParam('noJoin')) {
|
||||
// Commented to load name of leads w/o person name.
|
||||
/*if (!$relationDefs->getParam('noJoin')) {
|
||||
continue;
|
||||
}
|
||||
}*/
|
||||
|
||||
if (!$relationDefs->hasForeignEntityType()) {
|
||||
continue;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core\FieldProcessing\LinkParent;
|
||||
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\FieldProcessing\Loader as LoaderInterface;
|
||||
@@ -72,18 +73,12 @@ class Loader implements LoaderInterface
|
||||
$entityDefs = $this->ormDefs->getEntity($entityType);
|
||||
|
||||
foreach ($entityDefs->getFieldList() as $fieldDefs) {
|
||||
if ($fieldDefs->getType() !== 'linkParent') {
|
||||
if ($fieldDefs->getType() !== FieldType::LINK_PARENT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = $fieldDefs->getName();
|
||||
|
||||
if (!$entityDefs->hasRelation($fieldDefs->getName())) {
|
||||
// Otherwise, loadParentNameField produces an error.
|
||||
// @todo Revise.
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = $name;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,10 +29,9 @@
|
||||
|
||||
namespace Espo\Core\FieldProcessing\Reminder;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Reminder;
|
||||
use Espo\ORM\Collection;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\FieldProcessing\Loader as LoaderInterface;
|
||||
use Espo\Core\FieldProcessing\Loader\Params;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
@@ -43,8 +42,10 @@ use Espo\Core\ORM\EntityManager;
|
||||
*/
|
||||
class Loader implements LoaderInterface
|
||||
{
|
||||
public function __construct(private EntityManager $entityManager)
|
||||
{}
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
@@ -65,19 +66,20 @@ class Loader implements LoaderInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \stdClass[]
|
||||
* @return object{seconds: int, type: string}[]
|
||||
*/
|
||||
private function fetchReminderDataList(Entity $entity): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
/** @var Collection<Reminder> $collection */
|
||||
/** @var iterable<Reminder> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository(Reminder::ENTITY_TYPE)
|
||||
->select(['seconds', 'type'])
|
||||
->where([
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'entityId' => $entity->getId(),
|
||||
'userId' => $this->user->getId(),
|
||||
])
|
||||
->distinct()
|
||||
->order('seconds')
|
||||
|
||||
@@ -29,9 +29,13 @@
|
||||
|
||||
namespace Espo\Core\FieldProcessing\Reminder;
|
||||
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Utils\Id\RecordIdGenerator;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\Preferences;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Reminder;
|
||||
use Espo\Modules\Crm\Entities\Task;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\FieldProcessing\Saver as SaverInterface;
|
||||
@@ -39,8 +43,6 @@ use Espo\Core\FieldProcessing\Saver\Params;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
|
||||
use stdClass;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* @internal This class should not be removed as it's used by custom entities.
|
||||
@@ -53,177 +55,377 @@ class Saver implements SaverInterface
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private RecordIdGenerator $idGenerator
|
||||
private RecordIdGenerator $idGenerator,
|
||||
private User $user,
|
||||
private Metadata $metadata
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param CoreEntity $entity
|
||||
*/
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$hasReminder = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->hasField('reminders');
|
||||
|
||||
if (!$hasReminder) {
|
||||
if (!$this->hasRemindersField($entityType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateAttribute = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->getField('reminders')
|
||||
->getParam('dateField') ??
|
||||
$this->dateAttribute;
|
||||
$dateAttribute = $this->getDateAttribute($entityType);
|
||||
|
||||
$toProcess =
|
||||
$entity->isNew() ||
|
||||
$entity->isAttributeChanged('assignedUserId') ||
|
||||
($entity->hasLinkMultipleField('assignedUsers') && $entity->isAttributeChanged('assignedUsersIds')) ||
|
||||
($entity->hasLinkMultipleField('users') && $entity->isAttributeChanged('usersIds')) ||
|
||||
$entity->isAttributeChanged($dateAttribute) ||
|
||||
$entity->has('reminders');
|
||||
if ($this->toRemove($entity)) {
|
||||
$this->deleteAll($entity);
|
||||
|
||||
if (!$toProcess) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reminderTypeList = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity(Reminder::ENTITY_TYPE)
|
||||
->getField('type')
|
||||
->getParam('options') ?? [];
|
||||
|
||||
$reminderList = $entity->has('reminders') ?
|
||||
$entity->get('reminders') :
|
||||
$this->getEntityReminderDataList($entity);
|
||||
|
||||
if (!$entity->isNew()) {
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from(Reminder::ENTITY_TYPE)
|
||||
->where([
|
||||
'entityId' => $entity->getId(),
|
||||
'entityType' => $entityType,
|
||||
'deleted' => false,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($query);
|
||||
}
|
||||
|
||||
if (empty($reminderList)) {
|
||||
if (!$this->toProcess($entity, $dateAttribute)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateValue = $entity->get($dateAttribute);
|
||||
$typeList = $this->getTypeList();
|
||||
|
||||
if (!$entity->has($dateAttribute)) {
|
||||
$reloadedEntity = $this->entityManager->getEntity($entityType, $entity->getId());
|
||||
$onlyRemindersFieldChanged = $this->onlyRemindersFieldChanged($entity, $dateAttribute);
|
||||
|
||||
if ($reloadedEntity) {
|
||||
$dateValue = $reloadedEntity->get($dateAttribute);
|
||||
}
|
||||
if (!$entity->isNew() && !$onlyRemindersFieldChanged) {
|
||||
$this->deleteAll($entity);
|
||||
}
|
||||
|
||||
if (!$dateValue) {
|
||||
if (!$entity->isNew() && $onlyRemindersFieldChanged) {
|
||||
$this->deleteAllForUser($entity);
|
||||
}
|
||||
|
||||
$startString = $this->getStartString($entity, $dateAttribute);
|
||||
|
||||
if (!$startString) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entity->hasLinkMultipleField('users')) {
|
||||
$userIdList = $entity->getLinkMultipleIdList('users');
|
||||
}
|
||||
else if ($entity->hasLinkMultipleField('assignedUsers')) {
|
||||
$userIdList = $entity->getLinkMultipleIdList('assignedUsers');
|
||||
}
|
||||
else {
|
||||
$userIdList = [];
|
||||
$userIdList = $this->getUserIdList($entity);
|
||||
|
||||
if ($entity->get('assignedUserId')) {
|
||||
$userIdList[] = $entity->get('assignedUserId');
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($userIdList)) {
|
||||
if ($userIdList === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateValueObj = new DateTime($dateValue);
|
||||
if ($onlyRemindersFieldChanged && in_array($this->user->getId(), $userIdList)) {
|
||||
$userIdList = [$this->user->getId()];
|
||||
}
|
||||
|
||||
foreach ($reminderList as $item) {
|
||||
$remindAt = clone $dateValueObj;
|
||||
$seconds = intval($item->seconds);
|
||||
$type = $item->type;
|
||||
$start = DateTime::fromString($startString);
|
||||
|
||||
if (!in_array($type , $reminderTypeList)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($userIdList as $userId) {
|
||||
$reminderList = $userId === $this->user->getId() ?
|
||||
$this->getReminderList($entity, $typeList) :
|
||||
$this->getPreferencesReminderList($typeList, $userId, $entityType);
|
||||
|
||||
$remindAt->sub(new DateInterval('PT' . $seconds . 'S'));
|
||||
|
||||
foreach ($userIdList as $userId) {
|
||||
$reminderId = $this->idGenerator->generate();
|
||||
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->insert()
|
||||
->into(Reminder::ENTITY_TYPE)
|
||||
->columns([
|
||||
'id',
|
||||
'entityId',
|
||||
'entityType',
|
||||
'type',
|
||||
'userId',
|
||||
'remindAt',
|
||||
'startAt',
|
||||
'seconds',
|
||||
])
|
||||
->values([
|
||||
'id' => $reminderId,
|
||||
'entityId' => $entity->getId(),
|
||||
'entityType' => $entityType,
|
||||
'type' => $type,
|
||||
'userId' => $userId,
|
||||
'remindAt' => $remindAt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
|
||||
'startAt' => $dateValue,
|
||||
'seconds' => $seconds,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($query);
|
||||
foreach ($reminderList as $item) {
|
||||
$this->createReminder($entity, $userId, $start, $item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass[]
|
||||
* @return object{seconds: int, type: string}[]
|
||||
*/
|
||||
private function getEntityReminderDataList(Entity $entity): array
|
||||
private function getEntityReminderDataList(CoreEntity $entity): array
|
||||
{
|
||||
$reminderDataList = [];
|
||||
$dataList = [];
|
||||
|
||||
$reminderCollection = $this->entityManager
|
||||
/** @var iterable<Reminder> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository(Reminder::ENTITY_TYPE)
|
||||
->select(['seconds', 'type'])
|
||||
->where([
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'entityId' => $entity->getId(),
|
||||
'userId' => $this->user->getId(),
|
||||
])
|
||||
->distinct()
|
||||
->order('seconds')
|
||||
->find();
|
||||
|
||||
foreach ($reminderCollection as $reminder) {
|
||||
$reminderDataList[] = (object) [
|
||||
'seconds' => $reminder->get('seconds'),
|
||||
'type' => $reminder->get('type'),
|
||||
foreach ($collection as $reminder) {
|
||||
$dataList[] = (object) [
|
||||
'seconds' => $reminder->getSeconds(),
|
||||
'type' => $reminder->getType(),
|
||||
];
|
||||
}
|
||||
|
||||
return $reminderDataList;
|
||||
return $dataList;
|
||||
}
|
||||
|
||||
private function getDateAttribute(string $entityType): string
|
||||
{
|
||||
return $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->getField('reminders')
|
||||
->getParam('dateField') ??
|
||||
$this->dateAttribute;
|
||||
}
|
||||
|
||||
private function hasRemindersField(string $entityType): bool
|
||||
{
|
||||
return $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->hasField('reminders');
|
||||
}
|
||||
|
||||
private function isNewOrChanged(CoreEntity $entity, string $dateAttribute): bool
|
||||
{
|
||||
return $entity->isNew() ||
|
||||
$this->toReCreate($entity) ||
|
||||
$entity->isAttributeChanged('assignedUserId') ||
|
||||
($entity->hasLinkMultipleField('assignedUsers') && $entity->isAttributeChanged('assignedUsersIds')) ||
|
||||
($entity->hasLinkMultipleField('users') && $entity->isAttributeChanged('usersIds')) ||
|
||||
$entity->isAttributeChanged($dateAttribute);
|
||||
}
|
||||
|
||||
private function toProcess(CoreEntity $entity, string $dateAttribute): bool
|
||||
{
|
||||
return $this->isNewOrChanged($entity, $dateAttribute) || $entity->has('reminders');
|
||||
}
|
||||
|
||||
private function onlyRemindersFieldChanged(CoreEntity $entity, string $dateAttribute): bool
|
||||
{
|
||||
if ($this->isNewOrChanged($entity, $dateAttribute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $entity->isAttributeChanged('reminders');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getTypeList(): array
|
||||
{
|
||||
return $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity(Reminder::ENTITY_TYPE)
|
||||
->getField('type')
|
||||
->getParam('options') ?? [];
|
||||
}
|
||||
|
||||
private function deleteAll(CoreEntity $entity): void
|
||||
{
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from(Reminder::ENTITY_TYPE)
|
||||
->where([
|
||||
'entityId' => $entity->getId(),
|
||||
'entityType' => $entity->getEntityType(),
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($query);
|
||||
}
|
||||
|
||||
private function deleteAllForUser(CoreEntity $entity): void
|
||||
{
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from(Reminder::ENTITY_TYPE)
|
||||
->where([
|
||||
'entityId' => $entity->getId(),
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'userId' => $this->user->getId(),
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getUserIdList(CoreEntity $entity): array
|
||||
{
|
||||
if ($entity->hasLinkMultipleField('users')) {
|
||||
return $entity->getLinkMultipleIdList('users');
|
||||
}
|
||||
|
||||
if ($entity->hasLinkMultipleField('assignedUsers')) {
|
||||
return $entity->getLinkMultipleIdList('assignedUsers');
|
||||
}
|
||||
|
||||
$userIdList = [];
|
||||
|
||||
if ($entity->get('assignedUserId')) {
|
||||
$userIdList[] = $entity->get('assignedUserId');
|
||||
}
|
||||
|
||||
return $userIdList;
|
||||
}
|
||||
|
||||
private function getStartString(CoreEntity $entity, string $dateAttribute): ?string
|
||||
{
|
||||
$dateValue = $entity->get($dateAttribute);
|
||||
|
||||
if (!$entity->has($dateAttribute)) {
|
||||
$reloadedEntity = $this->entityManager->getEntityById($entity->getEntityType(), $entity->getId());
|
||||
|
||||
if ($reloadedEntity) {
|
||||
$dateValue = $reloadedEntity->get($dateAttribute);
|
||||
}
|
||||
|
||||
}
|
||||
return $dateValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $typeList
|
||||
* @return object{seconds: int, type: string}[]
|
||||
*/
|
||||
private function getReminderList(CoreEntity $entity, array $typeList): array
|
||||
{
|
||||
if ($entity->has('reminders')) {
|
||||
/** @var ?stdClass[] $list */
|
||||
$list = $entity->get('reminders');
|
||||
|
||||
if ($list === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->sanitizeList($list, $typeList);
|
||||
}
|
||||
|
||||
return $this->getEntityReminderDataList($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $typeList
|
||||
* @return object{seconds: int, type: string}[]
|
||||
*/
|
||||
private function getPreferencesReminderList(array $typeList, string $userId, string $entityType): array
|
||||
{
|
||||
$preferences = $this->entityManager->getRepositoryByClass(Preferences::class)->getById($userId);
|
||||
|
||||
if (!$preferences) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$param = 'defaultReminders';
|
||||
|
||||
// @todo Refactor.
|
||||
if ($entityType === Task::ENTITY_TYPE) {
|
||||
$param = 'defaultRemindersTask';
|
||||
}
|
||||
|
||||
/** @var stdClass[] $list */
|
||||
$list = $preferences->get($param) ?? [];
|
||||
|
||||
return $this->sanitizeList($list, $typeList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass[] $list
|
||||
* @param string[] $typeList
|
||||
* @return object{seconds: int, type: string}[]
|
||||
*/
|
||||
private function sanitizeList(array $list, array $typeList): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($list as $item) {
|
||||
$seconds = ($item->seconds ?? null);
|
||||
$type = ($item->type ?? null);
|
||||
|
||||
if (!is_int($seconds) || !in_array($type, $typeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = (object) [
|
||||
'seconds' => $seconds,
|
||||
'type' => $type,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object{seconds: int, type: string} $item
|
||||
*/
|
||||
private function createReminder(
|
||||
CoreEntity $entity,
|
||||
string $userId,
|
||||
DateTime $start,
|
||||
object $item
|
||||
): void {
|
||||
|
||||
$seconds = $item->seconds;
|
||||
$type = $item->type;
|
||||
|
||||
$remindAt = $start->addSeconds(- $seconds);
|
||||
|
||||
if ($remindAt->isLessThan(DateTime::createNow())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->insert()
|
||||
->into(Reminder::ENTITY_TYPE)
|
||||
->columns([
|
||||
'id',
|
||||
'entityId',
|
||||
'entityType',
|
||||
'type',
|
||||
'userId',
|
||||
'remindAt',
|
||||
'startAt',
|
||||
'seconds',
|
||||
])
|
||||
->values([
|
||||
'id' => $this->idGenerator->generate(),
|
||||
'entityId' => $entity->getId(),
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'type' => $type,
|
||||
'userId' => $userId,
|
||||
'remindAt' => $remindAt->toString(),
|
||||
'startAt' => $start->toString(),
|
||||
'seconds' => $seconds,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($query);
|
||||
}
|
||||
|
||||
private function toRemove(CoreEntity $entity): bool
|
||||
{
|
||||
if (!$entity->isAttributeChanged('status')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$status = $entity->get('status');
|
||||
|
||||
$ignoreStatusList = [
|
||||
...($this->metadata->get("scopes.$entityType.completedStatusList") ?? []),
|
||||
...($this->metadata->get("scopes.$entityType.canceledStatusList") ?? []),
|
||||
];
|
||||
|
||||
return in_array($status, $ignoreStatusList);
|
||||
}
|
||||
|
||||
private function toReCreate(CoreEntity $entity): bool
|
||||
{
|
||||
if (!$entity->isAttributeChanged('status')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$statusFetched = $entity->getFetched('status');
|
||||
$status = $entity->get('status');
|
||||
|
||||
$ignoreStatusList = [
|
||||
...($this->metadata->get("scopes.$entityType.completedStatusList") ?? []),
|
||||
...($this->metadata->get("scopes.$entityType.canceledStatusList") ?? []),
|
||||
];
|
||||
|
||||
return in_array($statusFetched, $ignoreStatusList) && !in_array($status, $ignoreStatusList);
|
||||
}
|
||||
}
|
||||
|
||||
59
application/Espo/Core/FieldProcessing/Stars/StarLoader.php
Normal file
59
application/Espo/Core/FieldProcessing/Stars/StarLoader.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\FieldProcessing\Stars;
|
||||
|
||||
use Espo\Core\FieldProcessing\Loader;
|
||||
use Espo\Core\FieldProcessing\Loader\Params;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\Stars\StarService;
|
||||
|
||||
/**
|
||||
* @implements Loader<Entity>
|
||||
*/
|
||||
class StarLoader implements Loader
|
||||
{
|
||||
public function __construct(
|
||||
private StarService $service,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
if (
|
||||
!$entity->hasAttribute('isStarred') ||
|
||||
!$this->service->isEnabled($entity->getEntityType())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->set('isStarred', $this->service->isStarred($entity, $this->user));
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,9 @@
|
||||
|
||||
namespace Espo\Core\FieldProcessing\Wysiwyg;
|
||||
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\FieldProcessing\Saver as SaverInterface;
|
||||
use Espo\Core\FieldProcessing\Saver\Params;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
@@ -40,15 +41,11 @@ use Espo\Core\ORM\EntityManager;
|
||||
*/
|
||||
class Saver implements SaverInterface
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
|
||||
/** @var array<string, string[]> */
|
||||
private $fieldListMapCache = [];
|
||||
|
||||
public function __construct(EntityManager $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
public function __construct(private EntityManager $entityManager)
|
||||
{}
|
||||
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
@@ -86,13 +83,13 @@ class Saver implements SaverInterface
|
||||
}
|
||||
|
||||
foreach ($matches[1] as $id) {
|
||||
$attachment = $this->entityManager->getEntity('Attachment', $id);
|
||||
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($id);
|
||||
|
||||
if (!$attachment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($attachment->get('relatedId')) {
|
||||
if ($attachment->getRelated()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -123,7 +120,7 @@ class Saver implements SaverInterface
|
||||
foreach ($entityDefs->getFieldNameList() as $name) {
|
||||
$defs = $entityDefs->getField($name);
|
||||
|
||||
if ($defs->getType() !== 'wysiwyg') {
|
||||
if ($defs->getType() !== FieldType::WYSIWYG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,16 @@ class SanitizeManager
|
||||
|
||||
/** @var class-string<Sanitizer>[] $fieldClassNameList */
|
||||
$fieldClassNameList = $this->metadata->get("entityDefs.$entityType.fields.$field.sanitizerClassNameList") ?? [];
|
||||
$ignoreList = $this->metadata->get("entityDefs.$entityType.fields.$field.sanitizerSuppressClassNameList") ?? [];
|
||||
|
||||
return array_merge($classNameList, $fieldClassNameList);
|
||||
$list = array_merge($classNameList, $fieldClassNameList);
|
||||
|
||||
if ($ignoreList === []) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
$list = array_diff($list, $ignoreList);
|
||||
|
||||
return array_values($list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,9 @@ class Argument implements Evaluatable
|
||||
}
|
||||
|
||||
if ($this->data instanceof Variable) {
|
||||
return new ArgumentList([$this->data->getName()]);
|
||||
$value = new Value($this->data->getName());
|
||||
|
||||
return new ArgumentList([$value]);
|
||||
}
|
||||
|
||||
if ($this->data instanceof Attribute) {
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Core\Formula;
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\ExecutionException;
|
||||
use Espo\Core\Formula\Exceptions\SyntaxError;
|
||||
use Espo\Core\Formula\Exceptions\UnsafeFunction;
|
||||
use Espo\Core\Formula\Functions\Base as DeprecatedBaseFunction;
|
||||
use Espo\Core\Formula\Functions\BaseFunction;
|
||||
use Espo\Core\Formula\Parser\Ast\Attribute;
|
||||
@@ -46,6 +47,8 @@ use stdClass;
|
||||
|
||||
/**
|
||||
* Creates an instance of Processor and executes a script.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Evaluator
|
||||
{
|
||||
@@ -56,10 +59,12 @@ class Evaluator
|
||||
|
||||
/**
|
||||
* @param array<string, class-string<BaseFunction|Func|DeprecatedBaseFunction>> $functionClassNameMap
|
||||
* @param string[] $unsafeFunctionList
|
||||
*/
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private array $functionClassNameMap = []
|
||||
private array $functionClassNameMap = [],
|
||||
private array $unsafeFunctionList = []
|
||||
) {
|
||||
$this->attributeFetcher = $injectableFactory->create(AttributeFetcher::class);
|
||||
$this->parser = new Parser();
|
||||
@@ -74,6 +79,31 @@ class Evaluator
|
||||
*/
|
||||
public function process(string $expression, ?Entity $entity = null, ?stdClass $variables = null): mixed
|
||||
{
|
||||
return $this->processInternal($expression, $entity, $variables, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process expression in safe mode.
|
||||
*
|
||||
* @throws SyntaxError
|
||||
* @throws Error
|
||||
*/
|
||||
public function processSafe(string $expression, ?Entity $entity = null, ?stdClass $variables = null): mixed
|
||||
{
|
||||
return $this->processInternal($expression, $entity, $variables, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SyntaxError
|
||||
* @throws Error
|
||||
*/
|
||||
private function processInternal(
|
||||
string $expression,
|
||||
?Entity $entity,
|
||||
?stdClass $variables,
|
||||
bool $safeMode,
|
||||
): mixed {
|
||||
|
||||
$processor = new Processor(
|
||||
$this->injectableFactory,
|
||||
$this->attributeFetcher,
|
||||
@@ -84,6 +114,10 @@ class Evaluator
|
||||
|
||||
$item = $this->getParsedExpression($expression);
|
||||
|
||||
if ($safeMode) {
|
||||
$this->checkIsSafe($item->getData());
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $processor->process($item);
|
||||
}
|
||||
@@ -107,4 +141,24 @@ class Evaluator
|
||||
|
||||
return new Argument($this->parsedHash[$expression]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsafeFunction
|
||||
*/
|
||||
private function checkIsSafe(mixed $data): void
|
||||
{
|
||||
if (!$data instanceof Node) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $data->getType();
|
||||
|
||||
if (in_array($name, $this->unsafeFunctionList)) {
|
||||
throw new UnsafeFunction("$name is not safe.");
|
||||
}
|
||||
|
||||
foreach ($data->getChildNodes() as $subData) {
|
||||
$this->checkIsSafe($subData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,16 +35,18 @@ namespace Espo\Core\Formula\Exceptions;
|
||||
class BadArgumentValue extends Error
|
||||
{
|
||||
private ?int $position = null;
|
||||
private ?string $logMessage = null;
|
||||
|
||||
/**
|
||||
* Create.
|
||||
*
|
||||
* @param int $position An argument position.
|
||||
*/
|
||||
public static function create(int $position): self
|
||||
public static function create(int $position, ?string $message = null): self
|
||||
{
|
||||
$obj = new self();
|
||||
$obj->position = $position;
|
||||
$obj->logMessage = $message;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
@@ -53,6 +55,12 @@ class BadArgumentValue extends Error
|
||||
{
|
||||
$position = (string) ($this->position ?? '?');
|
||||
|
||||
return "Bad argument value on position {$position}.";
|
||||
$message = "Bad argument value on position $position.";
|
||||
|
||||
if ($this->logMessage) {
|
||||
$message .= " " . $this->logMessage;
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
|
||||
34
application/Espo/Core/Formula/Exceptions/UnsafeFunction.php
Normal file
34
application/Espo/Core/Formula/Exceptions/UnsafeFunction.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Formula\Exceptions;
|
||||
|
||||
class UnsafeFunction extends Error
|
||||
{
|
||||
}
|
||||
47
application/Espo/Core/Formula/FuncVariablesAware.php
Normal file
47
application/Espo/Core/Formula/FuncVariablesAware.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\Core\Formula;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
|
||||
/**
|
||||
* A function aware of variables.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*/
|
||||
interface FuncVariablesAware
|
||||
{
|
||||
/**
|
||||
* Process.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public function process(EvaluatedArgumentList $arguments, Variables $variables): mixed;
|
||||
}
|
||||
@@ -30,7 +30,6 @@
|
||||
namespace Espo\Core\Formula;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\UnknownFunction;
|
||||
|
||||
use Espo\Core\Formula\Functions\Base;
|
||||
use Espo\Core\Formula\Functions\BaseFunction;
|
||||
use Espo\ORM\Entity;
|
||||
@@ -59,8 +58,12 @@ class FunctionFactory
|
||||
/**
|
||||
* @throws UnknownFunction
|
||||
*/
|
||||
public function create(string $name, ?Entity $entity = null, ?stdClass $variables = null): Func|BaseFunction|Base
|
||||
{
|
||||
public function create(
|
||||
string $name,
|
||||
?Entity $entity = null,
|
||||
?stdClass $variables = null
|
||||
): Func|FuncVariablesAware|BaseFunction|Base {
|
||||
|
||||
if ($this->classNameMap && array_key_exists($name, $this->classNameMap)) {
|
||||
$className = $this->classNameMap[$name];
|
||||
}
|
||||
@@ -77,7 +80,7 @@ class FunctionFactory
|
||||
|
||||
$typeName = implode('\\', $arr);
|
||||
|
||||
/** @var class-string<Func|BaseFunction|Base> $className */
|
||||
/** @var class-string<Func|FuncVariablesAware|BaseFunction|Base> $className */
|
||||
$className = 'Espo\\Core\\Formula\\Functions\\' . $typeName . 'Type';
|
||||
}
|
||||
|
||||
@@ -85,7 +88,12 @@ class FunctionFactory
|
||||
throw new UnknownFunction("Unknown function: " . $name);
|
||||
}
|
||||
|
||||
if ((new ReflectionClass($className))->implementsInterface(Func::class)) {
|
||||
$class = new ReflectionClass($className);
|
||||
|
||||
if (
|
||||
$class->implementsInterface(Func::class) ||
|
||||
$class->implementsInterface(FuncVariablesAware::class)
|
||||
) {
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,10 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Formula\Functions;
|
||||
|
||||
use Espo\Core\Formula\{
|
||||
Functions\BaseFunction,
|
||||
ArgumentList,
|
||||
};
|
||||
use Espo\Core\Formula\ArgumentList;
|
||||
|
||||
class AssignType extends BaseFunction
|
||||
{
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?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\Formula\Functions\EnvGroup;
|
||||
|
||||
use Espo\Core\Formula\EvaluatedArgumentList;
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentValue;
|
||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||
use Espo\Core\Formula\Func;
|
||||
use Espo\Entities\User;
|
||||
|
||||
class UserAttributeSafeType implements Func
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $allowedAttributeList = [
|
||||
'id',
|
||||
'userName',
|
||||
'name',
|
||||
'teamsIds',
|
||||
'defaultTeamId',
|
||||
'type',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
public function process(EvaluatedArgumentList $arguments): mixed
|
||||
{
|
||||
if (count($arguments) < 1) {
|
||||
throw TooFewArguments::create(1);
|
||||
}
|
||||
|
||||
$attribute = $arguments[0];
|
||||
|
||||
if (!is_string($attribute)) {
|
||||
throw BadArgumentType::create(1, 'string');
|
||||
}
|
||||
|
||||
if (!in_array($attribute, $this->allowedAttributeList)) {
|
||||
throw BadArgumentValue::create(1, "No allowed attribute.");
|
||||
}
|
||||
|
||||
return $this->user->get($attribute);
|
||||
}
|
||||
}
|
||||
@@ -29,28 +29,39 @@
|
||||
|
||||
namespace Espo\Core\Formula\Functions\EnvGroup;
|
||||
|
||||
use Espo\Core\Formula\{
|
||||
Functions\BaseFunction,
|
||||
ArgumentList,
|
||||
};
|
||||
use Espo\Core\Formula\EvaluatedArgumentList;
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||
use Espo\Core\Formula\Func;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\Di;
|
||||
|
||||
class UserAttributeType extends BaseFunction implements
|
||||
Di\UserAware
|
||||
class UserAttributeType implements Func
|
||||
{
|
||||
use Di\UserSetter;
|
||||
/** @var string[] */
|
||||
private array $forbiddenAttributeList = [
|
||||
'password',
|
||||
'apiKey',
|
||||
'secretKey',
|
||||
];
|
||||
|
||||
public function process(ArgumentList $args)
|
||||
public function __construct(
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
public function process(EvaluatedArgumentList $arguments): mixed
|
||||
{
|
||||
if (count($args) < 1) {
|
||||
$this->throwTooFewArguments();
|
||||
if (count($arguments) < 1) {
|
||||
throw TooFewArguments::create(1);
|
||||
}
|
||||
|
||||
$attribute = $this->evaluate($args[0]);
|
||||
$attribute = $arguments[0];
|
||||
|
||||
if (!is_string($attribute)) {
|
||||
$this->throwBadArgumentType(1, 'string');
|
||||
throw BadArgumentType::create(1, 'string');
|
||||
}
|
||||
|
||||
if (in_array($attribute, $this->forbiddenAttributeList)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->user->get($attribute);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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\Formula\Functions\ExtGroup\AclGroup;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Formula\EvaluatedArgumentList;
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||
use Espo\Core\Formula\Func;
|
||||
use Espo\Core\Utils\Acl\UserAclManagerProvider;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class CheckEntityType implements Func
|
||||
{
|
||||
public function __construct(
|
||||
private UserAclManagerProvider $userAclManagerProvider,
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
public function process(EvaluatedArgumentList $arguments): bool
|
||||
{
|
||||
if (count($arguments) < 3) {
|
||||
throw TooFewArguments::create(3);
|
||||
}
|
||||
|
||||
$userId = $arguments[0];
|
||||
$entityType = $arguments[1];
|
||||
$id = $arguments[2];
|
||||
$action = $arguments[3] ?? Table::ACTION_READ;
|
||||
|
||||
if (!is_string($userId)) {
|
||||
throw BadArgumentType::create(1, 'string');
|
||||
}
|
||||
|
||||
if (!is_string($entityType)) {
|
||||
throw BadArgumentType::create(2, 'string');
|
||||
}
|
||||
|
||||
if (!is_string($id)) {
|
||||
throw BadArgumentType::create(3, 'string');
|
||||
}
|
||||
|
||||
if (!is_string($action)) {
|
||||
throw BadArgumentType::create(4, 'string');
|
||||
}
|
||||
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityType, $id);
|
||||
|
||||
if (!$entity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->userAclManagerProvider->get($user)->checkEntity($user, $entity, $action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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\Formula\Functions\ExtGroup\AclGroup;
|
||||
|
||||
use Espo\Core\Formula\EvaluatedArgumentList;
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||
use Espo\Core\Formula\Func;
|
||||
use Espo\Core\Utils\Acl\UserAclManagerProvider;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class CheckScopeType implements Func
|
||||
{
|
||||
public function __construct(
|
||||
private UserAclManagerProvider $userAclManagerProvider,
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
public function process(EvaluatedArgumentList $arguments): bool
|
||||
{
|
||||
if (count($arguments) < 2) {
|
||||
throw TooFewArguments::create(2);
|
||||
}
|
||||
|
||||
$userId = $arguments[0];
|
||||
$scope = $arguments[1];
|
||||
$action = $arguments[2] ?? null;
|
||||
|
||||
if (!is_string($userId)) {
|
||||
throw BadArgumentType::create(1, 'string');
|
||||
}
|
||||
|
||||
if (!is_string($scope)) {
|
||||
throw BadArgumentType::create(2, 'string');
|
||||
}
|
||||
|
||||
if ($action !== null && !is_string($action)) {
|
||||
throw BadArgumentType::create(3, 'string');
|
||||
}
|
||||
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->userAclManagerProvider->get($user)->checkScope($user, $scope, $action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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\Formula\Functions\ExtGroup\AclGroup;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Formula\EvaluatedArgumentList;
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentValue;
|
||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||
use Espo\Core\Formula\Func;
|
||||
use Espo\Core\Utils\Acl\UserAclManagerProvider;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class GetLevelType implements Func
|
||||
{
|
||||
public function __construct(
|
||||
private UserAclManagerProvider $userAclManagerProvider,
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
public function process(EvaluatedArgumentList $arguments): string
|
||||
{
|
||||
if (count($arguments) < 3) {
|
||||
throw TooFewArguments::create(3);
|
||||
}
|
||||
|
||||
$userId = $arguments[0];
|
||||
$scope = $arguments[1];
|
||||
$action = $arguments[2];
|
||||
|
||||
if (!is_string($userId)) {
|
||||
throw BadArgumentType::create(1, 'string');
|
||||
}
|
||||
|
||||
if (!is_string($scope)) {
|
||||
throw BadArgumentType::create(2, 'string');
|
||||
}
|
||||
|
||||
if (!is_string($action)) {
|
||||
throw BadArgumentType::create(3, 'string');
|
||||
}
|
||||
|
||||
if (
|
||||
!in_array($action, [
|
||||
Table::ACTION_READ,
|
||||
Table::ACTION_CREATE,
|
||||
Table::ACTION_EDIT,
|
||||
Table::ACTION_STREAM,
|
||||
Table::ACTION_DELETE,
|
||||
])
|
||||
) {
|
||||
throw BadArgumentValue::create(3);
|
||||
}
|
||||
|
||||
/** @var Table::ACTION_* $action */
|
||||
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
|
||||
|
||||
if (!$user) {
|
||||
return Table::LEVEL_NO;
|
||||
}
|
||||
|
||||
return $this->userAclManagerProvider->get($user)->getLevel($user, $scope, $action);
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Formula\Functions\ExtGroup\AclGroup;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Formula\EvaluatedArgumentList;
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||
use Espo\Core\Formula\Func;
|
||||
use Espo\Core\Utils\Acl\UserAclManagerProvider;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class GetPermissionLevelType implements Func
|
||||
{
|
||||
public function __construct(
|
||||
private UserAclManagerProvider $userAclManagerProvider,
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
public function process(EvaluatedArgumentList $arguments): string
|
||||
{
|
||||
if (count($arguments) < 2) {
|
||||
throw TooFewArguments::create(2);
|
||||
}
|
||||
|
||||
$userId = $arguments[0];
|
||||
$permission = $arguments[1];
|
||||
|
||||
if (!is_string($userId)) {
|
||||
throw BadArgumentType::create(1, 'string');
|
||||
}
|
||||
|
||||
if (!is_string($permission)) {
|
||||
throw BadArgumentType::create(2, 'string');
|
||||
}
|
||||
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
|
||||
|
||||
if (!$user) {
|
||||
return Table::LEVEL_NO;
|
||||
}
|
||||
|
||||
return $this->userAclManagerProvider->get($user)->getPermissionLevel($user, $permission);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user