mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-07 15:17:02 +00:00
Compare commits
1402 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62d1c0fae9 | ||
|
|
76546ff06c | ||
|
|
42af361ae4 | ||
|
|
440c6cee23 | ||
|
|
e659c79bf7 | ||
|
|
e017479f85 | ||
|
|
4824714b0d | ||
|
|
6ee936d522 | ||
|
|
ba172494b4 | ||
|
|
5c44a374d5 | ||
|
|
8aae2b18ba | ||
|
|
258e56c61d | ||
|
|
3084dddf1c | ||
|
|
eb6f9b602f | ||
|
|
cf508a540e | ||
|
|
5278e3bf06 | ||
|
|
5763f5b58e | ||
|
|
0666880786 | ||
|
|
e0113388d2 | ||
|
|
384f28ecae | ||
|
|
d7596c208c | ||
|
|
6d1ab5870f | ||
|
|
73dbfa38ec | ||
|
|
4adb068699 | ||
|
|
3e4c738ab1 | ||
|
|
cd92e4fcd8 | ||
|
|
fba191f22c | ||
|
|
8874c8827a | ||
|
|
34529a8ed9 | ||
|
|
8fd44acae2 | ||
|
|
4dd540ffc7 | ||
|
|
9c20116c9b | ||
|
|
926410d58f | ||
|
|
c64a107ad9 | ||
|
|
5dcd25946b | ||
|
|
d12865bbcb | ||
|
|
3fed415437 | ||
|
|
a03a13d3b9 | ||
|
|
e625951831 | ||
|
|
f533c68c9b | ||
|
|
2420746f1b | ||
|
|
2dfd00dd2e | ||
|
|
e043bb48e9 | ||
|
|
9aaef9d957 | ||
|
|
87449aae67 | ||
|
|
191d064fe1 | ||
|
|
dcb3e2feaf | ||
|
|
da9a423e59 | ||
|
|
3be4510e63 | ||
|
|
a6bb5a239b | ||
|
|
a5fb42609b | ||
|
|
40c2c1718e | ||
|
|
aeecfd63da | ||
|
|
1da1e6da9b | ||
|
|
bf9f23ebdd | ||
|
|
7beb4f8d83 | ||
|
|
991b859643 | ||
|
|
9bd74e08db | ||
|
|
cfdf65025d | ||
|
|
bf471e654c | ||
|
|
5feee1cf55 | ||
|
|
6af6fc017b | ||
|
|
8ee9a792fc | ||
|
|
9ef1c5928f | ||
|
|
64c933e365 | ||
|
|
32055f3d6e | ||
|
|
804acae44b | ||
|
|
6b3f37c00e | ||
|
|
4bdc4878cd | ||
|
|
625d2bc128 | ||
|
|
0af5bb1b4b | ||
|
|
7a0d59357c | ||
|
|
adbb46d02c | ||
|
|
ac3884179e | ||
|
|
7cf1af188d | ||
|
|
21b695e4ef | ||
|
|
fbda66defc | ||
|
|
beb4435ee0 | ||
|
|
6a4c78c1cb | ||
|
|
dc7f5d8e66 | ||
|
|
872a4c2f2c | ||
|
|
8a5204bc4c | ||
|
|
acbf6a1742 | ||
|
|
b2d3d56a85 | ||
|
|
8b83fa640f | ||
|
|
5a95abed73 | ||
|
|
ca1d689c3a | ||
|
|
0c750789b1 | ||
|
|
ccb6c19c72 | ||
|
|
011581f09d | ||
|
|
110dd7b8b3 | ||
|
|
f9bdd78df8 | ||
|
|
241b582923 | ||
|
|
348a089e18 | ||
|
|
7528fb3871 | ||
|
|
e76f849bde | ||
|
|
770658ffed | ||
|
|
348bcd64ba | ||
|
|
23ec5a66a7 | ||
|
|
210da100ea | ||
|
|
91aec9fc1d | ||
|
|
155565b946 | ||
|
|
5157430104 | ||
|
|
2eb88617b3 | ||
|
|
5e4e0b8d3c | ||
|
|
5f1cb260ae | ||
|
|
d854fc9fa6 | ||
|
|
d455d0c73f | ||
|
|
ae15b8fb5e | ||
|
|
d686aee9d4 | ||
|
|
6e667a5024 | ||
|
|
5610ac9b71 | ||
|
|
f9bb35ae68 | ||
|
|
768f6d7c71 | ||
|
|
ea1455a50c | ||
|
|
cd6a157f1d | ||
|
|
447d54b61b | ||
|
|
f721220b1a | ||
|
|
95ed44d2ec | ||
|
|
5ad74492e1 | ||
|
|
b191896fb1 | ||
|
|
314636e298 | ||
|
|
6e4d25fadd | ||
|
|
d401cb4e4c | ||
|
|
2f7c892f67 | ||
|
|
1f33f41965 | ||
|
|
7e6c64364f | ||
|
|
9916b95703 | ||
|
|
cba0bf3d20 | ||
|
|
82b043adf6 | ||
|
|
bfcf607468 | ||
|
|
6db74ec68f | ||
|
|
02cb7c1fd5 | ||
|
|
8b06f2c02c | ||
|
|
9644729f68 | ||
|
|
c0f3d5e765 | ||
|
|
e766625aa8 | ||
|
|
ad97a71cb6 | ||
|
|
d857231178 | ||
|
|
4976ebe89e | ||
|
|
571ccc08d4 | ||
|
|
63f73483a1 | ||
|
|
60e4d3e7c0 | ||
|
|
063b5e19a2 | ||
|
|
9899e5913f | ||
|
|
6730e8a48b | ||
|
|
fe833862f3 | ||
|
|
f660c9aedf | ||
|
|
58cd736ab9 | ||
|
|
693d952910 | ||
|
|
8ffb72df30 | ||
|
|
2ae4094c64 | ||
|
|
3c089c8628 | ||
|
|
a2f7bef58d | ||
|
|
cc2e64a4cd | ||
|
|
a2eb3d7b8c | ||
|
|
214f5cec96 | ||
|
|
d56b95d9f2 | ||
|
|
7f4595a374 | ||
|
|
c3b5191ca4 | ||
|
|
de8f9114ab | ||
|
|
960583c052 | ||
|
|
32baf94251 | ||
|
|
e290bc0b12 | ||
|
|
54bf5d98ae | ||
|
|
d00b77137e | ||
|
|
293ca499cc | ||
|
|
5825f8a8f9 | ||
|
|
70f8458d9c | ||
|
|
d8dc5d107e | ||
|
|
47d7064a72 | ||
|
|
6874bee0ad | ||
|
|
18f285be91 | ||
|
|
9be5d5b102 | ||
|
|
25782c7696 | ||
|
|
20ae316c63 | ||
|
|
8fd166ef2b | ||
|
|
af8ce3a34e | ||
|
|
e5956abb85 | ||
|
|
7024487528 | ||
|
|
a938905946 | ||
|
|
0250bfcd12 | ||
|
|
6d7e1f0952 | ||
|
|
a098b20221 | ||
|
|
4ab21cb80b | ||
|
|
f9e3953f68 | ||
|
|
313a2874cd | ||
|
|
60ae130612 | ||
|
|
22084a3d77 | ||
|
|
ce35f68584 | ||
|
|
d0ff17ed40 | ||
|
|
30cfea4ac4 | ||
|
|
f83d0a34c3 | ||
|
|
f2fd948c7b | ||
|
|
1b0a20c001 | ||
|
|
9fa4f40ccf | ||
|
|
71e39fa4fe | ||
|
|
d98dfecef3 | ||
|
|
adfd7efae6 | ||
|
|
0d75fa4929 | ||
|
|
43c8613e94 | ||
|
|
ee1e3bbaf9 | ||
|
|
1d03f557ce | ||
|
|
939d482cfb | ||
|
|
044ffbff13 | ||
|
|
37130f00a0 | ||
|
|
c40070ceb0 | ||
|
|
76d9b3cf9c | ||
|
|
532321ddeb | ||
|
|
40b2613580 | ||
|
|
e298ebec4b | ||
|
|
4073c60182 | ||
|
|
2553496ce6 | ||
|
|
67c0ac45bd | ||
|
|
26f3c5630a | ||
|
|
f92f21764c | ||
|
|
a8631a6ce9 | ||
|
|
4045e33453 | ||
|
|
8c79eb1ea4 | ||
|
|
8885db0ae5 | ||
|
|
a8d210a22a | ||
|
|
82912197eb | ||
|
|
33cac3a551 | ||
|
|
ca89fd021d | ||
|
|
ad4e7c0beb | ||
|
|
739e2b0ebc | ||
|
|
738c6791a4 | ||
|
|
9bfa1a9225 | ||
|
|
7e3cc0480e | ||
|
|
5365640ca8 | ||
|
|
fe0570b0c4 | ||
|
|
cca76ec4e0 | ||
|
|
c226fd7936 | ||
|
|
f65143edf3 | ||
|
|
483b717825 | ||
|
|
b8a53f793a | ||
|
|
0210f9642a | ||
|
|
b6af13767d | ||
|
|
73ec521f41 | ||
|
|
ea4ff23eee | ||
|
|
d4131bbcb0 | ||
|
|
237ef71ad0 | ||
|
|
918f3fdd70 | ||
|
|
98bbf8c1ba | ||
|
|
613b82a85a | ||
|
|
f72cefeaef | ||
|
|
316e7ec876 | ||
|
|
e0ad00d857 | ||
|
|
108998be43 | ||
|
|
5b48d2d3eb | ||
|
|
66c3aa2011 | ||
|
|
f7d0723516 | ||
|
|
52588323a0 | ||
|
|
43451f99f1 | ||
|
|
93139ee7d0 | ||
|
|
4ee86fccfa | ||
|
|
cd1786c8ac | ||
|
|
6a346bd01d | ||
|
|
b0b770eabe | ||
|
|
21ef8c5fe6 | ||
|
|
db6ecd6b68 | ||
|
|
cf54ede1ba | ||
|
|
f8493d3300 | ||
|
|
fad66fe813 | ||
|
|
03c47cb625 | ||
|
|
21f2b22720 | ||
|
|
e4142bc0a1 | ||
|
|
cba4d92f08 | ||
|
|
0836168771 | ||
|
|
565f19f51b | ||
|
|
b7de48a8e8 | ||
|
|
402074a9b0 | ||
|
|
c5904fdda4 | ||
|
|
70fe7dfd62 | ||
|
|
c3afa55f89 | ||
|
|
3d9c5c01d2 | ||
|
|
9422429df0 | ||
|
|
4ecb8bf898 | ||
|
|
71dd03c914 | ||
|
|
9691f62ac3 | ||
|
|
374fcf2f9c | ||
|
|
940d407c77 | ||
|
|
9b2c988ddc | ||
|
|
6d25fb36a0 | ||
|
|
2fa287389b | ||
|
|
5b7010eebe | ||
|
|
32977a09f1 | ||
|
|
b222a0996f | ||
|
|
267dd01228 | ||
|
|
f07b129826 | ||
|
|
aae417a5d9 | ||
|
|
68aeaa54b9 | ||
|
|
8be18a2d11 | ||
|
|
cf35c20460 | ||
|
|
e51a8d74cb | ||
|
|
be6a872eda | ||
|
|
34392267f1 | ||
|
|
b960228799 | ||
|
|
bb8f6c9941 | ||
|
|
e6771aa5f6 | ||
|
|
3f52538773 | ||
|
|
f66de9054d | ||
|
|
b109e8da68 | ||
|
|
d14bb881bb | ||
|
|
eedfdd14e8 | ||
|
|
14c28a5aab | ||
|
|
975ed83753 | ||
|
|
df4e9c8ed0 | ||
|
|
cd50307fdf | ||
|
|
feee057e2b | ||
|
|
6581acd083 | ||
|
|
f3824117b3 | ||
|
|
f3736da8a6 | ||
|
|
198b746ce3 | ||
|
|
a9ded55e26 | ||
|
|
b50d56f884 | ||
|
|
207794e168 | ||
|
|
831e589b3d | ||
|
|
61e6f5aeb5 | ||
|
|
a3477ac57d | ||
|
|
9f08d3e9d6 | ||
|
|
d0f65e5fd9 | ||
|
|
5f20571958 | ||
|
|
4634597b7f | ||
|
|
ca44c8e9c7 | ||
|
|
2551b024f0 | ||
|
|
e652320a3e | ||
|
|
c1f13b146d | ||
|
|
36f3d040dd | ||
|
|
5ec3ac222b | ||
|
|
739f94a41b | ||
|
|
678ddfaf11 | ||
|
|
2cda441421 | ||
|
|
e518b8bef5 | ||
|
|
ae998c33bd | ||
|
|
dc2def1af7 | ||
|
|
9570857f8a | ||
|
|
6df100c2c2 | ||
|
|
0b46188034 | ||
|
|
eb8d958fa1 | ||
|
|
7e74ddc54f | ||
|
|
90ac4bc071 | ||
|
|
c59b655200 | ||
|
|
a1c7814f5a | ||
|
|
db713e4c41 | ||
|
|
33d7f99c5c | ||
|
|
487fed21a1 | ||
|
|
8fed65f5be | ||
|
|
d2f57cb558 | ||
|
|
8b905b6cd3 | ||
|
|
e5389af306 | ||
|
|
d524178d3b | ||
|
|
6f7198690d | ||
|
|
d52b73f738 | ||
|
|
785aadc288 | ||
|
|
42175f98e4 | ||
|
|
62975b2957 | ||
|
|
dc433f5a82 | ||
|
|
1af9256658 | ||
|
|
a3696af196 | ||
|
|
20913c283e | ||
|
|
f9df70e710 | ||
|
|
c0c878c20b | ||
|
|
abc19ee574 | ||
|
|
3b97d17aed | ||
|
|
06ceb371e3 | ||
|
|
d78666e0d2 | ||
|
|
cde19542a6 | ||
|
|
263e553a0f | ||
|
|
a129d54e06 | ||
|
|
09f9cbf3df | ||
|
|
14cb5730b5 | ||
|
|
64a0064f3c | ||
|
|
7c9a605450 | ||
|
|
7a9d0af314 | ||
|
|
e2e3bf9a71 | ||
|
|
7752c85304 | ||
|
|
51fcd5aca9 | ||
|
|
ad9391ad9e | ||
|
|
0a97096c61 | ||
|
|
fd5b77d919 | ||
|
|
f118f572bc | ||
|
|
c08b43991a | ||
|
|
f58f2071ed | ||
|
|
879df57fa0 | ||
|
|
46d0fa2c15 | ||
|
|
f242cd1454 | ||
|
|
ba04f1c7f1 | ||
|
|
df45635fd5 | ||
|
|
260f13bf3f | ||
|
|
bdd0ced5d2 | ||
|
|
f3a729e283 | ||
|
|
af345e8721 | ||
|
|
99670c7b3d | ||
|
|
01dffd98f2 | ||
|
|
137b989064 | ||
|
|
19eaca9bf4 | ||
|
|
83c82767c3 | ||
|
|
838be733ff | ||
|
|
6a49e812fb | ||
|
|
95ce8645f2 | ||
|
|
b707bf70b8 | ||
|
|
bc7de312c1 | ||
|
|
9c851a1662 | ||
|
|
41cc7efe5b | ||
|
|
22d141cd20 | ||
|
|
768c8f6247 | ||
|
|
478079ce19 | ||
|
|
d472809175 | ||
|
|
37ed7f3527 | ||
|
|
1d9b011a68 | ||
|
|
c6e8ce5ad4 | ||
|
|
63223a1150 | ||
|
|
580e2858a5 | ||
|
|
f298a5d0b2 | ||
|
|
1e9e60640c | ||
|
|
c36096cb45 | ||
|
|
cf6bf2ad28 | ||
|
|
09b62d5112 | ||
|
|
c12eacc4c1 | ||
|
|
b5289212b1 | ||
|
|
856e68f9ee | ||
|
|
9eb7e2ce0f | ||
|
|
a0777dc452 | ||
|
|
7ecf2ea11d | ||
|
|
4af7fde960 | ||
|
|
838a80fa55 | ||
|
|
36a72d0cb9 | ||
|
|
ea00d2d869 | ||
|
|
1b2f96fe5b | ||
|
|
8323627e9a | ||
|
|
73a2ece6a1 | ||
|
|
3fc84a8453 | ||
|
|
d2480e9637 | ||
|
|
fb95614171 | ||
|
|
97e497cced | ||
|
|
0857e28931 | ||
|
|
b1226fb091 | ||
|
|
a68c797cb6 | ||
|
|
f29a1a1e80 | ||
|
|
bea7442ecf | ||
|
|
fc09c29769 | ||
|
|
5c997b04c3 | ||
|
|
9a7e0e8b2a | ||
|
|
ff519194c9 | ||
|
|
b89d9bd6b5 | ||
|
|
183ab82f5a | ||
|
|
8be9e1ec55 | ||
|
|
a4ae94f5e9 | ||
|
|
be981741ce | ||
|
|
9491895991 | ||
|
|
038c374dcd | ||
|
|
ccc2ed09b8 | ||
|
|
4a570c6699 | ||
|
|
8bccde26d2 | ||
|
|
f924f60de3 | ||
|
|
3d646039be | ||
|
|
da064ff76f | ||
|
|
4f17ebabee | ||
|
|
0cd88f2ec9 | ||
|
|
77d5e1997d | ||
|
|
2e688d7671 | ||
|
|
3a5e514691 | ||
|
|
5e7e920687 | ||
|
|
adefd7d3c5 | ||
|
|
e2a1e497fc | ||
|
|
bd9dc3dfe3 | ||
|
|
58a0be2cef | ||
|
|
fea97768d9 | ||
|
|
41865d8cde | ||
|
|
b2df17cf93 | ||
|
|
ec0649f65a | ||
|
|
01cc896972 | ||
|
|
522e9be624 | ||
|
|
856f4687e7 | ||
|
|
ca4f787131 | ||
|
|
d518d3671b | ||
|
|
9bcdef9107 | ||
|
|
8854f60f4a | ||
|
|
760f51352b | ||
|
|
11860495c6 | ||
|
|
320420ef29 | ||
|
|
157b2a60bb | ||
|
|
1299b20fe1 | ||
|
|
b69109a5d4 | ||
|
|
3d6e83a8cf | ||
|
|
abe076dc68 | ||
|
|
cf5b6a6b62 | ||
|
|
ac115642cd | ||
|
|
41af71ebf3 | ||
|
|
85ed285c79 | ||
|
|
906f6e5717 | ||
|
|
4d2933797d | ||
|
|
e237084ec4 | ||
|
|
6e5148c48a | ||
|
|
9dd9d4e33a | ||
|
|
306de81763 | ||
|
|
533b51aed4 | ||
|
|
7576428284 | ||
|
|
ab24606bdf | ||
|
|
204046620c | ||
|
|
f3d234877b | ||
|
|
81f346fcc9 | ||
|
|
3033d3f09b | ||
|
|
5c7b674b60 | ||
|
|
89ccbbb2ec | ||
|
|
899c9dcb74 | ||
|
|
74d00c8b89 | ||
|
|
f8bfd6869c | ||
|
|
2aaafe850f | ||
|
|
4d7f86b5da | ||
|
|
9eaab5176a | ||
|
|
74661e676c | ||
|
|
7f3db957a4 | ||
|
|
0f7ed3d4c7 | ||
|
|
5f2a2728fc | ||
|
|
429faf405c | ||
|
|
6a5f15bbc5 | ||
|
|
375454fc5d | ||
|
|
41b92c9ee1 | ||
|
|
6293fb98d6 | ||
|
|
306321258d | ||
|
|
03e85ea32e | ||
|
|
551e2a6b47 | ||
|
|
0d4214fe46 | ||
|
|
c8d0fb019e | ||
|
|
9867eab381 | ||
|
|
252c5a34b3 | ||
|
|
ce5e7d79e8 | ||
|
|
e8946bb4fe | ||
|
|
d79a5cadd4 | ||
|
|
4e3cee4dc8 | ||
|
|
61922e0558 | ||
|
|
300cb53f3a | ||
|
|
0a43c781e0 | ||
|
|
87c6b9e5d5 | ||
|
|
3f6a8eea5a | ||
|
|
1b8142d4c3 | ||
|
|
28113d50ca | ||
|
|
0851d2ce7a | ||
|
|
b32f627bda | ||
|
|
e88153b917 | ||
|
|
893a260ed8 | ||
|
|
e93ff18b4c | ||
|
|
048a75ff67 | ||
|
|
911bdb3a51 | ||
|
|
b87019e200 | ||
|
|
e413ca6643 | ||
|
|
642ddf5a6c | ||
|
|
42c62ea59b | ||
|
|
d19b7ef657 | ||
|
|
6fbc892acf | ||
|
|
38d62d3a8f | ||
|
|
a44d3aa4a3 | ||
|
|
248d288803 | ||
|
|
564956d151 | ||
|
|
75888d2f38 | ||
|
|
3b9f705a53 | ||
|
|
e24d695eb3 | ||
|
|
55d68f1143 | ||
|
|
58e006cdde | ||
|
|
48811450a8 | ||
|
|
8aa2f12a8b | ||
|
|
a13c7d9465 | ||
|
|
3dd82aa61e | ||
|
|
e67a1b67ed | ||
|
|
463649ffa8 | ||
|
|
e037f2fddb | ||
|
|
6aef2939f9 | ||
|
|
0b6486a03b | ||
|
|
fb39f0915f | ||
|
|
a80178494a | ||
|
|
7bdb61c05e | ||
|
|
47403e2e35 | ||
|
|
a7d86dff4b | ||
|
|
a1ef2886dc | ||
|
|
2f8ec94169 | ||
|
|
1c6c445a59 | ||
|
|
f1cfb01451 | ||
|
|
8921ed42ad | ||
|
|
0836d52065 | ||
|
|
4f4b362c05 | ||
|
|
63db221a2a | ||
|
|
31c4206b2e | ||
|
|
13e0f0f06a | ||
|
|
153dd65a0d | ||
|
|
306fca8291 | ||
|
|
ef253690d6 | ||
|
|
f5377fe12b | ||
|
|
c35cb98b32 | ||
|
|
009f090061 | ||
|
|
61c586c31d | ||
|
|
a90cf7b8e3 | ||
|
|
db4e5c001b | ||
|
|
ead76d78a3 | ||
|
|
02cf4f4bb8 | ||
|
|
f9040b3afb | ||
|
|
e47155006b | ||
|
|
a07213fa5f | ||
|
|
d74d89e9d4 | ||
|
|
fc2c3ce644 | ||
|
|
7745a92490 | ||
|
|
92c0cf297c | ||
|
|
c6accac161 | ||
|
|
58b4a9b965 | ||
|
|
d77022a167 | ||
|
|
1c77a197ab | ||
|
|
fa1354a3b9 | ||
|
|
fc5c159d00 | ||
|
|
d4aa273661 | ||
|
|
ff1c5e9e20 | ||
|
|
eff9ae7225 | ||
|
|
146bdf01f1 | ||
|
|
487201d8e7 | ||
|
|
788d72afc4 | ||
|
|
f5da342192 | ||
|
|
2ce82fe605 | ||
|
|
291a54f070 | ||
|
|
14750fa131 | ||
|
|
26fde45450 | ||
|
|
cef116b4d6 | ||
|
|
04324bd03b | ||
|
|
4a302e637b | ||
|
|
da13823dbc | ||
|
|
9b6d1455a8 | ||
|
|
10845e1432 | ||
|
|
88dacb6a02 | ||
|
|
d9cc84b50a | ||
|
|
9505fe8222 | ||
|
|
70eca7a7b2 | ||
|
|
e9636e7e6f | ||
|
|
84dc7f796e | ||
|
|
16ccab49a5 | ||
|
|
3dfd6b6c57 | ||
|
|
5ac84f20d3 | ||
|
|
590b32c7ae | ||
|
|
9530c378d5 | ||
|
|
8fc2f85c37 | ||
|
|
88063f5ec7 | ||
|
|
9e6e8f580c | ||
|
|
586baeea14 | ||
|
|
3bf44c44a1 | ||
|
|
f05cc3f045 | ||
|
|
0aced49759 | ||
|
|
45d87e5462 | ||
|
|
5b10212354 | ||
|
|
5324c4f681 | ||
|
|
e315063e3f | ||
|
|
ba35a08cca | ||
|
|
e4a2fafb90 | ||
|
|
2c89644149 | ||
|
|
07d2a1e897 | ||
|
|
89f832bf22 | ||
|
|
6161cd8ea2 | ||
|
|
7b708ace1e | ||
|
|
e0b542fac6 | ||
|
|
17f871b424 | ||
|
|
4b565d9dd0 | ||
|
|
d1c2678cb1 | ||
|
|
1fbe13bb03 | ||
|
|
38b0dbd4d0 | ||
|
|
6d9bab52bd | ||
|
|
11aac106cd | ||
|
|
9a043cc95e | ||
|
|
c527144719 | ||
|
|
73905a789e | ||
|
|
d115005a14 | ||
|
|
df2c381946 | ||
|
|
3b4539858c | ||
|
|
0403af91fa | ||
|
|
b98bbd8d4c | ||
|
|
1722e43fa8 | ||
|
|
05af5fa0f6 | ||
|
|
fdab5c42a6 | ||
|
|
d62fce6b32 | ||
|
|
33b737296e | ||
|
|
dfcb7f1445 | ||
|
|
14511dce70 | ||
|
|
f0dc216994 | ||
|
|
023b5a977a | ||
|
|
2c08b5d80a | ||
|
|
890b9d8db0 | ||
|
|
7240f13c12 | ||
|
|
910fe5546c | ||
|
|
4d633853a8 | ||
|
|
1b0c9f77ab | ||
|
|
d262c4bc51 | ||
|
|
625ee91bc0 | ||
|
|
05e296879f | ||
|
|
33140d4928 | ||
|
|
f178d92273 | ||
|
|
ffae105b38 | ||
|
|
7a65acc8ee | ||
|
|
5de892d3b3 | ||
|
|
180e4143a1 | ||
|
|
f92b1168ac | ||
|
|
c343ee4c5f | ||
|
|
ee33e560e7 | ||
|
|
6589c8ed17 | ||
|
|
7a93eb59a1 | ||
|
|
a18bd4d184 | ||
|
|
34821f2b4b | ||
|
|
16d46abb1a | ||
|
|
10259a9c99 | ||
|
|
937971e3dd | ||
|
|
dd732d3044 | ||
|
|
77129b9f81 | ||
|
|
00bf9561da | ||
|
|
1fb820c930 | ||
|
|
76c1fc1669 | ||
|
|
64be5837f6 | ||
|
|
2b2af83ddc | ||
|
|
1817ec2aaa | ||
|
|
738a10f970 | ||
|
|
85a44b6657 | ||
|
|
869ebbe44e | ||
|
|
b648cf730f | ||
|
|
046a901c40 | ||
|
|
df478027d8 | ||
|
|
3eca6024cd | ||
|
|
fc3f517a1b | ||
|
|
f2e82acd4a | ||
|
|
f9122b7450 | ||
|
|
c6fdff53c3 | ||
|
|
dfcb63c18a | ||
|
|
9afcdbf65d | ||
|
|
6c7e7b7df2 | ||
|
|
6fd0f10ee7 | ||
|
|
7889d562b4 | ||
|
|
5731e7e408 | ||
|
|
e333503954 | ||
|
|
fd85bb95d2 | ||
|
|
b48660118b | ||
|
|
e9de116400 | ||
|
|
6127ef9c72 | ||
|
|
21842d159d | ||
|
|
21e0598159 | ||
|
|
8e4f83f7fe | ||
|
|
b394089a79 | ||
|
|
2faf1e65c3 | ||
|
|
5eafec9f03 | ||
|
|
668bc38d70 | ||
|
|
20108478b5 | ||
|
|
fbd1a9d02f | ||
|
|
5b466f7979 | ||
|
|
c1d7854eb7 | ||
|
|
b642f091c3 | ||
|
|
19a2523089 | ||
|
|
1bc1d23934 | ||
|
|
b1a0f979c9 | ||
|
|
fa67206416 | ||
|
|
ae891362c7 | ||
|
|
8d94a7d7bd | ||
|
|
4d450111e9 | ||
|
|
5d2bb9bea9 | ||
|
|
dc04e6c5f8 | ||
|
|
7551032364 | ||
|
|
1a935d0758 | ||
|
|
a696a1b0b4 | ||
|
|
25a553c8f4 | ||
|
|
9c4a472ac1 | ||
|
|
fb7202ad34 | ||
|
|
1de62f442e | ||
|
|
257bf317d0 | ||
|
|
0fbfde71fa | ||
|
|
10ba796288 | ||
|
|
0249c6fdce | ||
|
|
7cabbf1ee7 | ||
|
|
bcbf15df13 | ||
|
|
578e77b5c3 | ||
|
|
68ccfde3f3 | ||
|
|
9edc8eeab5 | ||
|
|
b3d570527e | ||
|
|
c1e5af36c4 | ||
|
|
1404698c5e | ||
|
|
7ece2673cc | ||
|
|
95ceb279a2 | ||
|
|
3a6611602f | ||
|
|
468cdebb38 | ||
|
|
9faca0fefe | ||
|
|
58c5eb8118 | ||
|
|
ac2eca2529 | ||
|
|
f1054af1f1 | ||
|
|
6864405e55 | ||
|
|
39027acc91 | ||
|
|
6e65991341 | ||
|
|
760bf67c32 | ||
|
|
9ece367ff1 | ||
|
|
8ff9c94936 | ||
|
|
d401655b45 | ||
|
|
85b266cb79 | ||
|
|
39be4cf2be | ||
|
|
68477319e2 | ||
|
|
08d3cc11d9 | ||
|
|
082e67287a | ||
|
|
518fe07ea1 | ||
|
|
d2e36316ea | ||
|
|
5362e98500 | ||
|
|
2839f0457a | ||
|
|
a77f70f457 | ||
|
|
73036c7347 | ||
|
|
b9dbac65cb | ||
|
|
8f49fe7adf | ||
|
|
52e968809e | ||
|
|
7192d5d5f5 | ||
|
|
dc7d4714d9 | ||
|
|
9a714b92fb | ||
|
|
ed57338919 | ||
|
|
d696f7347f | ||
|
|
d7aa35bf7c | ||
|
|
d67eba2684 | ||
|
|
6779684834 | ||
|
|
9f594f8259 | ||
|
|
46bab9cd8f | ||
|
|
c533eea6e3 | ||
|
|
6c187a0cc2 | ||
|
|
4a14f91c54 | ||
|
|
bcadc1e1d0 | ||
|
|
ceb4979949 | ||
|
|
a913eb600d | ||
|
|
bbdf8edce8 | ||
|
|
9abb977844 | ||
|
|
b2d143c931 | ||
|
|
486b14e818 | ||
|
|
92a9280e8e | ||
|
|
8741baeb52 | ||
|
|
db39305290 | ||
|
|
c1152d7fe7 | ||
|
|
dddf3f6e7c | ||
|
|
e2510113d4 | ||
|
|
23fbea2b9b | ||
|
|
1fcef2654c | ||
|
|
b2f98d25bc | ||
|
|
9bae43d200 | ||
|
|
2b4c690886 | ||
|
|
132264864c | ||
|
|
7f1297e6ae | ||
|
|
69a5ab9b83 | ||
|
|
a49eb15cbe | ||
|
|
ca5b56266d | ||
|
|
822ef61a0c | ||
|
|
363184e7fa | ||
|
|
5e877a8a8b | ||
|
|
4715350d7c | ||
|
|
ce32d6dcec | ||
|
|
d801c10e62 | ||
|
|
3b143de0ca | ||
|
|
0ac1d6bbce | ||
|
|
66761665da | ||
|
|
8236902325 | ||
|
|
f4a2d21ca6 | ||
|
|
4fea9b5ef3 | ||
|
|
5a4c151cff | ||
|
|
6e067a091e | ||
|
|
40562ef5b2 | ||
|
|
1a04a17d0c | ||
|
|
6e89eaaa4e | ||
|
|
0ecdf6b037 | ||
|
|
62eb882d31 | ||
|
|
971779b408 | ||
|
|
726c21ee47 | ||
|
|
92ef1620b2 | ||
|
|
e5589d8b7d | ||
|
|
14d4647b5a | ||
|
|
b46717e8f0 | ||
|
|
edfaf8993c | ||
|
|
95b4dd3d71 | ||
|
|
53ff803f38 | ||
|
|
6d5bd92f1b | ||
|
|
63ae29af74 | ||
|
|
7acc0b2021 | ||
|
|
b5f998eb0e | ||
|
|
dbe35eb3c4 | ||
|
|
cf9d446035 | ||
|
|
6e768dda5d | ||
|
|
22d70ad987 | ||
|
|
d0f0042073 | ||
|
|
31685d57b7 | ||
|
|
f054a3fb0d | ||
|
|
935253d79d | ||
|
|
30feae269c | ||
|
|
bb86f2b01e | ||
|
|
8247429780 | ||
|
|
f8134aa08c | ||
|
|
863ea657ed | ||
|
|
77c5471656 | ||
|
|
ba33994252 | ||
|
|
c08cb0a8f3 | ||
|
|
e25465ba5b | ||
|
|
1276f4c286 | ||
|
|
b66d09634b | ||
|
|
3c127293bc | ||
|
|
f79e64883f | ||
|
|
479a26b5c9 | ||
|
|
662a6cd2dd | ||
|
|
1ee63dec26 | ||
|
|
7f1186c316 | ||
|
|
a35fd6759a | ||
|
|
8466c44a9a | ||
|
|
5750ae3a6b | ||
|
|
ccab48d31e | ||
|
|
eba68da269 | ||
|
|
83e56e4279 | ||
|
|
cbb2267164 | ||
|
|
526317f142 | ||
|
|
3e68adf77a | ||
|
|
bd58259a43 | ||
|
|
df75d4c989 | ||
|
|
7e88d16197 | ||
|
|
3b7a884f4c | ||
|
|
fc910fec05 | ||
|
|
2f797719fa | ||
|
|
e30af4f4c6 | ||
|
|
1a85f9a047 | ||
|
|
7737128b6e | ||
|
|
bd2a318618 | ||
|
|
441551f8d8 | ||
|
|
4b679732b1 | ||
|
|
676a9d42cd | ||
|
|
b24a45e4bf | ||
|
|
b2ae958992 | ||
|
|
f2c8942b84 | ||
|
|
9523ff2db0 | ||
|
|
00a921e0e7 | ||
|
|
d960bd978c | ||
|
|
83263717fd | ||
|
|
bc2389968d | ||
|
|
2b49625106 | ||
|
|
d375986c36 | ||
|
|
ddd4c1e427 | ||
|
|
9848e18caf | ||
|
|
d490107bce | ||
|
|
55c72a5570 | ||
|
|
ef892af56f | ||
|
|
46ea00aef8 | ||
|
|
bac2a36472 | ||
|
|
3156909cf6 | ||
|
|
956beb3850 | ||
|
|
7869268e2d | ||
|
|
8e94164976 | ||
|
|
9ef06d7a08 | ||
|
|
fd7960643d | ||
|
|
72ee2c4939 | ||
|
|
01e5350060 | ||
|
|
478fc69ca5 | ||
|
|
98a5c584e0 | ||
|
|
c34211ae59 | ||
|
|
30fbc5b50a | ||
|
|
a2356ce977 | ||
|
|
34df179c4c | ||
|
|
20f3c21368 | ||
|
|
0c1c10a5f1 | ||
|
|
4c0eea3dae | ||
|
|
72c9ec3453 | ||
|
|
2602e970e1 | ||
|
|
fecf9b43aa | ||
|
|
70e25d17ea | ||
|
|
2943f46829 | ||
|
|
dfb667f0de | ||
|
|
90f7a9ec81 | ||
|
|
26a43e76a4 | ||
|
|
d28e2513ee | ||
|
|
2ad0336a53 | ||
|
|
d9405ea058 | ||
|
|
f46132b95c | ||
|
|
1bfad8ba22 | ||
|
|
7893eae4ca | ||
|
|
06a3edc446 | ||
|
|
237bd8e6b7 | ||
|
|
e15c200541 | ||
|
|
405c5d145b | ||
|
|
848a05beb8 | ||
|
|
96917675a0 | ||
|
|
751b3f2fc7 | ||
|
|
c12807d040 | ||
|
|
d8c42dcee6 | ||
|
|
2f073dc8eb | ||
|
|
cfb50440d7 | ||
|
|
216e945c06 | ||
|
|
4174864b8c | ||
|
|
9e7cc42c78 | ||
|
|
d6846adacf | ||
|
|
a30900aebe | ||
|
|
9c0e0122c5 | ||
|
|
2dd59f36a0 | ||
|
|
d5d4c5874b | ||
|
|
d2107eccde | ||
|
|
377268359b | ||
|
|
a726f7f1d5 | ||
|
|
e252e554c4 | ||
|
|
85b3d698b2 | ||
|
|
abfcae48e0 | ||
|
|
4ab88c19c9 | ||
|
|
7615857663 | ||
|
|
00045f4837 | ||
|
|
348c3ef725 | ||
|
|
7d3d4de51c | ||
|
|
0c5c7953b6 | ||
|
|
58049dd449 | ||
|
|
8413b5c9d5 | ||
|
|
101a9084b8 | ||
|
|
79c51379b8 | ||
|
|
7db116cb89 | ||
|
|
219168a378 | ||
|
|
958cfd7eba | ||
|
|
3fbd65ceca | ||
|
|
4bef98e3f4 | ||
|
|
610a0c3c92 | ||
|
|
9ae755af2a | ||
|
|
c5900911d7 | ||
|
|
926787fffb | ||
|
|
1b5bd3f5d8 | ||
|
|
562fb14cea | ||
|
|
b5104cef3c | ||
|
|
a067eb2070 | ||
|
|
d5b6120d45 | ||
|
|
bba15ab759 | ||
|
|
afb42e3589 | ||
|
|
59bf7d5a7a | ||
|
|
8e297fda48 | ||
|
|
1093143773 | ||
|
|
2ccc796b6f | ||
|
|
634002c471 | ||
|
|
5d41085dd4 | ||
|
|
5f2bc52e6b | ||
|
|
2d1cbaae73 | ||
|
|
2d4f11717c | ||
|
|
be6cfefbec | ||
|
|
2e1aa59250 | ||
|
|
04398052d8 | ||
|
|
88db4a5020 | ||
|
|
6338fe68c9 | ||
|
|
bf11678e20 | ||
|
|
4d0bda7238 | ||
|
|
2c4d5ab31d | ||
|
|
13629079a8 | ||
|
|
4d2cd9eff4 | ||
|
|
4a358f12f4 | ||
|
|
a182ad7a0f | ||
|
|
dda1b4e547 | ||
|
|
572aceb107 | ||
|
|
97440a776c | ||
|
|
8b23c591ff | ||
|
|
1d776ea766 | ||
|
|
3b34c6746e | ||
|
|
73aaba15a9 | ||
|
|
dd7220a62b | ||
|
|
4a774d387c | ||
|
|
1a46723abb | ||
|
|
2052bc7353 | ||
|
|
6e9136d4ea | ||
|
|
08e5f3cf40 | ||
|
|
9af6b6a8bd | ||
|
|
9bc31c1111 | ||
|
|
6b887f26c5 | ||
|
|
97a33bde9a | ||
|
|
10597931be | ||
|
|
452f7a5d98 | ||
|
|
3b6e690046 | ||
|
|
233b7163a2 | ||
|
|
821a4dd5b4 | ||
|
|
2116b7fc7d | ||
|
|
aed9191584 | ||
|
|
75633fc216 | ||
|
|
efe41212b0 | ||
|
|
cfca519c5a | ||
|
|
544ddbbddd | ||
|
|
3cbc2e07bb | ||
|
|
cae82e9866 | ||
|
|
c3ea3ac6cc | ||
|
|
0641cf4d62 | ||
|
|
ebd859e4d2 | ||
|
|
2fe09b5f40 | ||
|
|
29d6ea3668 | ||
|
|
d901459581 | ||
|
|
38897f7f3b | ||
|
|
d13a82fc9f | ||
|
|
b0d3d19694 | ||
|
|
512fb35fb4 | ||
|
|
f61714db17 | ||
|
|
2a08678635 | ||
|
|
4e3ff06d7c | ||
|
|
d9d7567f06 | ||
|
|
ce84389980 | ||
|
|
c78feffc41 | ||
|
|
d3f490ee89 | ||
|
|
5195656bac | ||
|
|
dfce7eeac7 | ||
|
|
e8060925a3 | ||
|
|
0fe2ea1d44 | ||
|
|
f461a02b1f | ||
|
|
6c9196fdb2 | ||
|
|
e76ee0b093 | ||
|
|
498f1bf2dc | ||
|
|
e3cd5edaf7 | ||
|
|
b8d1d954b6 | ||
|
|
7c2687b753 | ||
|
|
96b3a522c3 | ||
|
|
8ec6fa0f52 | ||
|
|
0fb683bbac | ||
|
|
bc9490a7e4 | ||
|
|
a0642e85b4 | ||
|
|
e6e76df8c2 | ||
|
|
9b506f12e3 | ||
|
|
c176f72fd8 | ||
|
|
678ce4f139 | ||
|
|
1d2818ef61 | ||
|
|
6f56fffcbd | ||
|
|
c25d0e1304 | ||
|
|
45dce17fc6 | ||
|
|
e61f9eb242 | ||
|
|
8a37c9ec83 | ||
|
|
49bc91117a | ||
|
|
9f7177d847 | ||
|
|
4bd330ff59 | ||
|
|
4895066215 | ||
|
|
f9e30b778d | ||
|
|
72cf250afe | ||
|
|
efb99f090d | ||
|
|
661402d052 | ||
|
|
45a1c38418 | ||
|
|
7d0851a785 | ||
|
|
7f8c935a82 | ||
|
|
169bb09576 | ||
|
|
b14a989141 | ||
|
|
74844260c8 | ||
|
|
970b4ce104 | ||
|
|
74abbe9cc0 | ||
|
|
7ac7fea803 | ||
|
|
c28aec44b6 | ||
|
|
da8b117055 | ||
|
|
2c87d47507 | ||
|
|
92c9f2ca2d | ||
|
|
8d34a5bd1a | ||
|
|
27b9035fd8 | ||
|
|
59b20dabbb | ||
|
|
1e6dcd8fa7 | ||
|
|
e08bf1b18d | ||
|
|
645b7f4482 | ||
|
|
37a71f050f | ||
|
|
8c5b24689a | ||
|
|
37777f0d92 | ||
|
|
c5191deb40 | ||
|
|
78afd40955 | ||
|
|
2c7249f12d | ||
|
|
d9806e8ea9 | ||
|
|
373de97515 | ||
|
|
52a96ae480 | ||
|
|
d2398400e2 | ||
|
|
50de79e2f7 | ||
|
|
8af367d67e | ||
|
|
31a94da8ff | ||
|
|
a9d5bc5d60 | ||
|
|
344a0f7ff0 | ||
|
|
1bd6c1c451 | ||
|
|
1d2f862d98 | ||
|
|
2ecfdc2d4b | ||
|
|
503e1f6eff | ||
|
|
6516616bf4 | ||
|
|
bcd063b8f3 | ||
|
|
459de7be6f | ||
|
|
2d12868849 | ||
|
|
dfe921c231 | ||
|
|
17dddfd5d9 | ||
|
|
21cb6f505e | ||
|
|
e5f0ca77a9 | ||
|
|
6e5c85be7e | ||
|
|
43d5f93c4e | ||
|
|
955a489e21 | ||
|
|
05ef2099d4 | ||
|
|
af717a53b0 | ||
|
|
59c53a191d | ||
|
|
9ec7c897df | ||
|
|
cfe7212a80 | ||
|
|
c0dd692263 | ||
|
|
71dc18afdb | ||
|
|
ebf0fbd5d5 | ||
|
|
7a75c3c961 | ||
|
|
b7ebc55c17 | ||
|
|
3c97db414b | ||
|
|
988fc94710 | ||
|
|
d33355d561 | ||
|
|
bbc3aa5dd2 | ||
|
|
4afc962f79 | ||
|
|
f22fe29b26 | ||
|
|
0f7b28a6cd | ||
|
|
1539574fba | ||
|
|
3e5f193964 | ||
|
|
40e1e8b559 | ||
|
|
4871811385 | ||
|
|
bf46534d4d | ||
|
|
471486ce01 | ||
|
|
9590a39d4b | ||
|
|
51a4dd2a05 | ||
|
|
c85f28f8ea | ||
|
|
973e8cc77a | ||
|
|
e852253f93 | ||
|
|
4e5c041a81 | ||
|
|
250f6f0670 | ||
|
|
313d52ad14 | ||
|
|
e1af7b22f0 | ||
|
|
db2303bc16 | ||
|
|
6010aa1336 | ||
|
|
f7e9dbf292 | ||
|
|
d13222bfe3 | ||
|
|
e8daec0d0d | ||
|
|
2bc4c3c1c7 | ||
|
|
cb43e60ae9 | ||
|
|
b69d55d114 | ||
|
|
c4ce4d6258 | ||
|
|
01081ab9a7 | ||
|
|
d2a400d08a | ||
|
|
5c1fe910b6 | ||
|
|
97a2f644f0 | ||
|
|
35e1caf817 | ||
|
|
bccf6a4e05 | ||
|
|
7ef9b5be4e | ||
|
|
075509f97e | ||
|
|
56dfe8d88a | ||
|
|
f2c5fdf9b5 | ||
|
|
48020c771e | ||
|
|
cd7f060feb | ||
|
|
c0bb2148c6 | ||
|
|
966025c04f | ||
|
|
e9fdd976ce | ||
|
|
d1b5847f4c | ||
|
|
ad58272a21 | ||
|
|
ec5f66d6f1 | ||
|
|
5e59538865 | ||
|
|
4f884b3321 | ||
|
|
cc55430837 | ||
|
|
9e2fbbe494 | ||
|
|
322091248e | ||
|
|
6f241d8803 | ||
|
|
720c318186 | ||
|
|
caeed2ab43 | ||
|
|
4a5ef3c1b2 | ||
|
|
2da413754e | ||
|
|
7375149ada | ||
|
|
2cfd1b76d5 | ||
|
|
7de4a806a2 | ||
|
|
9e21dc43e9 | ||
|
|
b711f263eb | ||
|
|
d61cb52afe | ||
|
|
f0f20dc39f | ||
|
|
3beebe0a87 | ||
|
|
3de40cd5cf | ||
|
|
680ddad042 | ||
|
|
d65234508f | ||
|
|
8c7bce4244 | ||
|
|
58b9b5a775 | ||
|
|
3c2b7a757c | ||
|
|
69fc1c199d | ||
|
|
8c5e597f43 | ||
|
|
551427b5a1 | ||
|
|
9905cfa01b | ||
|
|
981693093c | ||
|
|
69ef1bf275 | ||
|
|
cd953b47fe | ||
|
|
0578268f2d | ||
|
|
2422940477 | ||
|
|
c551828192 | ||
|
|
077dd51e8a | ||
|
|
1a06f66555 | ||
|
|
345c6226b8 | ||
|
|
d2052bf8b1 | ||
|
|
2f90205a16 | ||
|
|
e267c88043 | ||
|
|
11c9bf7704 | ||
|
|
fb4730942f | ||
|
|
68b3fcaa43 | ||
|
|
45d8cc9e8e | ||
|
|
e501eaf238 | ||
|
|
c8395c6558 | ||
|
|
00b69bfbab | ||
|
|
4d5fe65988 | ||
|
|
9441825b8f | ||
|
|
48047cfea7 | ||
|
|
b26ea22041 | ||
|
|
2bdde353b1 | ||
|
|
cb18485c3a | ||
|
|
0e267ca2a6 | ||
|
|
085f93ba39 | ||
|
|
afea99ae6f | ||
|
|
3cd9e4919a | ||
|
|
e6b7914ccd | ||
|
|
c7e74fe209 | ||
|
|
b3f69c0c70 | ||
|
|
11848b1ded | ||
|
|
d6b48962ce | ||
|
|
521f407714 | ||
|
|
fd50868899 | ||
|
|
4f00a18599 | ||
|
|
767e775f31 | ||
|
|
bedb66fdd5 | ||
|
|
e3319a3bd4 | ||
|
|
c74111e29b | ||
|
|
bd2204156e | ||
|
|
fe15eeeac7 | ||
|
|
280a1a9313 | ||
|
|
27105910df | ||
|
|
f3d2616afb | ||
|
|
99cd5ddf3c | ||
|
|
02ebef1db8 | ||
|
|
28227ddc2e | ||
|
|
b848261468 | ||
|
|
f1ff31829c | ||
|
|
0736285e24 | ||
|
|
79bc345307 | ||
|
|
a737711652 | ||
|
|
b5e8b89ed5 | ||
|
|
a51b45df90 | ||
|
|
5612055b85 | ||
|
|
e3aff78e89 | ||
|
|
84a7576772 | ||
|
|
69cd14f471 | ||
|
|
1a2d223740 | ||
|
|
4ccbf86589 | ||
|
|
502b6586f1 | ||
|
|
4f57677e03 | ||
|
|
8c1494e17e | ||
|
|
178d06650a | ||
|
|
926ee94aae | ||
|
|
50a6209f11 | ||
|
|
e82a467f42 | ||
|
|
f0d508be6f | ||
|
|
82a3c606c7 | ||
|
|
e2b0aa65f8 | ||
|
|
271344cfc1 | ||
|
|
72cafdb27a | ||
|
|
930675672b | ||
|
|
73f8731014 | ||
|
|
33addcd90c | ||
|
|
b5ee03b142 | ||
|
|
a0d4fa5b82 | ||
|
|
317bbba8c9 | ||
|
|
cd58ccbb52 | ||
|
|
da032f3518 | ||
|
|
487da37bea | ||
|
|
3da95cce76 | ||
|
|
9a7cf5c35e | ||
|
|
a022a4e8e8 | ||
|
|
b96ae74c14 | ||
|
|
7a420fb6f9 | ||
|
|
58d161210d | ||
|
|
41c7cf115d | ||
|
|
f511553ff1 | ||
|
|
9e047650e8 | ||
|
|
ba3bbfc097 | ||
|
|
dcc2d1ee70 | ||
|
|
f167f0a0f9 | ||
|
|
7758e20224 | ||
|
|
a1cc2ee8c6 | ||
|
|
bdf397ad0f | ||
|
|
4dcae92ab7 | ||
|
|
a7f5192057 | ||
|
|
1d4fedf7c7 | ||
|
|
7496d0c347 | ||
|
|
24e6f2469f | ||
|
|
fdad7065f7 | ||
|
|
7b16d3541d | ||
|
|
3c9c3c9c8c | ||
|
|
f0413b7037 | ||
|
|
749f1a6a70 | ||
|
|
512a23ef64 | ||
|
|
23729c5b89 | ||
|
|
f31ee10cf7 | ||
|
|
21d34b091b | ||
|
|
fd19aa8b2e | ||
|
|
5af0b62e8e | ||
|
|
965cb33e34 | ||
|
|
aca19b3427 | ||
|
|
238baf89be | ||
|
|
50218a1146 | ||
|
|
d1280a8797 | ||
|
|
ca9358cf1a | ||
|
|
c3fa8f3131 | ||
|
|
4af110cd69 | ||
|
|
45d2d4306b | ||
|
|
385f01da8b | ||
|
|
85da160957 | ||
|
|
ff15f318c4 | ||
|
|
5769e1b58f | ||
|
|
028a7c728a | ||
|
|
9ec3889f96 | ||
|
|
3aa6502996 | ||
|
|
847f8713c9 | ||
|
|
7405476f61 | ||
|
|
985903e99b | ||
|
|
347f4e5566 | ||
|
|
15d72afbd5 | ||
|
|
085964dc58 | ||
|
|
6c0a1265ab | ||
|
|
6ee0de8f62 | ||
|
|
bc2613d6a3 | ||
|
|
655a499caa | ||
|
|
bcf6f33965 | ||
|
|
7c04febb3b | ||
|
|
de39bcf197 | ||
|
|
4e7779ee48 | ||
|
|
4b17896ff1 | ||
|
|
caf20a5e61 |
@@ -4,8 +4,7 @@ root = true
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
end_of_line = crlf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
|
||||
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -8,4 +8,9 @@
|
||||
*.tpl text eol=crlf
|
||||
*.html text eol=crlf
|
||||
|
||||
bin/command text eol=lf
|
||||
|
||||
.gitattributes text eol=crlf
|
||||
.gitignore text eol=crlf
|
||||
|
||||
*.png binary
|
||||
|
||||
2
.github/SECURITY.md
vendored
2
.github/SECURITY.md
vendored
@@ -6,4 +6,4 @@ If you believe you have discovered a vulnerability in EspoCRM please contacts us
|
||||
|
||||
## Supported versions
|
||||
|
||||
For severe vulnerabilities we provide fixes for 2 minor versions (the second number in the version string) back from the current stable version. Separate patches or manual fix guidelines will be provided for more old versions.
|
||||
For severe vulnerabilities we provide fixes for 2 minor versions (the second number in the version string) back from the current stable version.
|
||||
|
||||
@@ -24,11 +24,6 @@ Options -Indexes
|
||||
# Skip redirect for `client` dir.
|
||||
RewriteRule ^client/ - [L]
|
||||
|
||||
# {#dev}
|
||||
# Skip redirect for `node_modules` dir. Actual only for dev environment.
|
||||
RewriteRule ^node_modules/ - [L]
|
||||
# {/dev}
|
||||
|
||||
# Store base path.
|
||||
RewriteCond %{REQUEST_URI}::$1 ^(.*?/)(.*)::\2$
|
||||
RewriteRule ^(.*)$ - [E=BASE:%1]
|
||||
|
||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
*
|
||||
!.gitignore
|
||||
!/codeStyles
|
||||
!/fileTemplates
|
||||
!/inspectionProfiles
|
||||
11
.idea/codeStyles/Project.xml
generated
Normal file
11
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<PHPCodeStyleSettings>
|
||||
<option name="GROUP_USE_WRAP" value="2" />
|
||||
</PHPCodeStyleSettings>
|
||||
<codeStyleSettings language="PHP">
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
27
.idea/fileTemplates/includes/PHP File Header.php
generated
Normal file
27
.idea/fileTemplates/includes/PHP File Header.php
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-${YEAR} Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
1
.idea/fileTemplates/internal/JavaScript File.js
generated
Normal file
1
.idea/fileTemplates/internal/JavaScript File.js
generated
Normal file
@@ -0,0 +1 @@
|
||||
#parse("PHP File Header.php")
|
||||
19
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
19
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ES6ConvertVarToLetConst" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="JSIgnoredPromiseFromCall" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpDocMissingThrowsInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpDocSignatureIsNotCompleteInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpMissingFieldTypeInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpMissingParamTypeInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpMissingReturnTypeInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpStanGlobal" enabled="false" level="WEAK WARNING" enabled_by_default="false">
|
||||
<option name="config" value="$PROJECT_DIR$/phpstan.neon" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PhpSwitchStatementWitSingleBranchInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PsalmAdvanceCallableParamsInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="TrivialIfJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
147
Gruntfile.js
147
Gruntfile.js
@@ -32,36 +32,41 @@
|
||||
const fs = require('fs');
|
||||
const cp = require('child_process');
|
||||
const path = require('path');
|
||||
const buildUtils = require('./js/build-utils');
|
||||
|
||||
module.exports = grunt => {
|
||||
|
||||
const pkg = grunt.file.readJSON('package.json');
|
||||
const bundleConfig = require('./frontend/bundle-config.json');
|
||||
const libs = require('./frontend/libs.json');
|
||||
|
||||
let jsFilesToBundle = getBundleLibList().concat(bundleConfig.jsFiles);
|
||||
let jsFilesToCopy = getCopyLibDataList();
|
||||
const originalLibDir = 'client/lib/original';
|
||||
|
||||
let libFilesToMinify = jsFilesToCopy
|
||||
let bundleJsFileList = buildUtils.getPreparedBundleLibList(libs).concat(originalLibDir + '/espo.js');
|
||||
let copyJsFileList = buildUtils.getCopyLibDataList(libs);
|
||||
|
||||
let minifyLibFileList = copyJsFileList
|
||||
.filter(item => item.minify)
|
||||
.reduce((map, item) => (
|
||||
map[item.dest] = item.dest,
|
||||
map
|
||||
), {});
|
||||
.map(item => {
|
||||
return {
|
||||
dest: item.dest,
|
||||
src: item.originalDest,
|
||||
};
|
||||
});
|
||||
|
||||
let currentPath = path.dirname(fs.realpathSync(__filename));
|
||||
|
||||
let themeList = [];
|
||||
|
||||
fs.readdirSync('application/Espo/Resources/metadata/themes').forEach(file => {
|
||||
themeList.push(file.substr(0, file.length - 5));
|
||||
themeList.push(file.substring(0, file.length - 5));
|
||||
});
|
||||
|
||||
let cssminFilesData = {};
|
||||
|
||||
let lessData = {};
|
||||
|
||||
themeList.forEach(theme => {
|
||||
let name = camelCaseToHyphen(theme);
|
||||
let name = buildUtils.camelCaseToHyphen(theme);
|
||||
|
||||
let files = {};
|
||||
|
||||
@@ -71,14 +76,12 @@ module.exports = grunt => {
|
||||
cssminFilesData['client/css/espo/'+name+'.css'] = 'client/css/espo/'+name+'.css';
|
||||
cssminFilesData['client/css/espo/'+name+'-iframe.css'] = 'client/css/espo/'+name+'-iframe.css';
|
||||
|
||||
let o = {
|
||||
lessData[theme] = {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: files,
|
||||
};
|
||||
|
||||
lessData[theme] = o;
|
||||
});
|
||||
|
||||
grunt.initConfig({
|
||||
@@ -118,7 +121,7 @@ module.exports = grunt => {
|
||||
'build/tmp/client/custom/modules/*',
|
||||
'!build/tmp/client/custom/modules/dummy.txt',
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
less: lessData,
|
||||
@@ -142,11 +145,11 @@ module.exports = grunt => {
|
||||
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
|
||||
},
|
||||
files: {
|
||||
'client/lib/espo.min.js': jsFilesToBundle,
|
||||
'client/lib/espo.min.js': bundleJsFileList,
|
||||
},
|
||||
},
|
||||
lib: {
|
||||
files: libFilesToMinify,
|
||||
files: minifyLibFileList,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -172,7 +175,7 @@ module.exports = grunt => {
|
||||
dest: 'build/tmp/client',
|
||||
},
|
||||
frontendLib: {
|
||||
files: jsFilesToCopy,
|
||||
files: copyJsFileList,
|
||||
},
|
||||
backend: {
|
||||
expand: true,
|
||||
@@ -246,15 +249,33 @@ module.exports = grunt => {
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
grunt.registerTask('espo-bundle', () => {
|
||||
const Bundler = require('./js/bundler');
|
||||
|
||||
let contents = (new Bundler()).bundle(bundleConfig.jsFiles);
|
||||
|
||||
if (!fs.existsSync(originalLibDir)) {
|
||||
fs.mkdirSync(originalLibDir);
|
||||
}
|
||||
|
||||
fs.writeFileSync(originalLibDir + '/espo.js', contents, 'utf8');
|
||||
});
|
||||
|
||||
grunt.registerTask('prepare-lib-original', () => {
|
||||
// Even though `npm ci` runs the same script, 'clean:start' deletes files.
|
||||
cp.execSync("node js/scripts/prepare-lib-original");
|
||||
});
|
||||
|
||||
grunt.registerTask('prepare-lib', () => {
|
||||
cp.execSync("node js/scripts/prepare-lib");
|
||||
});
|
||||
|
||||
grunt.registerTask('chmod-folders', () => {
|
||||
cp.execSync(
|
||||
"find . -type d -exec chmod 755 {} +",
|
||||
{
|
||||
cwd: 'build/EspoCRM-' + pkg.version,
|
||||
}
|
||||
{cwd: 'build/EspoCRM-' + pkg.version}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -360,7 +381,7 @@ module.exports = grunt => {
|
||||
});
|
||||
|
||||
grunt.registerTask('upgrade', () => {
|
||||
cp.execSync("node diff --all --vendor", {stdio: 'inherit'});
|
||||
cp.execSync("node diff --closest", {stdio: 'inherit'});
|
||||
});
|
||||
|
||||
grunt.registerTask('unit-tests-run', () => {
|
||||
@@ -375,7 +396,7 @@ module.exports = grunt => {
|
||||
cp.execSync("composer run-script setConfigParams", {stdio: 'ignore'});
|
||||
});
|
||||
|
||||
grunt.registerTask('zip', () => {
|
||||
grunt.registerTask('zip', function () { // Don't change to arrow-function.
|
||||
const archiver = require('archiver');
|
||||
|
||||
let resolve = this.async();
|
||||
@@ -404,8 +425,9 @@ module.exports = grunt => {
|
||||
|
||||
archive
|
||||
.directory(currentPath + '/build/' + folder, folder)
|
||||
.pipe(zipOutput)
|
||||
.finalize();
|
||||
.pipe(zipOutput);
|
||||
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
grunt.registerTask('npm-install', () => {
|
||||
@@ -423,8 +445,11 @@ module.exports = grunt => {
|
||||
grunt.registerTask('internal', [
|
||||
'less',
|
||||
'cssmin',
|
||||
'espo-bundle',
|
||||
'prepare-lib-original',
|
||||
'uglify:bundle',
|
||||
'copy:frontendLib',
|
||||
'prepare-lib',
|
||||
'uglify:lib',
|
||||
]);
|
||||
|
||||
@@ -487,77 +512,3 @@ module.exports = grunt => {
|
||||
'offline',
|
||||
]);
|
||||
};
|
||||
|
||||
function getBundleLibList() {
|
||||
const libs = require('./frontend/libs.json');
|
||||
|
||||
let list = [];
|
||||
|
||||
libs.forEach(item => {
|
||||
if (!item.bundle) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.files) {
|
||||
item.files.forEach(item => {
|
||||
list.push(item.src);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.src) {
|
||||
throw new Error("No lib src.");
|
||||
}
|
||||
|
||||
list.push(item.src);
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function getCopyLibDataList() {
|
||||
const libs = require('./frontend/libs.json');
|
||||
|
||||
let list = [];
|
||||
|
||||
libs.forEach(item => {
|
||||
if (item.bundle) {
|
||||
return;
|
||||
}
|
||||
|
||||
let minify = item.minify;
|
||||
|
||||
if (item.files) {
|
||||
item.files.forEach(item => {
|
||||
list.push({
|
||||
src: item.src,
|
||||
dest: item.dest || 'client/lib/' + item.src.split('/').pop(),
|
||||
minify: minify,
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.src) {
|
||||
throw new Error("No lib src.");
|
||||
}
|
||||
|
||||
list.push({
|
||||
src: item.src,
|
||||
dest: item.dest || 'client/lib/' + item.src.split('/').pop(),
|
||||
minify: minify,
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function camelCaseToHyphen(string){
|
||||
if (string === null) {
|
||||
return string;
|
||||
}
|
||||
|
||||
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ class AccessChecker implements AccessEntityCREDSChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $assignedUserIdList */
|
||||
$assignedUserIdList = $entity->getLinkMultipleIdList('assignedUsers');
|
||||
|
||||
if (
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\Acl\Note;
|
||||
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
@@ -77,6 +78,9 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$parentId = $entity->get('parentId');
|
||||
@@ -95,6 +99,62 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$parentId = $entity->getParentId();
|
||||
$parentType = $entity->getParentType();
|
||||
|
||||
if ($parentId && $parentType) {
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if (!$parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->aclManager->checkEntityStream($user, $parent);
|
||||
}
|
||||
|
||||
if ($entity->getType() !== Note::TYPE_POST) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->getCreatedById() === $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($entity->getTargetType() === Note::TARGET_ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($entity->getTargetType() === Note::TARGET_TEAMS) {
|
||||
$targetTeamIdList = $entity->getLinkMultipleIdList('teams') ?? [];
|
||||
|
||||
foreach ($user->getTeamIdList() as $teamId) {
|
||||
if (in_array($teamId, $targetTeamIdList)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->getTargetType() === Note::TARGET_USERS) {
|
||||
return in_array($user->getId(), $entity->getLinkMultipleIdList('users') ?? []);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
@@ -106,7 +166,7 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkOwnershipOwn($user, $entity)) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
$createdAt = $entity->get('createdAt');
|
||||
@@ -134,6 +194,9 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
@@ -145,7 +208,7 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkOwnershipOwn($user, $entity)) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
$createdAt = $entity->get('createdAt');
|
||||
|
||||
@@ -29,22 +29,24 @@
|
||||
|
||||
namespace Espo\Classes\Acl\Note;
|
||||
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl\OwnershipOwnChecker,
|
||||
};
|
||||
use Espo\Core\Acl\OwnershipOwnChecker;
|
||||
|
||||
/**
|
||||
* @implements OwnershipOwnChecker<\Espo\Entities\Note>
|
||||
*/
|
||||
class OwnershipChecker implements OwnershipOwnChecker
|
||||
{
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkOwn(User $user, Entity $entity): bool
|
||||
{
|
||||
if ($entity->get('type') === 'Post' && $user->getId() === $entity->get('createdById')) {
|
||||
if ($entity->getType() === Note::TYPE_POST && $user->getId() === $entity->getCreatedById()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class OwnershipChecker implements OwnershipOwnChecker
|
||||
{
|
||||
public function checkOwn(User $user, Entity $entity): bool
|
||||
{
|
||||
/** @var string[] */
|
||||
/** @var string[] $userTeamIdList */
|
||||
$userTeamIdList = $user->getLinkMultipleIdList('teams');
|
||||
|
||||
return in_array($entity->getId(), $userTeamIdList);
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\AclPortal\Note;
|
||||
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
@@ -77,16 +78,19 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$parentId = $entity->get('parentId');
|
||||
$parentType = $entity->get('parentType');
|
||||
$parentId = $entity->getParentId();
|
||||
$parentType = $entity->getParentType();
|
||||
|
||||
if (!$parentId || !$parentType) {
|
||||
return $this->defaultAccessChecker->checkEntityCreate($user, $entity, $data);
|
||||
}
|
||||
|
||||
$parent = $this->entityManager->getEntity($parentType, $parentId);
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if ($parent && $this->aclManager->checkEntityStream($user, $parent)) {
|
||||
return true;
|
||||
@@ -95,31 +99,42 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
return $this->defaultAccessChecker->checkEntityCreate($user, $entity, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if ($entity->get('type') !== 'Post') {
|
||||
return false;
|
||||
}
|
||||
$parentId = $entity->getParentId();
|
||||
$parentType = $entity->getParentType();
|
||||
|
||||
if ($entity->get('type') === 'Post' && $entity->get('targetType')) {
|
||||
return false;
|
||||
}
|
||||
if ($parentId && $parentType) {
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if (!$entity->get('parentId') || !$entity->get('parentType')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parent = $this->entityManager->getEntity($entity->get('parentType'), $entity->get('parentId'));
|
||||
|
||||
if ($parent) {
|
||||
if ($this->aclManager->checkEntityStream($user, $parent)) {
|
||||
return true;
|
||||
if (!$parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->aclManager->checkEntityStream($user, $parent);
|
||||
}
|
||||
|
||||
if ($entity->getType() !== Note::TYPE_POST) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->getCreatedById() === $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($entity->getTargetType() === Note::TARGET_PORTALS) {
|
||||
return in_array($user->getPortalId(), $entity->getLinkMultipleIdList('portals') ?? []);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if (!$this->defaultAccessChecker->checkEntityEdit($user, $entity, $data)) {
|
||||
@@ -127,7 +142,7 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkOwnershipOwn($user, $entity)) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
$createdAt = $entity->get('createdAt');
|
||||
@@ -155,6 +170,9 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if (!$this->defaultAccessChecker->checkEntityDelete($user, $entity, $data)) {
|
||||
@@ -162,7 +180,7 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkOwnershipOwn($user, $entity)) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
$createdAt = $entity->get('createdAt');
|
||||
|
||||
@@ -29,22 +29,24 @@
|
||||
|
||||
namespace Espo\Classes\AclPortal\Note;
|
||||
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl\OwnershipOwnChecker,
|
||||
};
|
||||
use Espo\Core\Acl\OwnershipOwnChecker;
|
||||
|
||||
/**
|
||||
* @implements OwnershipOwnChecker<\Espo\Entities\Note>
|
||||
*/
|
||||
class OwnershipChecker implements OwnershipOwnChecker
|
||||
{
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkOwn(User $user, Entity $entity): bool
|
||||
{
|
||||
if ($entity->get('type') === 'Post' && $user->getId() === $entity->get('createdById')) {
|
||||
if ($entity->getType() === Note::TYPE_POST && $user->getId() === $entity->getCreatedById()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class Container
|
||||
'user',
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $fileList */
|
||||
$fileList = scandir('application/Espo/Core/Loaders');
|
||||
|
||||
if (file_exists('custom/Espo/Custom/Core/Loaders')) {
|
||||
|
||||
@@ -34,20 +34,24 @@ use Espo\Core\{
|
||||
Select\SelectBuilderFactory,
|
||||
ORM\EntityManager,
|
||||
};
|
||||
use Espo\Tools\App\AppParam;
|
||||
|
||||
/**
|
||||
* Returns a list of entity types for which a PDF template exists.
|
||||
*/
|
||||
class TemplateEntityTypeList
|
||||
class TemplateEntityTypeList implements AppParam
|
||||
{
|
||||
private $acl;
|
||||
private Acl $acl;
|
||||
|
||||
private $selectBuilderFactory;
|
||||
private SelectBuilderFactory $selectBuilderFactory;
|
||||
|
||||
private $entityManager;
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(Acl $acl, SelectBuilderFactory $selectBuilderFactory, EntityManager $entityManager)
|
||||
{
|
||||
public function __construct(
|
||||
Acl $acl,
|
||||
SelectBuilderFactory $selectBuilderFactory,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->acl = $acl;
|
||||
$this->selectBuilderFactory = $selectBuilderFactory;
|
||||
$this->entityManager = $entityManager;
|
||||
|
||||
@@ -83,14 +83,21 @@ class Import implements Command
|
||||
$resultId = $result->getId();
|
||||
$countCreated = $result->getCountCreated();
|
||||
$countUpdated = $result->getCountUpdated();
|
||||
$countError = $result->getCountError();
|
||||
$countDuplicate = $result->getCountDuplicate();
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$io->writeLine("Error occurred: ". $e->getMessage() . "");
|
||||
$io->writeLine("Error occurred: " . $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$io->writeLine("Finished. Import ID: {$resultId}. Created: {$countCreated}. Updated: {$countUpdated}.");
|
||||
$io->writeLine("Finished.");
|
||||
$io->writeLine(" Import ID: {$resultId}");
|
||||
$io->writeLine(" Created: {$countCreated}");
|
||||
$io->writeLine(" Updated: {$countUpdated}");
|
||||
$io->writeLine(" Duplicates: {$countDuplicate}");
|
||||
$io->writeLine(" Errors: {$countError}");
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -102,7 +109,7 @@ class Import implements Command
|
||||
$this->service->revert($id);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$io->writeLine("Error occurred: " . $e->getMessage() . "");
|
||||
$io->writeLine("Error occurred: " . $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -119,15 +126,21 @@ class Import implements Command
|
||||
$result = $this->service->importById($id, true, $forceResume);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$io->writeLine("Error occurred: " . $e->getMessage() . "");
|
||||
$io->writeLine("Error occurred: " . $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$countCreated = $result->getCountCreated();
|
||||
$countUpdated = $result->getCountUpdated();
|
||||
$countError = $result->getCountError();
|
||||
$countDuplicate = $result->getCountDuplicate();
|
||||
|
||||
$io->writeLine("Finished. Created: {$countCreated}. Updated: {$countUpdated}.");
|
||||
$io->writeLine("Finished.");
|
||||
$io->writeLine(" Created: {$countCreated}");
|
||||
$io->writeLine(" Updated: {$countUpdated}");
|
||||
$io->writeLine(" Duplicates: {$countDuplicate}");
|
||||
$io->writeLine(" Errors: {$countError}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class LinkMultiple implements FieldDuplicator
|
||||
->getRelation($relationDefs->getForeignRelationName())
|
||||
->getType();
|
||||
|
||||
if ($foreignRelationType !== Entity::HAS_MANY) {
|
||||
if ($foreignRelationType !== Entity::MANY_MANY) {
|
||||
$valueMap->{$field . 'Ids'} = [];
|
||||
$valueMap->{$field . 'Names'} = (object) [];
|
||||
$valueMap->{$field . 'Columns'} = (object) [];
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
namespace Espo\Classes\FieldProcessing\Email;
|
||||
|
||||
use Espo\Modules\Crm\Entities\Call;
|
||||
use Espo\Modules\Crm\Entities\Meeting;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\EmailAddress as EmailAddressRepository;
|
||||
@@ -44,7 +46,6 @@ use Espo\Core\{
|
||||
};
|
||||
|
||||
use ICal\ICal;
|
||||
use ICal\Event;
|
||||
|
||||
use Throwable;
|
||||
use stdClass;
|
||||
@@ -85,7 +86,7 @@ class IcsDataLoader implements Loader
|
||||
|
||||
$ical->initString($icsContents);
|
||||
|
||||
/* @var $event Event */
|
||||
/* @var \ICal\Event|null $event */
|
||||
$event = $ical->events()[0] ?? null;
|
||||
|
||||
if ($event === null) {
|
||||
@@ -121,11 +122,14 @@ class IcsDataLoader implements Loader
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->eventAlreadyExists($espoEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var EmailAddressRepository $emailAddressRepository */
|
||||
$emailAddressRepository = $this->entityManager->getRepository(EmailAddress::ENTITY_TYPE);
|
||||
|
||||
$attendeeEmailAddressList = $espoEvent->getAttendeeEmailAddressList();
|
||||
|
||||
$organizerEmailAddress = $espoEvent->getOrganizerEmailAddress();
|
||||
|
||||
if ($organizerEmailAddress) {
|
||||
@@ -167,7 +171,6 @@ class IcsDataLoader implements Loader
|
||||
$this->loadCreatedEvent($entity, $espoEvent, $eventData);
|
||||
|
||||
$entity->set('icsEventData', $eventData);
|
||||
|
||||
$entity->set('icsEventDateStart', $espoEvent->getDateStart());
|
||||
|
||||
if ($espoEvent->isAllDay()) {
|
||||
@@ -209,4 +212,35 @@ class IcsDataLoader implements Loader
|
||||
'name' => $createdEvent->get('name'),
|
||||
];
|
||||
}
|
||||
|
||||
private function eventAlreadyExists(EspoEvent $espoEvent): bool
|
||||
{
|
||||
$id = $espoEvent->getUid();
|
||||
|
||||
if (!$id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$found1 = $this->entityManager
|
||||
->getRDBRepository(Meeting::ENTITY_TYPE)
|
||||
->select(['id'])
|
||||
->where(['id' => $id])
|
||||
->findOne();
|
||||
|
||||
if ($found1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$found2 = $this->entityManager
|
||||
->getRDBRepository(Call::ENTITY_TYPE)
|
||||
->select(['id'])
|
||||
->where(['id' => $id])
|
||||
->findOne();
|
||||
|
||||
if ($found2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ class StringDataLoader implements Loader
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var ?string */
|
||||
/** @var ?string $fromEmailAddressId */
|
||||
$fromEmailAddressId = $entity->get('fromEmailAddressId');
|
||||
|
||||
if (!$fromEmailAddressId) {
|
||||
|
||||
@@ -29,12 +29,27 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use StdClass;
|
||||
use stdClass;
|
||||
|
||||
class ArrayType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private Defs $defs;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 100;
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->isNotEmpty($entity, $field);
|
||||
@@ -55,9 +70,92 @@ class ArrayType
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rawCheckArray(StdClass $data, string $field): bool
|
||||
public function checkArrayOfString(Entity $entity, string $field): bool
|
||||
{
|
||||
if (isset($data->$field) && $data->$field !== null && !is_array($data->$field)) {
|
||||
/** @var ?mixed[] $list */
|
||||
$list = $entity->get($field);
|
||||
|
||||
if ($list === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($list as $item) {
|
||||
if (!is_string($item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$entity->has($field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var ?string[] $value */
|
||||
$value = $entity->get($field);
|
||||
|
||||
if ($value === null || $value === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fieldDefs = $this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->getField($field);
|
||||
|
||||
if ($fieldDefs->getParam('allowCustomOptions')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$optionList = $this->getOptionList($entity->getEntityType(), $field);
|
||||
|
||||
if ($optionList === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($value as $item) {
|
||||
if (!in_array($item, $optionList)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
private function getOptionList(string $entityType, string $field): ?array
|
||||
{
|
||||
$fieldDefs = $this->defs
|
||||
->getEntity($entityType)
|
||||
->getField($field);
|
||||
|
||||
/** @var ?string $path */
|
||||
$path = $fieldDefs->getParam('optionsPath');
|
||||
|
||||
/** @var string[]|null|false $optionList */
|
||||
$optionList = $path ?
|
||||
$this->metadata->get($path) :
|
||||
$fieldDefs->getParam('options');
|
||||
|
||||
if ($optionList === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For bc.
|
||||
if ($optionList === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $optionList;
|
||||
}
|
||||
|
||||
public function rawCheckArray(stdClass $data, string $field): bool
|
||||
{
|
||||
if (isset($data->$field) && !is_array($data->$field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -82,4 +180,73 @@ class ArrayType
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, ?int $validationValue): bool
|
||||
{
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
/** @var string[] $value */
|
||||
$value = $entity->get($field) ?? [];
|
||||
|
||||
foreach ($value as $item) {
|
||||
if (mb_strlen($item) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkPattern(Entity $entity, string $field, ?string $validationValue): bool
|
||||
{
|
||||
if (!$validationValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pattern = $validationValue;
|
||||
|
||||
if ($validationValue[0] === '$') {
|
||||
$patternName = substr($validationValue, 1);
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', $patternName, 'pattern']) ??
|
||||
$pattern;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
/** @var string[] $value */
|
||||
$value = $entity->get($field) ?? [];
|
||||
|
||||
foreach ($value as $item) {
|
||||
if ($item === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!preg_match($preparedPattern, $item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkNoEmptyString(Entity $entity, string $field, ?bool $validationValue): bool
|
||||
{
|
||||
if (!$validationValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var string[] $value */
|
||||
$value = $entity->get($field) ?? [];
|
||||
|
||||
$optionList = $this->getOptionList($entity->getEntityType(), $field) ?? [];
|
||||
|
||||
foreach ($value as $item) {
|
||||
if ($item === '' && !in_array($item, $optionList)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators\Attachment;
|
||||
|
||||
use Espo\Classes\FieldValidators\LinkParentType;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class Related extends LinkParentType
|
||||
{
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
$typeValue = $entity->get($field . 'Type');
|
||||
|
||||
if ($typeValue === 'TemplateManager') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::checkValid($entity, $field);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,4 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
class ChecklistType extends ArrayType
|
||||
{
|
||||
|
||||
}
|
||||
class ChecklistType extends ArrayType {}
|
||||
|
||||
@@ -29,10 +29,21 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class EmailType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 255;
|
||||
|
||||
public function __construct(Metadata $metadata)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
}
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
if ($this->isNotEmpty($entity, $field)) {
|
||||
@@ -71,6 +82,10 @@ class EmailType
|
||||
}
|
||||
|
||||
foreach ($dataList as $item) {
|
||||
if (!$item instanceof stdClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($item->emailAddress)) {
|
||||
continue;
|
||||
}
|
||||
@@ -85,6 +100,36 @@ class EmailType
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?string $value */
|
||||
$value = $entity->get($field);
|
||||
|
||||
/** @var int $maxLength */
|
||||
$maxLength = $this->metadata->get(['entityDefs', 'EmailAddress', 'fields', 'name', 'maxLength']) ??
|
||||
self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
if ($value && mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dataList = $entity->get($field . 'Data');
|
||||
|
||||
if (!is_array($dataList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($dataList as $item) {
|
||||
$value = $item->emailAddress;
|
||||
|
||||
if ($value && mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return $entity->has($field) && $entity->get($field) !== '' && $entity->get($field) !== null;
|
||||
|
||||
@@ -29,15 +29,90 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class EnumType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private Defs $defs;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 255;
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->isNotEmpty($entity, $field);
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$entity->has($field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fieldDefs = $this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->getField($field);
|
||||
|
||||
/** @var ?string $path */
|
||||
$path = $fieldDefs->getParam('optionsPath');
|
||||
|
||||
/** @var string[]|null|false $optionList */
|
||||
$optionList = $path ?
|
||||
$this->metadata->get($path) :
|
||||
$fieldDefs->getParam('options');
|
||||
|
||||
if ($optionList === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For bc.
|
||||
if ($optionList === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$optionList = array_map(
|
||||
fn ($item) => $item === '' ? null : $item,
|
||||
$optionList
|
||||
);
|
||||
|
||||
$value = $entity->get($field);
|
||||
|
||||
// For bc.
|
||||
// @todo Remove in v8.0.
|
||||
if ($value === '') {
|
||||
$value = null;
|
||||
}
|
||||
|
||||
return in_array($value, $optionList);
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, ?int $validationValue): bool
|
||||
{
|
||||
if (!$this->isNotEmpty($entity, $field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$value = $entity->get($field);
|
||||
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
if (mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return $entity->has($field) && $entity->get($field) !== null;
|
||||
|
||||
@@ -37,7 +37,7 @@ class JsonArrayType
|
||||
{
|
||||
public function rawCheckArray(StdClass $data, string $field): bool
|
||||
{
|
||||
if (isset($data->$field) && $data->$field !== null && !is_array($data->$field)) {
|
||||
if (isset($data->$field) && !is_array($data->$field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,20 +29,240 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
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 checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $idList */
|
||||
$idList = $entity->getLinkMultipleIdList($field);
|
||||
|
||||
return count($idList) > 0;
|
||||
}
|
||||
|
||||
public function checkPattern(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?mixed[] $idList */
|
||||
$idList = $entity->get($field . 'Ids');
|
||||
|
||||
if ($idList === null || $idList === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'id', 'pattern']);
|
||||
|
||||
if (!$pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
foreach ($idList as $id) {
|
||||
if (!is_string($id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preg_match($preparedPattern, $id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkColumnsValid(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$entity->has($field . 'Columns')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var ?stdClass $columnsData */
|
||||
$columnsData = $entity->get($field . 'Columns');
|
||||
|
||||
if ($columnsData === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$entityDefs = $this->defs->getEntity($entity->getEntityType());
|
||||
$fieldDefs = $entityDefs->getField($field);
|
||||
|
||||
if ($fieldDefs->isNotStorable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var ?array<string,string> $columnsMap */
|
||||
$columnsMap = $fieldDefs->getParam('columns');
|
||||
|
||||
if ($columnsMap === null || $columnsMap === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$entityDefs->hasRelation($field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$relationDefs = $entityDefs->getRelation($field);
|
||||
|
||||
if (!$relationDefs->hasForeignEntityType()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$foreignEntityType = $relationDefs->getForeignEntityType();
|
||||
|
||||
foreach (array_keys(get_object_vars($columnsData)) as $id) {
|
||||
$itemData = $columnsData->$id;
|
||||
|
||||
if (!$itemData instanceof stdClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($columnsMap as $column => $foreignField) {
|
||||
if (!property_exists($itemData, $column)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $itemData->$column;
|
||||
|
||||
$result = $this->checkColumnValue($foreignEntityType, $foreignField, $value);
|
||||
|
||||
if (!$result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function checkColumnValue(string $entityType, string $field, $value): bool
|
||||
{
|
||||
$fieldDefs = $this->defs
|
||||
->getEntity($entityType)
|
||||
->getField($field);
|
||||
|
||||
$type = $fieldDefs->getType();
|
||||
|
||||
if ($type === self::COLUMN_TYPE_VARCHAR) {
|
||||
return $this->checkColumnValueVarchar($fieldDefs, $value);
|
||||
}
|
||||
|
||||
if ($type === self::COLUMN_TYPE_ENUM) {
|
||||
return $this->checkColumnValueEnum($fieldDefs, $value);
|
||||
}
|
||||
|
||||
if ($type === self::COLUMN_TYPE_BOOL) {
|
||||
return is_bool($value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function checkColumnValueVarchar(Defs\FieldDefs $fieldDefs, $value): bool
|
||||
{
|
||||
if ($value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$maxLength = $fieldDefs->getParam('maxLength');
|
||||
$pattern = $fieldDefs->getParam('pattern');
|
||||
|
||||
if ($maxLength && mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($pattern) {
|
||||
if ($pattern[0] === '$') {
|
||||
$patternName = substr($pattern, 1);
|
||||
|
||||
$pattern = $this->metadata
|
||||
->get(['app', 'regExpPatterns', $patternName, 'pattern']) ??
|
||||
$pattern;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
if (!preg_match($preparedPattern, $value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function checkColumnValueEnum(Defs\FieldDefs $fieldDefs, $value): bool
|
||||
{
|
||||
if (!is_string($value) && $value !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var ?string $path */
|
||||
$path = $fieldDefs->getParam('optionsPath');
|
||||
|
||||
/** @var string[]|null|false $optionList */
|
||||
$optionList = $path ?
|
||||
$this->metadata->get($path) :
|
||||
$fieldDefs->getParam('options');
|
||||
|
||||
if ($optionList === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For bc.
|
||||
if ($optionList === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$optionList = array_map(
|
||||
fn ($item) => $item === '' ? null : $item,
|
||||
$optionList
|
||||
);
|
||||
|
||||
// For bc.
|
||||
// @todo Remove in v8.0.
|
||||
if ($value === '') {
|
||||
$value = null;
|
||||
}
|
||||
|
||||
return in_array($value, $optionList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,21 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class LinkParentType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
private Defs $defs;
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
$idAttribute = $field . 'Id';
|
||||
@@ -52,4 +63,46 @@ class LinkParentType
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkPattern(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?string $idValue */
|
||||
$idValue = $entity->get($field . 'Id');
|
||||
|
||||
if ($idValue === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'id', 'pattern']);
|
||||
|
||||
if (!$pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
return (bool) preg_match($preparedPattern, $idValue);
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?string $typeValue */
|
||||
$typeValue = $entity->get($field . 'Type');
|
||||
|
||||
if ($typeValue === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var ?string[] $entityTypeList */
|
||||
$entityTypeList = $this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->getField($field)
|
||||
->getParam('entityList');
|
||||
|
||||
if ($entityTypeList !== null) {
|
||||
return in_array($typeValue, $entityTypeList);
|
||||
}
|
||||
|
||||
return (bool) $this->metadata->get(['entityDefs', $typeValue]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,18 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class LinkType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
public function __construct(Metadata $metadata)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
$idAttribute = $field . 'Id';
|
||||
@@ -43,4 +51,23 @@ class LinkType
|
||||
|
||||
return $entity->get($idAttribute) !== null && $entity->get($idAttribute) !== '';
|
||||
}
|
||||
|
||||
public function checkPattern(Entity $entity, string $field): bool
|
||||
{
|
||||
$idValue = $entity->get($field . 'Id');
|
||||
|
||||
if ($idValue === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'id', 'pattern']);
|
||||
|
||||
if (!$pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
return (bool) preg_match($preparedPattern, $idValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,12 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class MultiEnumType extends ArrayType
|
||||
{
|
||||
|
||||
public function checkNoEmptyString(Entity $entity, string $field, ?bool $validationValue): bool
|
||||
{
|
||||
return parent::checkNoEmptyString($entity, $field, true);
|
||||
}
|
||||
}
|
||||
|
||||
65
application/Espo/Classes/FieldValidators/PasswordType.php
Normal file
65
application/Espo/Classes/FieldValidators/PasswordType.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class PasswordType
|
||||
{
|
||||
private const DEFAULT_MAX_LENGTH = 255;
|
||||
|
||||
public function rawCheckValid(stdClass $data, string $field): bool
|
||||
{
|
||||
$value = $data->$field ?? null;
|
||||
|
||||
if ($value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return is_string($value);
|
||||
}
|
||||
|
||||
public function rawCheckMaxLength(stdClass $data, string $field, ?int $validationValue): bool
|
||||
{
|
||||
$value = $data->$field ?? null;
|
||||
|
||||
if (!is_string($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
if (mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,26 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class PhoneType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private Defs $defs;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 36;
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
if ($this->isNotEmpty($entity, $field)) {
|
||||
@@ -54,6 +70,132 @@ class PhoneType
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
if ($this->isNotEmpty($entity, $field)) {
|
||||
$number = $entity->get($field);
|
||||
|
||||
if (!$this->isValidNumber($number)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$dataList = $entity->get($field . 'Data');
|
||||
|
||||
if (!is_array($dataList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($dataList as $item) {
|
||||
if (!$item instanceof stdClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$number = $item->phoneNumber ?? null;
|
||||
$type = $item->type ?? null;
|
||||
|
||||
if (!$number) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isValidNumber($number)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isValidType($entity->getEntityType(), $field, $type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?string $value */
|
||||
$value = $entity->get($field);
|
||||
|
||||
/** @var int $maxLength */
|
||||
$maxLength = $this->metadata->get(['entityDefs', 'PhoneNumber', 'fields', 'name', 'maxLength']) ??
|
||||
self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
if ($value && mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dataList = $entity->get($field . 'Data');
|
||||
|
||||
if (!is_array($dataList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($dataList as $item) {
|
||||
$value = $item->phoneNumber;
|
||||
|
||||
if ($value && mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $type
|
||||
*/
|
||||
private function isValidType(string $entityType, string $field, $type): bool
|
||||
{
|
||||
if ($type === null) {
|
||||
// Will be stored with a default type.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_string($type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var string[]|null|false $typeList */
|
||||
$typeList = $this->defs
|
||||
->getEntity($entityType)
|
||||
->getField($field)
|
||||
->getParam('typeList');
|
||||
|
||||
if ($typeList === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For bc.
|
||||
if ($typeList === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($type, $typeList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $number
|
||||
*/
|
||||
private function isValidNumber($number): bool
|
||||
{
|
||||
if (!is_string($number)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($number === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'phoneNumberLoose', 'pattern']);
|
||||
|
||||
if (!$pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
return (bool) preg_match($preparedPattern, $number);
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return $entity->has($field) && $entity->get($field) !== '' && $entity->get($field) !== null;
|
||||
|
||||
63
application/Espo/Classes/FieldValidators/TextType.php
Normal file
63
application/Espo/Classes/FieldValidators/TextType.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class TextType
|
||||
{
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->isNotEmpty($entity, $field);
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, int $validationValue): bool
|
||||
{
|
||||
if (!$this->isNotEmpty($entity, $field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$value = $entity->get($field);
|
||||
|
||||
if (mb_strlen($value) > $validationValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return
|
||||
$entity->has($field) &&
|
||||
$entity->get($field) !== '' &&
|
||||
$entity->get($field) !== null;
|
||||
}
|
||||
}
|
||||
72
application/Espo/Classes/FieldValidators/UrlType.php
Normal file
72
application/Espo/Classes/FieldValidators/UrlType.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class UrlType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private VarcharType $varcharType;
|
||||
|
||||
public function __construct(Metadata $metadata, VarcharType $varcharType)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->varcharType = $varcharType;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->varcharType->checkRequired($entity, $field);
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, ?int $validationValue): bool
|
||||
{
|
||||
return $this->varcharType->checkMaxLength($entity, $field, $validationValue);
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
$value = $entity->get($field);
|
||||
|
||||
if ($value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var string $pattern */
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'uriOptionalProtocol', 'pattern']);
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
return (bool) preg_match($preparedPattern, $value);
|
||||
}
|
||||
}
|
||||
@@ -29,30 +29,79 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class VarcharType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 255;
|
||||
private Defs $defs;
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->isNotEmpty($entity, $field);
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, int $validationValue): bool
|
||||
public function checkMaxLength(Entity $entity, string $field, ?int $validationValue): bool
|
||||
{
|
||||
if ($this->isNotEmpty($entity, $field)) {
|
||||
$value = $entity->get($field);
|
||||
if (!$this->isNotEmpty($entity, $field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mb_strlen($value) > $validationValue) {
|
||||
return false;
|
||||
}
|
||||
$fieldDefs = $this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->getField($field);
|
||||
|
||||
if ($fieldDefs->isNotStorable() && !$validationValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$value = $entity->get($field);
|
||||
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
if (mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkPattern(Entity $entity, string $field, ?string $validationValue): bool
|
||||
{
|
||||
if (!$this->isNotEmpty($entity, $field) || !$validationValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$value = $entity->get($field);
|
||||
$pattern = $validationValue;
|
||||
|
||||
if ($validationValue[0] === '$') {
|
||||
$patternName = substr($validationValue, 1);
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', $patternName, 'pattern']) ??
|
||||
$pattern;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
return (bool) preg_match($preparedPattern, $value);
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return $entity->has($field) && $entity->get($field) !== '' && $entity->get($field) !== null;
|
||||
return
|
||||
$entity->has($field) &&
|
||||
$entity->get($field) !== '' &&
|
||||
$entity->get($field) !== null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Classes\Jobs;
|
||||
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\ORM\Repository\RDBRepository;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
@@ -134,7 +135,7 @@ class Cleanup implements JobDataLess
|
||||
|
||||
foreach ($items as $name => $item) {
|
||||
try {
|
||||
/** @var class-string<\Espo\Core\Cleanup\Cleanup> */
|
||||
/** @var class-string<\Espo\Core\Cleanup\Cleanup> $className */
|
||||
$className = $item['className'];
|
||||
|
||||
$obj = $injectableFactory->create($className);
|
||||
@@ -312,11 +313,15 @@ class Cleanup implements JobDataLess
|
||||
$datetime->modify($period);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository('Attachment')
|
||||
->getRDBRepository(Attachment::ENTITY_TYPE)
|
||||
->where([
|
||||
'OR' => [
|
||||
[
|
||||
'role' => ['Export File', 'Mail Merge', 'Mass Pdf']
|
||||
'role' => [
|
||||
Attachment::ROLE_EXPORT_FILE,
|
||||
'Mail Merge',
|
||||
'Mass Pdf',
|
||||
]
|
||||
]
|
||||
],
|
||||
'createdAt<' => $datetime->format('Y-m-d H:i:s'),
|
||||
@@ -357,7 +362,7 @@ class Cleanup implements JobDataLess
|
||||
|
||||
$datetimeFrom->modify($fromPeriod);
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['scopes']));
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
@@ -450,6 +455,19 @@ class Cleanup implements JobDataLess
|
||||
}
|
||||
}
|
||||
|
||||
$isBeingUploadedCollection = $this->entityManager
|
||||
->getRDBRepository('Attachment')
|
||||
->sth()
|
||||
->where([
|
||||
'isBeingUploaded' => true,
|
||||
'createdAt<' => $datetime->format('Y-m-d H:i:s'),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($isBeingUploadedCollection as $e) {
|
||||
$this->entityManager->removeEntity($e);
|
||||
}
|
||||
|
||||
$delete = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
@@ -554,7 +572,7 @@ class Cleanup implements JobDataLess
|
||||
$fileManager = $this->fileManager;
|
||||
|
||||
if ($fileManager->exists($path)) {
|
||||
/** @var string[] */
|
||||
/** @var string[] $fileList */
|
||||
$fileList = $fileManager->getFileList($path, false, '', false);
|
||||
|
||||
foreach ($fileList as $dirName) {
|
||||
@@ -713,7 +731,7 @@ class Cleanup implements JobDataLess
|
||||
|
||||
$datetime = new DateTime($period);
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['scopes']));
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
|
||||
@@ -29,40 +29,54 @@
|
||||
|
||||
namespace Espo\Classes\MassAction\User;
|
||||
|
||||
use Espo\Core\{
|
||||
MassAction\Actions\MassUpdate as MassUpdateOriginal,
|
||||
MassAction\QueryBuilder,
|
||||
MassAction\Params,
|
||||
MassAction\Result,
|
||||
MassAction\Data,
|
||||
MassAction\MassAction,
|
||||
Utils\File\Manager as FileManager,
|
||||
DataManager,
|
||||
Acl,
|
||||
ORM\EntityManager,
|
||||
Exceptions\Forbidden,
|
||||
};
|
||||
use Espo\Core\MassAction\Actions\MassUpdate as MassUpdateOriginal;
|
||||
use Espo\Core\MassAction\QueryBuilder;
|
||||
use Espo\Core\MassAction\Params;
|
||||
use Espo\Core\MassAction\Result;
|
||||
use Espo\Core\MassAction\Data;
|
||||
use Espo\Core\MassAction\MassAction;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
|
||||
use Espo\{
|
||||
Entities\User,
|
||||
ORM\Entity,
|
||||
};
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Tools\MassUpdate\Data as MassUpdateData;
|
||||
|
||||
class MassUpdate implements MassAction
|
||||
{
|
||||
private $massUpdateOriginal;
|
||||
private MassUpdateOriginal $massUpdateOriginal;
|
||||
|
||||
private $queryBuilder;
|
||||
private QueryBuilder $queryBuilder;
|
||||
|
||||
private $entityManager;
|
||||
private EntityManager $entityManager;
|
||||
|
||||
private $acl;
|
||||
private Acl $acl;
|
||||
|
||||
private $user;
|
||||
private User $user;
|
||||
|
||||
private $fileManager;
|
||||
private FileManager $fileManager;
|
||||
|
||||
private $dataManager;
|
||||
private DataManager $dataManager;
|
||||
|
||||
private const PERMISSION = 'massUpdatePermission';
|
||||
|
||||
private const SYSTEM_USER_ID = 'system';
|
||||
|
||||
/** @var string[] */
|
||||
private array $notAllowedAttributeList = [
|
||||
'type',
|
||||
'password',
|
||||
'emailAddress',
|
||||
'isAdmin',
|
||||
'isSuperAdmin',
|
||||
'isPortalUser',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
MassUpdateOriginal $massUpdateOriginal,
|
||||
@@ -86,48 +100,54 @@ class MassUpdate implements MassAction
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
|
||||
if (!$this->acl->check($entityType, 'edit')) {
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden("Only admin can mass-update users.");
|
||||
}
|
||||
|
||||
if (!$this->acl->check($entityType, Table::ACTION_EDIT)) {
|
||||
throw new Forbidden("No edit access for '{$entityType}'.");
|
||||
}
|
||||
|
||||
if ($this->acl->get('massUpdatePermission') !== 'yes') {
|
||||
if ($this->acl->get(self::PERMISSION) !== Table::LEVEL_YES) {
|
||||
throw new Forbidden("No mass-update permission.");
|
||||
}
|
||||
|
||||
if (
|
||||
$data->has('type') ||
|
||||
$data->has('password') ||
|
||||
$data->has('emailAddress') ||
|
||||
$data->has('isAdmin') ||
|
||||
$data->has('isSuperAdmin') ||
|
||||
$data->has('isPortalUser')
|
||||
) {
|
||||
throw new Forbidden("Not allowed fields.");
|
||||
}
|
||||
$massUpdateData = MassUpdateData::fromMassActionData($data);
|
||||
|
||||
$this->checkAccess($massUpdateData);
|
||||
|
||||
$query = $this->queryBuilder->build($params);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository('User')
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->select(['id'])
|
||||
->find();
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
$this->checkEntity($entity, $data);
|
||||
$this->checkEntity($entity, $massUpdateData);
|
||||
}
|
||||
|
||||
$result = $this->massUpdateOriginal->process($params, $data);
|
||||
|
||||
$this->afterProcess($result, $data);
|
||||
$this->afterProcess($result, $massUpdateData);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function checkEntity(Entity $entity, Data $data): void
|
||||
private function checkAccess(MassUpdateData $data): void
|
||||
{
|
||||
if ($entity->getId() === 'system') {
|
||||
foreach ($this->notAllowedAttributeList as $attribute) {
|
||||
if ($data->has($attribute)) {
|
||||
throw new Forbidden("Attribute '{$attribute}' not allowed for mass-update.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkEntity(Entity $entity, MassUpdateData $data): void
|
||||
{
|
||||
if ($entity->getId() === self::SYSTEM_USER_ID) {
|
||||
throw new Forbidden("Can't update 'system' user.");
|
||||
}
|
||||
|
||||
@@ -138,9 +158,9 @@ class MassUpdate implements MassAction
|
||||
}
|
||||
}
|
||||
|
||||
protected function afterProcess(Result $result, Data $dataWrapped): void
|
||||
private function afterProcess(Result $result, MassUpdateData $dataWrapped): void
|
||||
{
|
||||
$data = $dataWrapped->getRaw();
|
||||
$data = $dataWrapped->getValues();
|
||||
|
||||
if (
|
||||
property_exists($data, 'rolesIds') ||
|
||||
@@ -168,12 +188,12 @@ class MassUpdate implements MassAction
|
||||
}
|
||||
}
|
||||
|
||||
protected function clearRoleCache(string $id): void
|
||||
private function clearRoleCache(string $id): void
|
||||
{
|
||||
$this->fileManager->removeFile('data/cache/application/acl/' . $id . '.php');
|
||||
}
|
||||
|
||||
protected function clearPortalRolesCache(): void
|
||||
private function clearPortalRolesCache(): void
|
||||
{
|
||||
$this->fileManager->removeInDir('data/cache/application/aclPortal');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\RecordHooks\Event;
|
||||
|
||||
use Espo\Core\Record\Hook\UpdateHook;
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Field\Date;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Defs as OrmDefs;
|
||||
|
||||
/**
|
||||
* @implements UpdateHook<CoreEntity>
|
||||
*/
|
||||
class BeforeUpdatePreserveDuration implements UpdateHook
|
||||
{
|
||||
private OrmDefs $ormDefs;
|
||||
|
||||
public function __construct(OrmDefs $ormDefs)
|
||||
{
|
||||
$this->ormDefs = $ormDefs;
|
||||
}
|
||||
|
||||
public function process(Entity $entity, UpdateParams $params): void
|
||||
{
|
||||
/** @var CoreEntity $entity */
|
||||
|
||||
if (!$entity->isAttributeChanged('dateStart') && !$entity->isAttributeChanged('dateStartDate')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entity->isAttributeWritten('dateEnd') || $entity->isAttributeWritten('dateEndDate')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$preserveDurationDisabled = $this->ormDefs
|
||||
->getEntity($entity->getEntityType())
|
||||
->getField('dateEnd')
|
||||
->getParam('preserveDurationDisabled');
|
||||
|
||||
if ($preserveDurationDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processDateTime($entity);
|
||||
$this->processDate($entity);
|
||||
}
|
||||
|
||||
private function processDateTime(Entity $entity): void
|
||||
{
|
||||
$dateStartFetchedString = $entity->getFetched('dateStart');
|
||||
$dateStartString = $entity->get('dateStart');
|
||||
$dateEndString = $entity->get('dateEnd');
|
||||
|
||||
if (!$dateStartFetchedString || !$dateStartString || !$dateEndString) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateStartFetched = DateTime::fromString($dateStartFetchedString);
|
||||
$dateStart = DateTime::fromString($dateStartString);
|
||||
$dateEnd = DateTime::fromString($dateEndString);
|
||||
|
||||
$diff = $dateStartFetched->diff($dateEnd);
|
||||
|
||||
$dateEndModified = $dateStart->add($diff);
|
||||
|
||||
$entity->set('dateEnd', $dateEndModified->getString());
|
||||
}
|
||||
|
||||
private function processDate(Entity $entity): void
|
||||
{
|
||||
$dateStartFetchedString = $entity->getFetched('dateStartDate');
|
||||
$dateStartString = $entity->get('dateStartDate');
|
||||
$dateEndString = $entity->get('dateEndDate');
|
||||
|
||||
if (!$dateStartFetchedString || !$dateStartString || !$dateEndString) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateStartFetched = Date::fromString($dateStartFetchedString);
|
||||
$dateStart = Date::fromString($dateStartString);
|
||||
$dateEnd = Date::fromString($dateEndString);
|
||||
|
||||
$diff = $dateStartFetched->diff($dateEnd);
|
||||
|
||||
$dateEndModified = $dateStart->add($diff);
|
||||
|
||||
$entity->set('dateEndDate', $dateEndModified->getString());
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class PortalOnlyAccount implements Filter
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $accountIdList */
|
||||
$accountIdList = $this->user->getLinkMultipleIdList('accounts');
|
||||
|
||||
if (count($accountIdList)) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\ImportError\OrderItemConverters;
|
||||
|
||||
use Espo\Core\Select\Order\ItemConverter;
|
||||
use Espo\Core\Select\Order\Item;
|
||||
|
||||
use Espo\ORM\Query\Part\OrderList;
|
||||
use Espo\ORM\Query\Part\Order;
|
||||
use Espo\ORM\Query\Part\Expression as Expr;
|
||||
|
||||
class ExportLineNumber implements ItemConverter
|
||||
{
|
||||
public function convert(Item $item): OrderList
|
||||
{
|
||||
return OrderList::create([
|
||||
Order
|
||||
::create(Expr::column('exportRowIndex'))
|
||||
->withDirection($item->getOrder())
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\ImportError\OrderItemConverters;
|
||||
|
||||
use Espo\Core\Select\Order\ItemConverter;
|
||||
use Espo\Core\Select\Order\Item;
|
||||
|
||||
use Espo\ORM\Query\Part\OrderList;
|
||||
use Espo\ORM\Query\Part\Order;
|
||||
use Espo\ORM\Query\Part\Expression as Expr;
|
||||
|
||||
class LineNumber implements ItemConverter
|
||||
{
|
||||
public function convert(Item $item): OrderList
|
||||
{
|
||||
return OrderList::create([
|
||||
Order
|
||||
::create(Expr::column('rowIndex'))
|
||||
->withDirection($item->getOrder())
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ class OnlyMyTeam implements Filter
|
||||
|
||||
public function apply(SelectBuilder $queryBuilder, OrGroupBuilder $orGroupBuilder): void
|
||||
{
|
||||
/** @var string[] */
|
||||
/** @var string[] $teamIdList */
|
||||
$teamIdList = $this->user->getLinkMultipleIdList('teams');
|
||||
|
||||
if (count($teamIdList) === 0) {
|
||||
|
||||
@@ -106,14 +106,16 @@ class Admin
|
||||
|
||||
|
||||
/**
|
||||
* @todo Use Request.
|
||||
*
|
||||
* @param array<string,mixed> $params
|
||||
* @param string $data
|
||||
* @return array{
|
||||
* id: string,
|
||||
* version: string,
|
||||
* }
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
* @todo Use Request.
|
||||
*
|
||||
*/
|
||||
public function postActionUploadUpgradePackage($params, $data): array
|
||||
{
|
||||
@@ -134,6 +136,10 @@ class Admin
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
*/
|
||||
public function postActionRunUpgrade(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
@@ -52,6 +52,11 @@ class Attachment extends RecordBase
|
||||
return parent::getActionList($request, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
*/
|
||||
public function postActionGetAttachmentFromImageUrl(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -69,6 +74,12 @@ class Attachment extends RecordBase
|
||||
->getValueMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
*/
|
||||
public function postActionGetCopiedAttachment(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -86,6 +97,11 @@ class Attachment extends RecordBase
|
||||
->getValueMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
*/
|
||||
public function getActionFile(Request $request, Response $response): void
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
@@ -103,6 +119,26 @@ class Attachment extends RecordBase
|
||||
->setBody($fileData->stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
*/
|
||||
public function postActionChunk(Request $request, Response $response): void
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
$body = $request->getBodyContents();
|
||||
|
||||
if (!$id || !$body) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getAttachmentService()->uploadChunk($id, $body);
|
||||
|
||||
$response->writeBody('true');
|
||||
}
|
||||
|
||||
private function getAttachmentService(): Service
|
||||
{
|
||||
/** @var Service */
|
||||
|
||||
@@ -29,22 +29,28 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
use Espo\Core\Controllers\Record;
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
use Espo\Entities\Email as EmailEntity;
|
||||
use Espo\Services\Email as Service;
|
||||
use Espo\Services\EmailTemplate as EmailTemplateService;
|
||||
|
||||
use Espo\Core\Utils\Crypt;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Email extends Record
|
||||
{
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionGetCopiedAttachments(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -61,89 +67,73 @@ class Email extends Record
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Move to service.
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionSendTestEmail(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (!$this->acl->checkScope('Email')) {
|
||||
if (!$this->acl->checkScope(EmailEntity::ENTITY_TYPE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (is_null($data->password)) {
|
||||
if ($data->type == 'preferences') {
|
||||
if (!$this->user->isAdmin() && $data->id !== $this->user->id) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
$data = get_object_vars($request->getParsedBody());
|
||||
|
||||
$preferences = $this->getEntityManager()->getEntity('Preferences', $data->id);
|
||||
$allowedParamList = [
|
||||
'type',
|
||||
'id',
|
||||
'username',
|
||||
'password',
|
||||
'auth',
|
||||
'authMechanism',
|
||||
'userId',
|
||||
'fromAddress',
|
||||
'fromName',
|
||||
'server',
|
||||
'port',
|
||||
'security',
|
||||
'emailAddress',
|
||||
];
|
||||
|
||||
if (!$preferences) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (is_null($data->password)) {
|
||||
$data->password = $this->getCrypt()->decrypt($preferences->get('smtpPassword'));
|
||||
}
|
||||
}
|
||||
else if ($data->type == 'emailAccount') {
|
||||
if (!$this->acl->checkScope('EmailAccount')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (!empty($data->id)) {
|
||||
$emailAccount = $this->getEntityManager()
|
||||
->getEntity('EmailAccount', $data->id);
|
||||
|
||||
if (!$emailAccount) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
if ($emailAccount->get('assignedUserId') !== $this->user->id) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($data->password)) {
|
||||
$data->password = $this->getCrypt()->decrypt($emailAccount->get('smtpPassword'));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ($data->type == 'inboundEmail') {
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (!empty($data->id)) {
|
||||
$emailAccount = $this->getEntityManager()->getEntity('InboundEmail', $data->id);
|
||||
|
||||
if (!$emailAccount) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (is_null($data->password)) {
|
||||
$data->password = $this->getCrypt()->decrypt($emailAccount->get('smtpPassword'));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (is_null($data->password)) {
|
||||
$data->password = $this->getConfig()->get('smtpPassword');
|
||||
}
|
||||
foreach (array_keys($data) as $key) {
|
||||
if (!in_array($key, $allowedParamList)) {
|
||||
throw new BadRequest("Not allowed parameter `{$key}`.");
|
||||
}
|
||||
}
|
||||
|
||||
$this->getEmailService()->sendTestEmail(get_object_vars($data));
|
||||
$emailAddress = $data['emailAddress'] ?? null;
|
||||
|
||||
if (!is_string($emailAddress)) {
|
||||
throw new BadRequest("No email address.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array{
|
||||
* type?: ?string,
|
||||
* id?: ?string,
|
||||
* username?: ?string,
|
||||
* password?: ?string,
|
||||
* auth?: bool,
|
||||
* authMechanism?: ?string,
|
||||
* userId?: ?string,
|
||||
* fromAddress?: ?string,
|
||||
* fromName?: ?string,
|
||||
* server: string,
|
||||
* port: int,
|
||||
* security: string,
|
||||
* emailAddress: string,
|
||||
* } $data
|
||||
*/
|
||||
|
||||
$this->getEmailService()->sendTestEmail($data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsRead(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -165,6 +155,9 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsNotRead(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -193,6 +186,9 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsImportant(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -214,6 +210,9 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsNotImportant(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -235,6 +234,9 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMoveToTrash(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -256,6 +258,9 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionRetrieveFromTrash(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -282,6 +287,9 @@ class Email extends Record
|
||||
return $this->getEmailService()->getFoldersNotReadCounts();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMoveToFolder(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -305,9 +313,12 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function getActionGetInsertFieldData(Request $request): stdClass
|
||||
{
|
||||
if (!$this->acl->checkScope('Email', 'create')) {
|
||||
if (!$this->acl->checkScope(EmailEntity::ENTITY_TYPE, Table::ACTION_CREATE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -329,10 +340,4 @@ class Email extends Record
|
||||
/** @var EmailTemplateService */
|
||||
return $this->getServiceFactory()->create('EmailTemplate');
|
||||
}
|
||||
|
||||
private function getCrypt(): Crypt
|
||||
{
|
||||
/** @var Crypt */
|
||||
return $this->getContainer()->get('crypt');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Mail\Account\PersonalAccount\Service;
|
||||
use Espo\Core\Mail\Account\Storage\Params as StorageParams;
|
||||
|
||||
@@ -44,6 +46,8 @@ class EmailAccount extends Record
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionGetFolders(Request $request): array
|
||||
{
|
||||
@@ -63,6 +67,10 @@ class EmailAccount extends Record
|
||||
return $this->getEmailAccountService()->getFolderList($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function postActionTestConnection(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
@@ -45,6 +45,8 @@ class EmailAddress extends RecordBase
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function actionSearchInAddressBook(Request $request): array
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ class ExternalAccount extends RecordBase
|
||||
$entity->get('enabled') &&
|
||||
$this->metadata->get('integrations.' . $entity->getId() .'.allowUserAccounts')
|
||||
) {
|
||||
/** @var string */
|
||||
/** @var string $id */
|
||||
$id = $entity->getId();
|
||||
|
||||
$userAccountAclScope = $this->metadata
|
||||
@@ -118,9 +118,13 @@ class ExternalAccount extends RecordBase
|
||||
|
||||
public function getActionRead(Request $request, Response $response): stdClass
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $id */
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
if ($id === '') {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
return $this->getRecordService()
|
||||
->read($id, ReadParams::create())
|
||||
->getValueMap();
|
||||
@@ -128,7 +132,7 @@ class ExternalAccount extends RecordBase
|
||||
|
||||
public function putActionUpdate(Request $request, Response $response): stdClass
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $id */
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
@@ -35,12 +35,12 @@ use Espo\{
|
||||
};
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\Conflict,
|
||||
Exceptions\Error,
|
||||
Exceptions\Forbidden,
|
||||
Exceptions\BadRequest,
|
||||
Api\Request,
|
||||
DataManager,
|
||||
};
|
||||
DataManager};
|
||||
|
||||
class FieldManager
|
||||
{
|
||||
@@ -50,6 +50,9 @@ class FieldManager
|
||||
|
||||
private $fieldManagerTool;
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function __construct(User $user, DataManager $dataManager, FieldManagerTool $fieldManagerTool)
|
||||
{
|
||||
$this->user = $user;
|
||||
@@ -59,6 +62,9 @@ class FieldManager
|
||||
$this->checkControllerAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
protected function checkControllerAccess(): void
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
@@ -68,6 +74,8 @@ class FieldManager
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function getActionRead(Request $request): array
|
||||
{
|
||||
@@ -83,6 +91,9 @@ class FieldManager
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Conflict
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionCreate(Request $request): array
|
||||
{
|
||||
@@ -113,6 +124,8 @@ class FieldManager
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function patchActionUpdate(Request $request): array
|
||||
{
|
||||
@@ -121,6 +134,8 @@ class FieldManager
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function putActionUpdate(Request $request): array
|
||||
{
|
||||
@@ -146,6 +161,10 @@ class FieldManager
|
||||
return $fieldManagerTool->read($scope, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function deleteActionDelete(Request $request): bool
|
||||
{
|
||||
$scope = $request->getRouteParam('scope');
|
||||
@@ -162,6 +181,10 @@ class FieldManager
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionResetToDefault(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -173,7 +196,6 @@ class FieldManager
|
||||
$this->fieldManagerTool->resetToDefault($data->scope, $data->name);
|
||||
|
||||
$this->dataManager->clearCache();
|
||||
|
||||
$this->dataManager->rebuildMetadata();
|
||||
|
||||
return true;
|
||||
|
||||
@@ -144,6 +144,25 @@ class Import extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
*/
|
||||
public function postActionExportErrors(Request $request): stdClass
|
||||
{
|
||||
$id = $request->getParsedBody()->id ?? null;
|
||||
|
||||
if (!$id) {
|
||||
throw new BadRequest("No `id`.");
|
||||
}
|
||||
|
||||
$attachmentId = $this->getImportService()->exportErrors($id);
|
||||
|
||||
return (object) [
|
||||
'attachmentId' => $attachmentId,
|
||||
];
|
||||
}
|
||||
|
||||
public function putActionUpdate(Request $request, Response $response): stdClass
|
||||
{
|
||||
throw new Forbidden();
|
||||
|
||||
36
application/Espo/Controllers/ImportError.php
Normal file
36
application/Espo/Controllers/ImportError.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Controllers\Record;
|
||||
|
||||
class ImportError extends Record
|
||||
{
|
||||
}
|
||||
@@ -44,6 +44,7 @@ class InboundEmail extends Record
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
*/
|
||||
public function postActionGetFolders(Request $request): array
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ class Integration
|
||||
|
||||
public function getActionRead(Request $request): stdClass
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $id */
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
$entity = $this->service->read($id);
|
||||
@@ -66,7 +66,7 @@ class Integration
|
||||
|
||||
public function putActionUpdate(Request $request): stdClass
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $id */
|
||||
$id = $request->getRouteParam('id');
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class Kanban
|
||||
|
||||
public function getActionGetData(Request $request): stdClass
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $entityType */
|
||||
$entityType = $request->getRouteParam('entityType');
|
||||
|
||||
$searchParams = $this->searchParamsFetcher->fetch($request);
|
||||
|
||||
@@ -29,10 +29,12 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Services\Layout as Service;
|
||||
use Espo\Entities\User;
|
||||
|
||||
@@ -50,6 +52,10 @@ class Layout
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function getActionRead(Request $request)
|
||||
{
|
||||
@@ -67,6 +73,10 @@ class Layout
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function putActionUpdate(Request $request)
|
||||
{
|
||||
@@ -95,6 +105,10 @@ class Layout
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionResetToDefault(Request $request)
|
||||
{
|
||||
@@ -113,6 +127,10 @@ class Layout
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function getActionGetOriginal(Request $request)
|
||||
{
|
||||
|
||||
@@ -99,6 +99,7 @@ class LeadCapture extends Record
|
||||
|
||||
/**
|
||||
* @return stdClass[]
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function getActionSmtpAccountDataList(): array
|
||||
{
|
||||
|
||||
@@ -119,6 +119,7 @@ class MassAction
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function prepareMassActionParams(stdClass $data): array
|
||||
{
|
||||
@@ -149,6 +150,9 @@ class MassAction
|
||||
throw new BadRequest("Bad search params for mass action.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
private function convertResult(ServiceResult $serviceResult): stdClass
|
||||
{
|
||||
if (!$serviceResult->hasResult()) {
|
||||
|
||||
@@ -49,6 +49,7 @@ class Metadata extends Base
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function getActionGet(Request $request)
|
||||
{
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
namespace Espo\Core;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl\GlobalRestriction,
|
||||
Acl\Table,
|
||||
Acl\Exceptions\NotImplemented,
|
||||
};
|
||||
Acl\Exceptions\NotImplemented};
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
@@ -283,7 +283,7 @@ class Acl
|
||||
/**
|
||||
* Get a restricted field list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedFieldList(string $scope, $type): array
|
||||
@@ -294,7 +294,7 @@ class Acl
|
||||
/**
|
||||
* Get a restricted attribute list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedAttributeList(string $scope, $type): array
|
||||
@@ -305,7 +305,7 @@ class Acl
|
||||
/**
|
||||
* Get a restricted link list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedLinkList(string $scope, $type): array
|
||||
|
||||
@@ -85,7 +85,7 @@ class AccessCheckerFactory
|
||||
*/
|
||||
private function getClassName(string $scope): string
|
||||
{
|
||||
/** @var ?class-string<AccessChecker> */
|
||||
/** @var ?class-string<AccessChecker> $className1 */
|
||||
$className1 = $this->metadata->get(['aclDefs', $scope, 'accessCheckerClassName']);
|
||||
|
||||
if ($className1) {
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Acl\AccessChecker\AccessCheckers;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use Espo\Core\Acl\DefaultAccessChecker;
|
||||
use Espo\Core\Acl\Traits\DefaultAccessCheckerDependency;
|
||||
use Espo\Core\Acl\AccessEntityCreateChecker;
|
||||
use Espo\Core\Acl\AccessEntityReadChecker;
|
||||
use Espo\Core\Acl\AccessEntityEditChecker;
|
||||
use Espo\Core\Acl\AccessEntityDeleteChecker;
|
||||
use Espo\Core\Acl\AccessEntityStreamChecker;
|
||||
use Espo\Core\Acl\ScopeData;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Access is determined by access to a foreign entity.
|
||||
*
|
||||
* @implements AccessEntityCreateChecker<Entity>
|
||||
* @implements AccessEntityReadChecker<Entity>
|
||||
* @implements AccessEntityEditChecker<Entity>
|
||||
* @implements AccessEntityDeleteChecker<Entity>
|
||||
* @implements AccessEntityStreamChecker<Entity>
|
||||
*/
|
||||
class Foreign implements
|
||||
|
||||
AccessEntityCreateChecker,
|
||||
AccessEntityReadChecker,
|
||||
AccessEntityEditChecker,
|
||||
AccessEntityDeleteChecker,
|
||||
AccessEntityStreamChecker
|
||||
{
|
||||
use DefaultAccessCheckerDependency;
|
||||
|
||||
private Metadata $metadata;
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
DefaultAccessChecker $defaultAccessChecker,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->defaultAccessChecker = $defaultAccessChecker;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
private function getForeignEntity(Entity $entity): ?Entity
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$link = $this->metadata->get(['aclDefs', $entityType, 'link']);
|
||||
|
||||
if (!$link) {
|
||||
throw new LogicException("No `link` in aclDefs for {$entityType}.");
|
||||
}
|
||||
|
||||
if ($entity->isNew()) {
|
||||
$foreignEntityType = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->getRelation($link)
|
||||
->getForeignEntityType();
|
||||
|
||||
/** @var ?string $id */
|
||||
$id = $entity->get($link . 'Id');
|
||||
|
||||
if (!$id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->entityManager->getEntityById($foreignEntityType, $id);
|
||||
}
|
||||
|
||||
return $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->getRelation($entity, $link)
|
||||
->findOne();
|
||||
}
|
||||
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityCreate($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityRead($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityEdit($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityDelete($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityStream($user, $entity, $data);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class AssignmentCheckerFactory
|
||||
*/
|
||||
private function getClassName(string $scope): string
|
||||
{
|
||||
/** @var ?class-string<AssignmentChecker<\Espo\ORM\Entity>> */
|
||||
/** @var ?class-string<AssignmentChecker<\Espo\ORM\Entity>> $className */
|
||||
$className = $this->metadata->get(['aclDefs', $scope, 'assignmentCheckerClassName']);
|
||||
|
||||
if ($className) {
|
||||
|
||||
@@ -228,7 +228,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $userTeamIdList */
|
||||
$userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
foreach ($newIdList as $id) {
|
||||
@@ -331,7 +331,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
|
||||
private function isPermittedAssignedUsersLevelNo(User $user, CoreEntity $entity): bool
|
||||
{
|
||||
/** @var string[] */
|
||||
/** @var string[] $userIdList */
|
||||
$userIdList = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS);
|
||||
|
||||
$fetchedAssignedUserIdList = $entity->getFetched(self::ATTR_ASSIGNED_USERS_IDS);
|
||||
@@ -351,12 +351,12 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
|
||||
private function isPermittedAssignedUsersLevelTeam(User $user, CoreEntity $entity): bool
|
||||
{
|
||||
/** @var string[] */
|
||||
/** @var string[] $userIdList */
|
||||
$userIdList = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS);
|
||||
|
||||
$fetchedAssignedUserIdList = $entity->getFetched(self::ATTR_ASSIGNED_USERS_IDS);
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $teamIdList */
|
||||
$teamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
foreach ($userIdList as $userId) {
|
||||
|
||||
@@ -85,7 +85,7 @@ class DefaultOwnershipChecker implements OwnershipOwnChecker, OwnershipTeamCheck
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $userTeamIdList */
|
||||
$userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
if (
|
||||
|
||||
@@ -33,7 +33,6 @@ use Espo\Core\{
|
||||
Utils\Metadata,
|
||||
Utils\DataCache,
|
||||
Utils\FieldUtil,
|
||||
Utils\Log,
|
||||
Utils\Config,
|
||||
};
|
||||
|
||||
@@ -43,68 +42,75 @@ use stdClass;
|
||||
* Lists of restricted fields can be obtained from here. Restricted fields
|
||||
* are specified in metadata > entityAcl.
|
||||
*/
|
||||
class GlobalRestricton
|
||||
class GlobalRestriction
|
||||
{
|
||||
/** Totally forbidden. */
|
||||
public const TYPE_FORBIDDEN = 'forbidden';
|
||||
|
||||
/** Reading forbidden, writing allowed. */
|
||||
public const TYPE_INTERNAL = 'internal';
|
||||
|
||||
/** Forbidden for non-admin users. */
|
||||
public const TYPE_ONLY_ADMIN = 'onlyAdmin';
|
||||
|
||||
/** Read-only for all users. */
|
||||
public const TYPE_READ_ONLY = 'readOnly';
|
||||
|
||||
/** Read-only for non-admin users. */
|
||||
public const TYPE_NON_ADMIN_READ_ONLY = 'nonAdminReadOnly';
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
* @var array<int,self::TYPE_*>
|
||||
*/
|
||||
protected $fieldTypeList = [
|
||||
'forbidden', // totally forbidden
|
||||
'internal', // reading forbidden, writing allowed
|
||||
'onlyAdmin', // forbidden for non admin users
|
||||
'readOnly', // read-only for all users
|
||||
'nonAdminReadOnly' // read-only for non-admin users
|
||||
private $fieldTypeList = [
|
||||
self::TYPE_FORBIDDEN,
|
||||
self::TYPE_INTERNAL,
|
||||
self::TYPE_ONLY_ADMIN,
|
||||
self::TYPE_READ_ONLY,
|
||||
self::TYPE_NON_ADMIN_READ_ONLY,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
* @var array<int,self::TYPE_*>
|
||||
*/
|
||||
protected $linkTypeList = [
|
||||
'forbidden', // totally forbidden
|
||||
'internal', // reading forbidden, writing allowed
|
||||
'onlyAdmin', // forbidden for non admin users
|
||||
'readOnly', // read-only for all users
|
||||
'nonAdminReadOnly' // read-only for non-admin users
|
||||
private $linkTypeList = [
|
||||
self::TYPE_FORBIDDEN,
|
||||
self::TYPE_INTERNAL,
|
||||
self::TYPE_ONLY_ADMIN,
|
||||
self::TYPE_READ_ONLY,
|
||||
self::TYPE_NON_ADMIN_READ_ONLY,
|
||||
];
|
||||
|
||||
/**
|
||||
* Types that should also be taken from entityDefs.
|
||||
* @var array<int,self::TYPE_*>
|
||||
*/
|
||||
private array $entityDefsTypeList = [
|
||||
self::TYPE_READ_ONLY,
|
||||
];
|
||||
|
||||
private ?stdClass $data = null;
|
||||
|
||||
protected string $cacheKey = 'entityAcl';
|
||||
private string $cacheKey = 'entityAcl';
|
||||
|
||||
private Metadata $metadata;
|
||||
|
||||
private DataCache $dataCache;
|
||||
|
||||
private FieldUtil $fieldUtil;
|
||||
|
||||
private Log $log;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
DataCache $dataCache,
|
||||
FieldUtil $fieldUtil,
|
||||
Log $log,
|
||||
Config $config
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->dataCache = $dataCache;
|
||||
$this->fieldUtil = $fieldUtil;
|
||||
$this->log = $log;
|
||||
|
||||
$useCache = $config->get('useCache');
|
||||
|
||||
if ($useCache && $this->dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass */
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $this->dataCache->get($this->cacheKey);
|
||||
|
||||
$this->data = $cachedData;
|
||||
@@ -121,25 +127,25 @@ class GlobalRestricton
|
||||
}
|
||||
}
|
||||
|
||||
protected function storeCacheFile(): void
|
||||
private function storeCacheFile(): void
|
||||
{
|
||||
assert($this->data !== null);
|
||||
|
||||
$this->dataCache->store($this->cacheKey, $this->data);
|
||||
}
|
||||
|
||||
protected function buildData(): void
|
||||
private function buildData(): void
|
||||
{
|
||||
/** @var string[] */
|
||||
$scopeList = array_keys($this->metadata->get(['entityDefs'], []));
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['entityDefs']) ?? []);
|
||||
|
||||
$data = (object) [];
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
/** @var string[] */
|
||||
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields'], []));
|
||||
/** @var string[] */
|
||||
$linkList = array_keys($this->metadata->get(['entityDefs', $scope, 'links'], []));
|
||||
/** @var string[] $fieldList */
|
||||
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
|
||||
/** @var string[] $linkList */
|
||||
$linkList = array_keys($this->metadata->get(['entityDefs', $scope, 'links']) ?? []);
|
||||
|
||||
$isNotEmpty = false;
|
||||
|
||||
@@ -154,16 +160,24 @@ class GlobalRestricton
|
||||
$resultAttributeList = [];
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
if ($this->metadata->get(['entityAcl', $scope, 'fields', $field, $type])) {
|
||||
$isNotEmpty = true;
|
||||
$value = $this->metadata->get(['entityAcl', $scope, 'fields', $field, $type]);
|
||||
|
||||
$resultFieldList[] = $field;
|
||||
if (!$value && in_array($type, $this->entityDefsTypeList)) {
|
||||
$value = $this->metadata->get(['entityDefs', $scope, 'fields', $field, $type]);
|
||||
}
|
||||
|
||||
$fieldAttributeList = $this->fieldUtil->getAttributeList($scope, $field);
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($fieldAttributeList as $attribute) {
|
||||
$resultAttributeList[] = $attribute;
|
||||
}
|
||||
$isNotEmpty = true;
|
||||
|
||||
$resultFieldList[] = $field;
|
||||
|
||||
$fieldAttributeList = $this->fieldUtil->getAttributeList($scope, $field);
|
||||
|
||||
foreach ($fieldAttributeList as $attribute) {
|
||||
$resultAttributeList[] = $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,12 +189,21 @@ class GlobalRestricton
|
||||
$resultLinkList = [];
|
||||
|
||||
foreach ($linkList as $link) {
|
||||
if ($this->metadata->get(['entityAcl', $scope, 'links', $link, $type])) {
|
||||
$isNotEmpty = true;
|
||||
$value = $this->metadata->get(['entityAcl', $scope, 'links', $link, $type]);
|
||||
|
||||
$resultLinkList[] = $link;
|
||||
if (!$value && in_array($type, $this->entityDefsTypeList)) {
|
||||
$value = $this->metadata->get(['entityDefs', $scope, 'links', $link, $type]);
|
||||
}
|
||||
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isNotEmpty = true;
|
||||
|
||||
$resultLinkList[] = $link;
|
||||
}
|
||||
|
||||
$scopeData->links->$type = $resultLinkList;
|
||||
}
|
||||
|
||||
@@ -193,6 +216,7 @@ class GlobalRestricton
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::TYPE_* $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedFieldList(string $scope, string $type): array
|
||||
@@ -215,6 +239,7 @@ class GlobalRestricton
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::TYPE_* $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedAttributeList(string $scope, string $type): array
|
||||
@@ -237,6 +262,7 @@ class GlobalRestricton
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::TYPE_* $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedLinkList(string $scope, string $type): array
|
||||
@@ -95,7 +95,7 @@ class Map
|
||||
$this->cacheKey = $cacheKeyProvider->get();
|
||||
|
||||
if ($this->config->get('useCache') && $this->dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass */
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $this->dataCache->get($this->cacheKey);
|
||||
|
||||
$this->data = $cachedData;
|
||||
|
||||
@@ -80,7 +80,7 @@ class OwnershipCheckerFactory
|
||||
*/
|
||||
private function getClassName(string $scope): string
|
||||
{
|
||||
/** @var ?class-string<OwnershipChecker> */
|
||||
/** @var ?class-string<OwnershipChecker> $className */
|
||||
$className = $this->metadata->get(['aclDefs', $scope, 'ownershipCheckerClassName']);
|
||||
|
||||
if ($className) {
|
||||
|
||||
@@ -53,7 +53,7 @@ class DefaultRoleListProvider implements RoleListProvider
|
||||
{
|
||||
$roleList = [];
|
||||
|
||||
/** @var iterable<RoleEntity> */
|
||||
/** @var iterable<RoleEntity> $userRoleList */
|
||||
$userRoleList = $this->entityManager
|
||||
->getRDBRepository('User')
|
||||
->getRelation($this->user, 'roles')
|
||||
@@ -63,14 +63,14 @@ class DefaultRoleListProvider implements RoleListProvider
|
||||
$roleList[] = $role;
|
||||
}
|
||||
|
||||
/** @var iterable<\Espo\Entities\Team> */
|
||||
/** @var iterable<\Espo\Entities\Team> $teamList */
|
||||
$teamList = $this->entityManager
|
||||
->getRDBRepository('User')
|
||||
->getRelation($this->user, 'teams')
|
||||
->find();
|
||||
|
||||
foreach ($teamList as $team) {
|
||||
/** @var iterable<RoleEntity> */
|
||||
/** @var iterable<RoleEntity> $teamRoleList */
|
||||
$teamRoleList = $this->entityManager
|
||||
->getRDBRepository('Team')
|
||||
->getRelation($team, 'roles')
|
||||
|
||||
@@ -146,7 +146,7 @@ class DefaultTable implements Table
|
||||
$this->cacheKey = $cacheKeyProvider->get();
|
||||
|
||||
if ($config->get('useCache') && $dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass */
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $dataCache->get($this->cacheKey);
|
||||
|
||||
$this->data = $cachedData;
|
||||
@@ -248,6 +248,7 @@ class DefaultTable implements Table
|
||||
$fieldTable = (object) [];
|
||||
|
||||
$this->applyHighest($aclTable, $fieldTable);
|
||||
$this->applyDisabled($aclTable, $fieldTable);
|
||||
$this->applyAdminMandatory($aclTable, $fieldTable);
|
||||
}
|
||||
|
||||
@@ -526,10 +527,6 @@ class DefaultTable implements Table
|
||||
|
||||
protected function applyDisabled(stdClass &$table, stdClass &$fieldTable): void
|
||||
{
|
||||
if ($this->user->isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->getScopeList() as $scope) {
|
||||
if ($this->metadata->get(['scopes', $scope, 'disabled'])) {
|
||||
$table->$scope = false;
|
||||
|
||||
@@ -37,7 +37,7 @@ use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl,
|
||||
Acl\GlobalRestricton,
|
||||
Acl\GlobalRestriction,
|
||||
Acl\OwnerUserFieldProvider,
|
||||
Acl\Table\TableFactory,
|
||||
Acl\Table,
|
||||
@@ -140,9 +140,9 @@ class AclManager
|
||||
private $mapFactory;
|
||||
|
||||
/**
|
||||
* @var GlobalRestricton
|
||||
* @var GlobalRestriction
|
||||
*/
|
||||
protected $globalRestricton;
|
||||
protected $globalRestriction;
|
||||
|
||||
/**
|
||||
* @var OwnerUserFieldProvider
|
||||
@@ -159,7 +159,7 @@ class AclManager
|
||||
OwnershipCheckerFactory $ownershipCheckerFactory,
|
||||
TableFactory $tableFactory,
|
||||
MapFactory $mapFactory,
|
||||
GlobalRestricton $globalRestricton,
|
||||
GlobalRestriction $globalRestriction,
|
||||
OwnerUserFieldProvider $ownerUserFieldProvider,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
@@ -167,7 +167,7 @@ class AclManager
|
||||
$this->ownershipCheckerFactory = $ownershipCheckerFactory;
|
||||
$this->tableFactory = $tableFactory;
|
||||
$this->mapFactory = $mapFactory;
|
||||
$this->globalRestricton = $globalRestricton;
|
||||
$this->globalRestriction = $globalRestriction;
|
||||
$this->ownerUserFieldProvider = $ownerUserFieldProvider;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
@@ -487,27 +487,27 @@ class AclManager
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @return array<int,GlobalRestriction::TYPE_*>
|
||||
*/
|
||||
protected function getGlobalRestrictionTypeList(User $user, string $action = Table::ACTION_READ): array
|
||||
{
|
||||
$typeList = [
|
||||
GlobalRestricton::TYPE_FORBIDDEN,
|
||||
GlobalRestriction::TYPE_FORBIDDEN,
|
||||
];
|
||||
|
||||
if ($action === Table::ACTION_READ) {
|
||||
$typeList[] = GlobalRestricton::TYPE_INTERNAL;
|
||||
$typeList[] = GlobalRestriction::TYPE_INTERNAL;
|
||||
}
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$typeList[] = GlobalRestricton::TYPE_ONLY_ADMIN;
|
||||
$typeList[] = GlobalRestriction::TYPE_ONLY_ADMIN;
|
||||
}
|
||||
|
||||
if ($action === Table::ACTION_EDIT) {
|
||||
$typeList[] = GlobalRestricton::TYPE_READ_ONLY;
|
||||
$typeList[] = GlobalRestriction::TYPE_READ_ONLY;
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$typeList[] = GlobalRestricton::TYPE_NON_ADMIN_READ_ONLY;
|
||||
$typeList[] = GlobalRestriction::TYPE_NON_ADMIN_READ_ONLY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +596,7 @@ class AclManager
|
||||
*/
|
||||
public function checkUserPermission(User $user, $target, string $permissionType = 'user'): bool
|
||||
{
|
||||
$permission = $this->get($user, $permissionType);
|
||||
$permission = $this->getPermissionLevel($user, $permissionType);
|
||||
|
||||
if (is_object($target)) {
|
||||
$userId = $target->getId();
|
||||
@@ -618,11 +618,11 @@ class AclManager
|
||||
}
|
||||
|
||||
if ($permission === Table::LEVEL_TEAM) {
|
||||
/** @var string[] */
|
||||
/** @var string[] $teamIdList */
|
||||
$teamIdList = $user->getLinkMultipleIdList('teams');
|
||||
|
||||
/** @var \Espo\Repositories\User $userRepository */
|
||||
$userRepository = $this->entityManager->getRepository('User');
|
||||
$userRepository = $this->entityManager->getRepository(User::ENTITY_TYPE);
|
||||
|
||||
if (!$userRepository->checkBelongsToAnyOfTeams($userId, $teamIdList)) {
|
||||
return false;
|
||||
@@ -658,7 +658,7 @@ class AclManager
|
||||
/**
|
||||
* Get a restricted field list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedFieldList(string $scope, $type): array
|
||||
@@ -671,20 +671,20 @@ class AclManager
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->globalRestricton->getScopeRestrictedFieldList($scope, $type)
|
||||
$this->globalRestriction->getScopeRestrictedFieldList($scope, $type)
|
||||
);
|
||||
}
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
return $this->globalRestricton->getScopeRestrictedFieldList($scope, $type);
|
||||
return $this->globalRestriction->getScopeRestrictedFieldList($scope, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a restricted attribute list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedAttributeList(string $scope, $type): array
|
||||
@@ -697,20 +697,20 @@ class AclManager
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->globalRestricton->getScopeRestrictedAttributeList($scope, $type)
|
||||
$this->globalRestriction->getScopeRestrictedAttributeList($scope, $type)
|
||||
);
|
||||
}
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
return $this->globalRestricton->getScopeRestrictedAttributeList($scope, $type);
|
||||
return $this->globalRestriction->getScopeRestrictedAttributeList($scope, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a restricted link list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedLinkList(string $scope, $type): array
|
||||
@@ -723,19 +723,19 @@ class AclManager
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->globalRestricton->getScopeRestrictedLinkList($scope, $type)
|
||||
$this->globalRestriction->getScopeRestrictedLinkList($scope, $type)
|
||||
);
|
||||
}
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
return $this->globalRestricton->getScopeRestrictedLinkList($scope, $type);
|
||||
return $this->globalRestriction->getScopeRestrictedLinkList($scope, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 getReadOwnerUserField(string $entityType): ?string
|
||||
{
|
||||
|
||||
@@ -65,8 +65,10 @@ class ActionFactory
|
||||
|
||||
/**
|
||||
* @param array<string,object> $with
|
||||
* @deprecated
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @todo Remove.
|
||||
* @deprecated
|
||||
*/
|
||||
public function createWith(string $action, ?string $entityType, array $with): Action
|
||||
{
|
||||
|
||||
@@ -209,7 +209,7 @@ class Merger
|
||||
{
|
||||
$list = [];
|
||||
|
||||
/** @var iterable<PhoneNumber> */
|
||||
/** @var iterable<PhoneNumber> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, 'phoneNumbers')
|
||||
@@ -229,7 +229,7 @@ class Merger
|
||||
{
|
||||
$list = [];
|
||||
|
||||
/** @var iterable<EmailAddress> */
|
||||
/** @var iterable<EmailAddress> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, 'emailAddresses')
|
||||
|
||||
@@ -135,6 +135,7 @@ class ActionProcessor
|
||||
|
||||
/**
|
||||
* @param mixed $result
|
||||
* @throws \JsonException
|
||||
*/
|
||||
private function handleResult(Response $response, $result): void
|
||||
{
|
||||
@@ -177,7 +178,7 @@ class ActionProcessor
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var class-string */
|
||||
/** @var class-string $className */
|
||||
$className = $type->getName();
|
||||
|
||||
$firstParamClass = new ReflectionClass($className);
|
||||
|
||||
@@ -33,8 +33,6 @@ use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\ServiceUnavailable;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Authentication\Authentication;
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
use Espo\Core\Authentication\Result;
|
||||
@@ -69,6 +67,10 @@ class Auth
|
||||
$this->isEntryPoint = $isEntryPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Exception
|
||||
*/
|
||||
public function process(Request $request, Response $response): AuthResult
|
||||
{
|
||||
$username = null;
|
||||
@@ -87,6 +89,18 @@ class Auth
|
||||
|
||||
$hasAuthData = (bool) ($username || $authenticationMethod);
|
||||
|
||||
if (!$hasAuthData) {
|
||||
$password = $this->obtainTokenFromCookies($request);
|
||||
|
||||
if ($password) {
|
||||
$authenticationData = AuthenticationData::create()
|
||||
->withPassword($password)
|
||||
->withByTokenOnly(true);
|
||||
|
||||
$hasAuthData = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->authRequired && !$this->isEntryPoint && $hasAuthData) {
|
||||
$authResult = $this->processAuthNotRequired(
|
||||
$authenticationData,
|
||||
@@ -118,6 +132,9 @@ class Auth
|
||||
return AuthResult::createNotResolved();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function processAuthNotRequired(
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
@@ -140,6 +157,9 @@ class Auth
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function processWithAuthData(
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
@@ -178,7 +198,7 @@ class Auth
|
||||
*/
|
||||
protected function decodeAuthorizationString(string $string): array
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $stringDecoded */
|
||||
$stringDecoded = base64_decode($string);
|
||||
|
||||
if (strpos($stringDecoded, ':') === false) {
|
||||
@@ -205,6 +225,9 @@ class Auth
|
||||
$response->writeBody(Json::encode($bodyData));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function handleException(Response $response, Exception $e): void
|
||||
{
|
||||
if (
|
||||
@@ -269,6 +292,7 @@ class Auth
|
||||
|
||||
/**
|
||||
* @return array{?string,?string}
|
||||
* @throws BadRequest
|
||||
*/
|
||||
protected function obtainUsernamePasswordFromRequest(Request $request): array
|
||||
{
|
||||
@@ -290,17 +314,6 @@ class Auth
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
if (
|
||||
$request->getCookieParam('auth-username') &&
|
||||
$request->getCookieParam('auth-token')
|
||||
) {
|
||||
|
||||
$username = $request->getCookieParam('auth-username');
|
||||
$password = $request->getCookieParam('auth-token');
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
$cgiAuthString = $request->getHeader('Http-Espo-Cgi-Auth') ??
|
||||
$request->getHeader('Redirect-Http-Espo-Cgi-Auth');
|
||||
|
||||
@@ -312,4 +325,9 @@ class Auth
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
private function obtainTokenFromCookies(Request $request): ?string
|
||||
{
|
||||
return $request->getCookieParam('auth-token');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,12 @@
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\Error,
|
||||
Authentication\Authentication,
|
||||
Utils\Log,
|
||||
};
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Builds Auth instance.
|
||||
*/
|
||||
@@ -77,7 +78,7 @@ class AuthBuilder
|
||||
public function build(): Auth
|
||||
{
|
||||
if (!$this->authentication) {
|
||||
throw new Error("Authentication is not set.");
|
||||
throw new RuntimeException("Authentication is not set.");
|
||||
}
|
||||
|
||||
return new Auth($this->log, $this->authentication, $this->authRequired, $this->isEntryPoint);
|
||||
|
||||
@@ -34,6 +34,7 @@ use Espo\Core\Exceptions\HasBody;
|
||||
use Espo\Core\{
|
||||
Api\Request,
|
||||
Api\Response,
|
||||
Exceptions\HasLogMessage,
|
||||
Utils\Log,
|
||||
Utils\Config,
|
||||
};
|
||||
@@ -121,14 +122,16 @@ class ErrorOutput
|
||||
$message = $exception->getMessage();
|
||||
$statusCode = $exception->getCode();
|
||||
|
||||
if ($exception instanceof HasLogMessage) {
|
||||
$message = $exception->getLogMessage();
|
||||
}
|
||||
|
||||
if ($route) {
|
||||
$this->processRoute($route, $request, $exception);
|
||||
}
|
||||
|
||||
$logLevel = 'error';
|
||||
|
||||
$messageLineFile = null;
|
||||
|
||||
$messageLineFile =
|
||||
'line: ' . $exception->getLine() . ', ' .
|
||||
'file: ' . $exception->getFile();
|
||||
@@ -176,10 +179,10 @@ class ErrorOutput
|
||||
}
|
||||
|
||||
if ($toPrintBody) {
|
||||
$codeDesription = $this->getCodeDescription($statusCode);
|
||||
$codeDescription = $this->getCodeDescription($statusCode);
|
||||
|
||||
$statusText = isset($codeDesription) ?
|
||||
$statusCode . ' '. $codeDesription :
|
||||
$statusText = isset($codeDescription) ?
|
||||
$statusCode . ' '. $codeDescription :
|
||||
'HTTP ' . $statusCode;
|
||||
|
||||
if ($message) {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Authentication\AuthenticationFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
@@ -37,6 +37,7 @@ use Espo\Core\ApplicationUser;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Processes requests. Handles authentication. Obtains a controller name, action, body from a request.
|
||||
@@ -113,6 +114,10 @@ class RequestProcessor
|
||||
ob_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function proceed(Request $request, Response $response): void
|
||||
{
|
||||
$this->beforeProceed($response);
|
||||
@@ -129,7 +134,7 @@ class RequestProcessor
|
||||
$actionName = $crudList[$httpMethod] ?? null;
|
||||
|
||||
if (!$actionName) {
|
||||
throw new Error("No action for method {$httpMethod}.");
|
||||
throw new BadRequest("No action for method {$httpMethod}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +148,7 @@ class RequestProcessor
|
||||
$controllerName = $request->getRouteParam('controller');
|
||||
|
||||
if (!$controllerName) {
|
||||
throw new Error("Route doesn't have specified controller.");
|
||||
throw new LogicException("Route doesn't have specified controller.");
|
||||
}
|
||||
|
||||
return ucfirst($controllerName);
|
||||
@@ -174,6 +179,7 @@ class RequestProcessor
|
||||
private function afterProceed(Response $response): void
|
||||
{
|
||||
$response
|
||||
->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'))
|
||||
->setHeader('Expires', '0')
|
||||
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
|
||||
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0')
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Psr\Http\Message\{
|
||||
ServerRequestInterface as Psr7Request,
|
||||
UriInterface,
|
||||
@@ -181,30 +184,44 @@ class RequestWrapper implements ApiRequest
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function getParsedBody(): stdClass
|
||||
{
|
||||
if ($this->parsedBody === null) {
|
||||
$this->initParsedBody();
|
||||
}
|
||||
|
||||
assert($this->parsedBody !== null);
|
||||
if ($this->parsedBody === null) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
return Util::cloneObject($this->parsedBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function initParsedBody(): void
|
||||
{
|
||||
$contents = $this->getBodyContents();
|
||||
|
||||
if ($this->getContentType() === 'application/json' && $contents) {
|
||||
$this->parsedBody = json_decode($contents);
|
||||
$parsedBody = Json::decode($contents);
|
||||
|
||||
if (is_array($this->parsedBody)) {
|
||||
$this->parsedBody = (object) [
|
||||
'list' => $this->parsedBody,
|
||||
if (is_array($parsedBody)) {
|
||||
$parsedBody = (object) [
|
||||
'list' => $parsedBody,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$parsedBody instanceof stdClass) {
|
||||
throw new BadRequest("Body is not a JSON object.");
|
||||
}
|
||||
|
||||
$this->parsedBody = $parsedBody;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class Application
|
||||
|
||||
protected function initContainer(): void
|
||||
{
|
||||
/** @var Container */
|
||||
/** @var Container $container */
|
||||
$container = (new ContainerBuilder())->build();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Core\ApplicationRunners;
|
||||
use Espo\Core\{
|
||||
Application\Runner,
|
||||
DataManager,
|
||||
Exceptions\Error,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,9 @@ class ClearCache implements Runner
|
||||
$this->dataManager = $dataManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->dataManager->clearCache();
|
||||
|
||||
@@ -42,7 +42,7 @@ class Command implements Runner
|
||||
use Cli;
|
||||
use SetupSystemUser;
|
||||
|
||||
private $commandManager;
|
||||
private ConsoleCommandManager $commandManager;
|
||||
|
||||
public function __construct(ConsoleCommandManager $commandManager)
|
||||
{
|
||||
@@ -52,10 +52,14 @@ class Command implements Runner
|
||||
public function run(): void
|
||||
{
|
||||
try {
|
||||
$this->commandManager->run($_SERVER['argv']);
|
||||
$exitStatus = $this->commandManager->run($_SERVER['argv']);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
exit($exitStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,10 @@
|
||||
|
||||
namespace Espo\Core\ApplicationRunners;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\{
|
||||
Application\RunnerParameterized,
|
||||
Application\Runner\Params,
|
||||
Exceptions\BadRequest,
|
||||
Exceptions\NotFound,
|
||||
Utils\ClientManager,
|
||||
Utils\Config,
|
||||
@@ -42,8 +41,7 @@ use Espo\Core\{
|
||||
Portal\Utils\Url,
|
||||
Api\ErrorOutput,
|
||||
Api\RequestWrapper,
|
||||
Api\ResponseWrapper,
|
||||
};
|
||||
Api\ResponseWrapper};
|
||||
|
||||
use Slim\{
|
||||
ResponseEmitter,
|
||||
@@ -89,7 +87,7 @@ class PortalClient implements RunnerParameterized
|
||||
$responseWrapped = new ResponseWrapper(new Response());
|
||||
|
||||
if ($requestWrapped->getMethod() !== 'GET') {
|
||||
throw new Error("Only GET request is allowed.");
|
||||
throw new BadRequest("Only GET request is allowed.");
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -33,12 +33,14 @@ use Espo\Core\Exceptions\Error;
|
||||
use Espo\Entities\Portal as PortalEntity;
|
||||
use Espo\Entities\User as UserEntity;
|
||||
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Provides information about an application, current user, portal.
|
||||
*/
|
||||
class ApplicationState
|
||||
{
|
||||
private $container;
|
||||
private Container $container;
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
@@ -59,7 +61,7 @@ class ApplicationState
|
||||
public function getPortalId(): string
|
||||
{
|
||||
if (!$this->isPortal()) {
|
||||
throw new Error("Can't get portal ID for non-portal application.");
|
||||
throw new LogicException("Can't get portal ID for non-portal application.");
|
||||
}
|
||||
|
||||
return $this->getPortal()->getId();
|
||||
@@ -71,7 +73,7 @@ class ApplicationState
|
||||
public function getPortal(): PortalEntity
|
||||
{
|
||||
if (!$this->isPortal()) {
|
||||
throw new Error("Can't get portal for non-portal application.");
|
||||
throw new LogicException("Can't get portal for non-portal application.");
|
||||
}
|
||||
|
||||
/** @var PortalEntity */
|
||||
@@ -92,7 +94,7 @@ class ApplicationState
|
||||
public function getUser(): UserEntity
|
||||
{
|
||||
if (!$this->hasUser()) {
|
||||
throw new Error("User is not yet available.");
|
||||
throw new LogicException("User is not yet available.");
|
||||
}
|
||||
|
||||
/** @var UserEntity */
|
||||
|
||||
@@ -29,10 +29,6 @@
|
||||
|
||||
namespace Espo\Core;
|
||||
|
||||
use Espo\Core\Exceptions\{
|
||||
Error,
|
||||
};
|
||||
|
||||
use Espo\Entities\{
|
||||
User,
|
||||
};
|
||||
@@ -41,6 +37,8 @@ use Espo\Core\{
|
||||
ORM\EntityManagerProxy,
|
||||
};
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Setting a current user for the application.
|
||||
*/
|
||||
@@ -57,14 +55,14 @@ class ApplicationUser
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the system user as a current user. The system user is used when no user is logged in.
|
||||
* Set up the system user as a current user. The system user is used when no user is logged in.
|
||||
*/
|
||||
public function setupSystemUser(): void
|
||||
{
|
||||
$user = $this->entityManagerProxy->getEntity('User', 'system');
|
||||
|
||||
if (!$user) {
|
||||
throw new Error("System user is not found.");
|
||||
throw new RuntimeException("System user is not found.");
|
||||
}
|
||||
|
||||
$user->set('ipAddress', $_SERVER['REMOTE_ADDR'] ?? null);
|
||||
|
||||
@@ -50,7 +50,7 @@ class EspoManager implements Manager
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
|
||||
/** @var RDBRepository<AuthTokenEntity> */
|
||||
/** @var RDBRepository<AuthTokenEntity> $repository */
|
||||
$repository = $entityManager->getRDBRepository(AuthTokenEntity::ENTITY_TYPE);
|
||||
|
||||
$this->repository = $repository;
|
||||
@@ -58,7 +58,7 @@ class EspoManager implements Manager
|
||||
|
||||
public function get(string $token): ?AuthToken
|
||||
{
|
||||
/** @var ?AuthTokenEntity */
|
||||
/** @var ?AuthTokenEntity $authToken */
|
||||
$authToken = $this->entityManager
|
||||
->getRDBRepository(AuthTokenEntity::ENTITY_TYPE)
|
||||
->select([
|
||||
@@ -83,7 +83,7 @@ class EspoManager implements Manager
|
||||
|
||||
public function create(Data $data): AuthToken
|
||||
{
|
||||
/** @var AuthTokenEntity */
|
||||
/** @var AuthTokenEntity $authToken */
|
||||
$authToken = $this->repository->getNew();
|
||||
|
||||
$authToken->set([
|
||||
@@ -169,7 +169,7 @@ class EspoManager implements Manager
|
||||
}
|
||||
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
/** @var string */
|
||||
/** @var string $randomValue */
|
||||
$randomValue = openssl_random_pseudo_bytes($length);
|
||||
|
||||
return bin2hex($randomValue);
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
namespace Espo\Core\Authentication;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ServiceUnavailable;
|
||||
|
||||
use Espo\Repositories\UserData as UserDataRepository;
|
||||
@@ -43,9 +42,7 @@ use Espo\Entities\{
|
||||
};
|
||||
|
||||
use Espo\Core\Authentication\{
|
||||
Result,
|
||||
Result\FailReason,
|
||||
LoginFactory,
|
||||
TwoFactor\LoginFactory as TwoFactorLoginFactory,
|
||||
AuthToken\Manager as AuthTokenManager,
|
||||
AuthToken\Data as AuthTokenData,
|
||||
@@ -124,7 +121,6 @@ class Authentication
|
||||
*
|
||||
* Warning: This method can change the state of the object (by setting the `portal` prop.).
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws ServiceUnavailable
|
||||
*/
|
||||
public function login(AuthenticationData $data, Request $request, Response $response): Result
|
||||
@@ -132,14 +128,14 @@ class Authentication
|
||||
$username = $data->getUsername();
|
||||
$password = $data->getPassword();
|
||||
$authenticationMethod = $data->getMethod();
|
||||
$byTokenOnly = $data->byTokenOnly();
|
||||
|
||||
if (
|
||||
$authenticationMethod &&
|
||||
!$this->configDataProvider->authenticationMethodIsApi($authenticationMethod)
|
||||
) {
|
||||
$this->log->warning(
|
||||
"AUTH: Trying to use not allowed authentication method '{$authenticationMethod}'."
|
||||
);
|
||||
$this->log
|
||||
->warning("AUTH: Trying to use not allowed authentication method '{$authenticationMethod}'.");
|
||||
|
||||
return $this->processFail(
|
||||
Result::fail(FailReason::METHOD_NOT_ALLOWED),
|
||||
@@ -184,9 +180,13 @@ class Authentication
|
||||
}
|
||||
}
|
||||
|
||||
$isByTokenOnly = !$authenticationMethod && $request->getHeader('Espo-Authorization-By-Token') === 'true';
|
||||
$byTokenAndUsername = $request->getHeader('Espo-Authorization-By-Token') === 'true';
|
||||
|
||||
if ($isByTokenOnly && !$authToken) {
|
||||
if ($authenticationMethod && $byTokenAndUsername) {
|
||||
return Result::fail(FailReason::DISCREPANT_DATA);
|
||||
}
|
||||
|
||||
if (($byTokenAndUsername || $byTokenOnly) && !$authToken) {
|
||||
if ($username) {
|
||||
$this->log->info(
|
||||
"AUTH: Trying to login as user '{$username}' by token but token is not found."
|
||||
@@ -200,6 +200,20 @@ class Authentication
|
||||
);
|
||||
}
|
||||
|
||||
if ($byTokenOnly) {
|
||||
assert($authToken !== null);
|
||||
|
||||
$username = $this->getUsernameByAuthToken($authToken);
|
||||
|
||||
if (!$username) {
|
||||
return $this->processFail(
|
||||
Result::fail(FailReason::USER_NOT_FOUND),
|
||||
$data,
|
||||
$request
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$authenticationMethod) {
|
||||
$authenticationMethod = $this->configDataProvider->getDefaultAuthenticationMethod();
|
||||
}
|
||||
@@ -354,7 +368,7 @@ class Authentication
|
||||
private function processAuthTokenCheck(AuthToken $authToken): bool
|
||||
{
|
||||
if ($this->allowAnyAccess && $authToken->getPortalId() && !$this->isPortal()) {
|
||||
/** @var ?Portal */
|
||||
/** @var ?Portal $portal */
|
||||
$portal = $this->entityManager->getEntity('Portal', $authToken->getPortalId());
|
||||
|
||||
if ($portal) {
|
||||
@@ -504,7 +518,7 @@ class Authentication
|
||||
);
|
||||
|
||||
if ($createSecret) {
|
||||
$this->setSecretInCookie($authToken->getSecret(), $response);
|
||||
$this->setSecretInCookie($authToken->getSecret(), $response, $request);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -563,7 +577,7 @@ class Authentication
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var AuthLogRecord */
|
||||
/** @var AuthLogRecord $authLogRecord */
|
||||
$authLogRecord = $this->entityManager->getNewEntity('AuthLogRecord');
|
||||
|
||||
$requestUrl =
|
||||
@@ -612,7 +626,7 @@ class Authentication
|
||||
$this->entityManager->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
private function setSecretInCookie(?string $secret, Response $response): void
|
||||
private function setSecretInCookie(?string $secret, Response $response, ?Request $request = null): void
|
||||
{
|
||||
$time = $secret ? strtotime('+1000 days') : 1;
|
||||
|
||||
@@ -625,9 +639,36 @@ class Authentication
|
||||
'; HttpOnly' .
|
||||
'; SameSite=Lax';
|
||||
|
||||
if ($request && self::isSecureRequest($request)) {
|
||||
$headerValue .= "; Secure";
|
||||
}
|
||||
|
||||
$response->addHeader('Set-Cookie', $headerValue);
|
||||
}
|
||||
|
||||
private static function isSecureRequest(Request $request): bool
|
||||
{
|
||||
$https = $request->getServerParam('HTTPS');
|
||||
|
||||
if ($https === 'on') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$scheme = $request->getServerParam('REQUEST_SCHEME');
|
||||
|
||||
if ($scheme === 'https') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$forwardedProto = $request->getServerParam('HTTP_X_FORWARDED_PROTO');
|
||||
|
||||
if ($forwardedProto === 'https') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function processFail(Result $result, AuthenticationData $data, Request $request): Result
|
||||
{
|
||||
$this->hookManager->processOnFail($result, $data, $request);
|
||||
@@ -669,4 +710,20 @@ class Authentication
|
||||
/** @var UserDataRepository */
|
||||
return $this->entityManager->getRepository(UserData::ENTITY_TYPE);
|
||||
}
|
||||
|
||||
private function getUsernameByAuthToken(AuthToken $authToken): ?string
|
||||
{
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->select(['userName'])
|
||||
->where(['id' => $authToken->getUserId()])
|
||||
->findOne();
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user->getUserName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,10 @@ namespace Espo\Core\Authentication;
|
||||
|
||||
class AuthenticationData
|
||||
{
|
||||
private $username;
|
||||
|
||||
private $password;
|
||||
|
||||
private $method;
|
||||
private ?string $username;
|
||||
private ?string $password;
|
||||
private ?string $method;
|
||||
private bool $byTokenOnly = false;
|
||||
|
||||
public function __construct(
|
||||
?string $username = null,
|
||||
@@ -52,21 +51,38 @@ class AuthenticationData
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* A username.
|
||||
*/
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* A password or auth-token.
|
||||
*/
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* A method.
|
||||
*/
|
||||
public function getMethod(): ?string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate by auth-token only. No username check.
|
||||
*/
|
||||
public function byTokenOnly(): bool
|
||||
{
|
||||
return $this->byTokenOnly;
|
||||
}
|
||||
|
||||
public function withUsername(?string $username): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
@@ -90,4 +106,12 @@ class AuthenticationData
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withByTokenOnly(bool $byTokenOnly): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->byTokenOnly = $byTokenOnly;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class UserFinder
|
||||
|
||||
public function find(string $username, string $hash): ?User
|
||||
{
|
||||
/** @var ?User */
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->where([
|
||||
@@ -59,7 +59,7 @@ class UserFinder
|
||||
|
||||
public function findApiHmac(string $apiKey): ?User
|
||||
{
|
||||
/** @var ?User */
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->where([
|
||||
@@ -74,7 +74,7 @@ class UserFinder
|
||||
|
||||
public function findApiApiKey(string $apiKey): ?User
|
||||
{
|
||||
/** @var ?User */
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->where([
|
||||
|
||||
@@ -33,6 +33,7 @@ class ClientFactory
|
||||
{
|
||||
/**
|
||||
* @param array<string,mixed> $options
|
||||
* @throws \Laminas\Ldap\Exception\LdapException
|
||||
*/
|
||||
public function create(array $options): Client
|
||||
{
|
||||
|
||||
@@ -52,14 +52,14 @@ class LoginFactory
|
||||
|
||||
public function create(string $method, bool $isPortal = false): Login
|
||||
{
|
||||
/** @var class-string<Login> */
|
||||
/** @var class-string<Login> $className */
|
||||
$className = $this->metadata->get(['authenticationMethods', $method, 'implementationClassName']);
|
||||
|
||||
if (!$className) {
|
||||
$sanitizedName = preg_replace('/[^a-zA-Z0-9]+/', '', $method);
|
||||
|
||||
if (!class_exists($className)) {
|
||||
/** @var class-string<Login> */
|
||||
/** @var class-string<Login> $className */
|
||||
$className = "Espo\\Core\\Authentication\\Logins\\" . $sanitizedName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +43,10 @@ use RuntimeException;
|
||||
|
||||
class Espo implements Login
|
||||
{
|
||||
private $userFinder;
|
||||
public const NAME = 'Espo';
|
||||
|
||||
private $passwordHash;
|
||||
private UserFinder $userFinder;
|
||||
private PasswordHash $passwordHash;
|
||||
|
||||
public function __construct(UserFinder $userFinder, PasswordHash $passwordHash)
|
||||
{
|
||||
@@ -81,7 +82,7 @@ class Espo implements Login
|
||||
return Result::fail(FailReason::WRONG_CREDENTIALS);
|
||||
}
|
||||
|
||||
if ($authToken && $user->id !== $authToken->getUserId()) {
|
||||
if ($authToken && $user->getId() !== $authToken->getUserId()) {
|
||||
return Result::fail(FailReason::USER_TOKEN_MISMATCH);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,11 @@ use Espo\Core\{
|
||||
Authentication\Login\Data,
|
||||
Authentication\Result,
|
||||
Authentication\Helper\UserFinder,
|
||||
Exceptions\Error,
|
||||
Authentication\Result\FailReason,
|
||||
};
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class Hmac implements Login
|
||||
{
|
||||
private $userFinder;
|
||||
@@ -71,7 +72,7 @@ class Hmac implements Login
|
||||
$secretKey = $this->apiKeyUtil->getSecretKeyForUserId($user->getId());
|
||||
|
||||
if (!$secretKey) {
|
||||
throw new Error("No secret key for API user '" . $user->getId() . "'.");
|
||||
throw new RuntimeException("No secret key for API user '" . $user->getId() . "'.");
|
||||
}
|
||||
|
||||
$string = $request->getMethod() . ' ' . $request->getResourcePath();
|
||||
|
||||
@@ -408,6 +408,8 @@ class LDAP implements Login
|
||||
|
||||
/**
|
||||
* Find LDAP user DN by his username.
|
||||
*
|
||||
* @throws \Laminas\Ldap\Exception\LdapException
|
||||
*/
|
||||
private function findLdapUserDnByUsername(string $username): ?string
|
||||
{
|
||||
@@ -426,7 +428,7 @@ class LDAP implements Login
|
||||
'(' . $options['userNameAttribute'] . '=' . $username . ')' .
|
||||
$loginFilterString . ')';
|
||||
|
||||
/** @var array<int,array{dn: string}> */
|
||||
/** @var array<int,array{dn: string}> $result */
|
||||
$result = $ldapClient->search($searchString, null, Client::SEARCH_SCOPE_SUB);
|
||||
|
||||
$this->log->debug('LDAP: user search string: "' . $searchString . '"');
|
||||
|
||||
@@ -37,7 +37,7 @@ class Data
|
||||
{
|
||||
private ?string $message = null;
|
||||
|
||||
private ?string$token = null;
|
||||
private ?string $token = null;
|
||||
|
||||
private ?string $view = null;
|
||||
|
||||
|
||||
@@ -50,4 +50,6 @@ class FailReason
|
||||
public const HASH_NOT_MATCHED = 'Hash not matched';
|
||||
|
||||
public const METHOD_NOT_ALLOWED = 'Not allowed authentication method';
|
||||
|
||||
public const DISCREPANT_DATA = 'Discrepant authentication data';
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ namespace Espo\Core\Authentication\TwoFactor;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class LoginFactory
|
||||
{
|
||||
@@ -47,11 +48,11 @@ class LoginFactory
|
||||
|
||||
public function create(string $method): Login
|
||||
{
|
||||
/** @var ?class-string<Login> */
|
||||
/** @var ?class-string<Login> $className */
|
||||
$className = $this->metadata->get(['app', 'authentication2FAMethods', $method, 'loginClassName']);
|
||||
|
||||
if (!$className) {
|
||||
throw new Error("No login-class class for '{$method}'.");
|
||||
throw new LogicException("No login-class class for '{$method}'.");
|
||||
}
|
||||
|
||||
return $this->injectableFactory->create($className);
|
||||
|
||||
@@ -48,7 +48,7 @@ class UserSetupFactory
|
||||
|
||||
public function create(string $method): UserSetup
|
||||
{
|
||||
/** @var ?class-string<UserSetup> */
|
||||
/** @var ?class-string<UserSetup> $className */
|
||||
$className = $this->metadata->get(['app', 'authentication2FAMethods', $method, 'userSetupClassName']);
|
||||
|
||||
if (!$className) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user