Compare commits

...

549 Commits
8.2.2 ... 8.3.2

Author SHA1 Message Date
Yuri Kuznetsov
5b0787474e 8.3.2 2024-06-26 07:53:49 +03:00
Yuri Kuznetsov
8c87f20374 comment 2024-06-26 07:50:19 +03:00
Yuri Kuznetsov
ba35115a48 inline edit: revert initial attributes on error 2024-06-26 07:44:44 +03:00
Yuri Kuznetsov
069010d0fe log opened only for system user 2024-06-26 07:22:09 +03:00
Yuri Kuznetsov
fe5878fd99 2fa auth log records on fail 2024-06-25 20:39:44 +03:00
Yuri Kuznetsov
839ceea142 root api endpoint fix type 2024-06-25 08:35:56 +03:00
Yuri Kuznetsov
9cce9d7347 comment 2024-06-24 11:16:45 +03:00
Yuri Kuznetsov
69d0dbbf1c email collapse fix 2024-06-24 11:09:29 +03:00
Yuri Kuznetsov
68ef9ce4ac disable email field layouts 2024-06-24 10:54:48 +03:00
Yuri Kuznetsov
163cf047e5 8.3.1 2024-06-24 10:25:00 +03:00
Yuri Kuznetsov
cc574afd3d labels 2024-06-23 17:52:57 +03:00
Yuri Kuznetsov
77d76fe0ee bg lang 2024-06-23 11:19:19 +03:00
Yuri Kuznetsov
e7ab75ec5a link-one fix 2024-06-23 10:47:07 +03:00
Yuri Kuznetsov
ff6983c9f3 fix wysiwyg sticky toolbar position 2024-06-22 09:26:40 +03:00
Yuri Kuznetsov
e2673473c5 wysiwyg codeview not limited height 2024-06-22 09:09:42 +03:00
Yuri Kuznetsov
e8bf70ab8e wysiwyg fullscreen codeview switch fix 2024-06-22 08:59:04 +03:00
Yuri Kuznetsov
f459d5811d wysiwyg height not being changed fix 2024-06-22 08:45:10 +03:00
Yuri Kuznetsov
53bf9b024a address field: support list-link template 2024-06-21 22:12:04 +03:00
Yuri Kuznetsov
25aed1a1c4 style fix 2024-06-21 16:17:45 +03:00
Yuri Kuznetsov
2589801993 css fix 2024-06-21 13:14:40 +03:00
Yuri Kuznetsov
5f954c22da fix header dropdown on small screen 2024-06-21 12:41:36 +03:00
Yuri Kuznetsov
423e2ca544 fix favicon metadata path 2024-06-21 12:04:11 +03:00
Yuri Kuznetsov
6176b8770f fulltext hyphen use natural language mode 2024-06-21 09:40:20 +03:00
Yuri Kuznetsov
8ab7452859 fix kanban fetch 2024-06-20 15:42:03 +03:00
Yuri Kuznetsov
07035bf8bf undo avatar 2024-06-20 08:19:34 +03:00
Yuri Kuznetsov
9e8df41174 check set held 2024-06-19 21:46:36 +03:00
Yuri Kuznetsov
64736349f0 ref 2024-06-19 21:44:15 +03:00
Yuri Kuznetsov
71f389703d remove action fix 2024-06-19 21:36:35 +03:00
Yuri Kuznetsov
02917943b6 quick view aux 2024-06-19 12:28:44 +03:00
Yuri Kuznetsov
acf1833d9f fix tooltip 2024-06-19 11:54:13 +03:00
Yuri Kuznetsov
9c45902213 stream notes quick view aux 2024-06-19 08:40:56 +03:00
Yuri Kuznetsov
f9674bd60c field view validation functions 2024-06-18 12:10:55 +03:00
Yuri Kuznetsov
831d840cc5 field view validation functions 2024-06-18 12:08:39 +03:00
Yuri Kuznetsov
3f6f718cd9 schema 2024-06-18 11:30:17 +03:00
Yuri Kuznetsov
c35b5c5aa5 label 2024-06-18 11:29:01 +03:00
Yuri Kuznetsov
0050f44a8a lang 2024-06-18 11:08:44 +03:00
dependabot[bot]
f86479afdc Bump ws from 8.13.0 to 8.17.1
Bumps [ws](https://github.com/websockets/ws) from 8.13.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.13.0...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 11:00:39 +03:00
Yuri Kuznetsov
f4cbb5e56a avatar color, use read only state 2024-06-18 09:03:46 +03:00
Yuri Kuznetsov
6c8e9b129a avatar color attribute in avatar field 2024-06-18 08:54:05 +03:00
Yuri Kuznetsov
c033cb171e avatar color combined field 2024-06-17 18:48:27 +03:00
Yuri Kuznetsov
bdf7d56e0c phone validation ref 2024-06-17 17:13:59 +03:00
Yuri Kuznetsov
3e7326f605 ref 2024-06-17 16:56:13 +03:00
Yuri Kuznetsov
61e51bfb31 ref 2024-06-17 16:54:21 +03:00
Yuri Kuznetsov
c35d934ba7 import array, trim 2024-06-17 13:23:04 +03:00
Yuri Kuznetsov
80fa391daa css fix 2024-06-17 12:51:28 +03:00
Yuri Kuznetsov
1092b17a13 orm mapper: check deleted when update 2024-06-17 12:50:10 +03:00
Yuri Kuznetsov
1de6568918 v 2024-06-17 09:09:27 +03:00
Yuri Kuznetsov
ee7c7046ac wysiwyg escape insert link 2024-06-16 10:38:15 +03:00
Yuri Kuznetsov
19638dd649 activities panel update-all 2024-06-15 16:14:16 +03:00
Yuri Kuznetsov
df30678484 css fix 2024-06-14 16:58:22 +03:00
dependabot[bot]
b05697d874 Bump braces from 3.0.2 to 3.0.3
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-12 23:10:31 +03:00
Yuri Kuznetsov
91177fc0d2 schema 2024-06-12 11:58:28 +03:00
Yuri Kuznetsov
e56c64dba4 schema 2024-06-12 09:15:35 +03:00
Yuri Kuznetsov
7ecc0dc6a9 upcoming activities order impr 2024-06-10 14:50:59 +03:00
Yuri Kuznetsov
808e2f8788 ref 2024-06-10 14:34:38 +03:00
Yuri Kuznetsov
7682539114 fix submit reminders 2024-06-10 12:15:29 +03:00
Yuri Kuznetsov
0558739e67 link multiple primary fix 2024-06-10 09:16:11 +03:00
Yuri Kuznetsov
552983ca33 ref 2024-06-09 18:33:14 +03:00
Yuri Kuznetsov
d328872479 ref 2024-06-09 18:28:57 +03:00
Yuri Kuznetsov
6cee542972 change todo 2024-06-08 21:16:11 +03:00
Yuri Kuznetsov
afb77424d4 array field: guard null item 2024-06-08 16:09:03 +03:00
Yuri Kuznetsov
c7465970bd viewModeIconClassMap 2024-06-08 12:21:00 +03:00
Yuri Kuznetsov
4f6ae321b9 schema 2024-06-08 12:13:37 +03:00
Yuri Kuznetsov
b92970992f ref 2024-06-08 11:50:46 +03:00
Yuri Kuznetsov
4923984e2d cleanup 2024-06-08 11:48:53 +03:00
Yuri Kuznetsov
071fb20f7b email insert data load additional fields 2024-06-08 09:51:16 +03:00
Yuri Kuznetsov
3e7d0f23c8 Merge branch 'fix' 2024-06-07 14:44:42 +03:00
Yuri Kuznetsov
268009e5f6 8.2.5 2024-06-07 14:35:06 +03:00
Yuri Kuznetsov
573d810d37 color changes 2024-06-06 20:58:51 +03:00
Yuri Kuznetsov
511b7b0d9d Merge branch 'fix' 2024-06-06 13:58:48 +03:00
Yuri Kuznetsov
3e54b58c8c formula parser assign operator fix 2024-06-06 13:57:46 +03:00
Yuri Kuznetsov
92467afad3 formula parser assign operator fix 2024-06-06 13:51:57 +03:00
Yuri Kuznetsov
a1c6004c4f user tabs fix 2024-06-06 11:29:17 +03:00
Yuri Kuznetsov
0d8943d86a cs 2024-06-06 11:17:00 +03:00
Yuri Kuznetsov
07ca2b6ceb user layout detail small change 2024-06-06 11:09:31 +03:00
Yuri Kuznetsov
e019de24cc fieldNotMatchingPattern$phoneNumberLoose 2024-06-05 12:05:57 +03:00
Yuri Kuznetsov
7b417313a2 reminder validator ref 2024-06-04 20:50:48 +03:00
Yuri Kuznetsov
cb6777d19b fix notification css 2024-06-04 20:38:31 +03:00
Yuri Kuznetsov
2790834c62 fix notification selector 2024-06-04 20:29:17 +03:00
Yuri Kuznetsov
9162b61a3f dynamic logic datetime second granuality 2024-06-04 12:06:53 +03:00
Yuri Kuznetsov
8488afb6f6 logo changes 2024-06-03 13:47:46 +03:00
Yuri Kuznetsov
1b518f4b6f style fix 2024-06-03 13:43:26 +03:00
Yuri Kuznetsov
92bad5240b calendar task fix 2024-06-03 13:40:29 +03:00
Yuri Kuznetsov
5f70d1b408 sent folder encoding 2024-06-03 11:27:42 +03:00
Yuri Kuznetsov
0941bd440e comment 2024-06-03 11:12:44 +03:00
Yuri Kuznetsov
2fabf4bfe1 EntityProvider getByClassName 2024-05-31 09:50:21 +03:00
Yurii Kuznietsov
f9e13e8093 Update README.md 2024-05-30 20:54:11 +03:00
Yurii Kuznietsov
d1f0fd4fb8 Update README.md 2024-05-30 20:53:41 +03:00
Yurii Kuznietsov
7c9ae7caa4 Update README.md 2024-05-30 20:01:09 +03:00
Yurii Kuznietsov
2adcab4417 Update README.md 2024-05-30 19:53:53 +03:00
Yurii Kuznietsov
2fd367cd7d Update README.md 2024-05-30 19:51:17 +03:00
Yurii Kuznietsov
82c74c23f9 Update README.md 2024-05-30 19:47:42 +03:00
Yurii Kuznietsov
9de57ba4b2 Update README.md 2024-05-30 19:41:22 +03:00
Yurii Kuznietsov
53d544a6d5 Update README.md 2024-05-30 19:40:26 +03:00
Yurii Kuznietsov
5f550f38f9 Update README.md 2024-05-30 19:39:56 +03:00
Yurii Kuznietsov
63a1c0be66 Update README.md 2024-05-30 19:36:49 +03:00
Yuri Kuznetsov
a43903dfd2 task reminders 2024-05-30 09:34:54 +03:00
Yuri Kuznetsov
ce369c774b ref 2024-05-30 09:34:54 +03:00
Yurii Kuznietsov
971543ab02 Update README.md 2024-05-29 21:51:00 +03:00
Yuri Kuznetsov
bf8b9a7e16 sakura color changes 2024-05-29 14:15:23 +03:00
Yuri Kuznetsov
d727cea9ee navbar color changes 2024-05-29 14:06:48 +03:00
Yuri Kuznetsov
373e05d219 navbar hover bg 2024-05-29 14:03:41 +03:00
Yuri Kuznetsov
8fa5cf4af1 color 2024-05-29 13:57:29 +03:00
Yuri Kuznetsov
776ae3ac9d radius change 2024-05-29 12:14:36 +03:00
Yuri Kuznetsov
f3d9f93564 espo theme change 2024-05-29 12:13:34 +03:00
Yuri Kuznetsov
d32995df9b hazyblue changes 2024-05-29 12:06:12 +03:00
Yuri Kuznetsov
6c8e5b2c92 calls scheduler panel 2024-05-29 11:54:39 +03:00
Yuri Kuznetsov
b44e536e35 logo espo color 2024-05-29 10:32:55 +03:00
Yuri Kuznetsov
8e9045352d color change 2024-05-27 17:06:48 +03:00
Yuri Kuznetsov
97499626db links no spell check 2024-05-27 15:38:31 +03:00
Yuri Kuznetsov
65cf99faad color change 2024-05-27 15:23:41 +03:00
Yuri Kuznetsov
5719f29262 table-panel border radius fix 2024-05-27 10:45:44 +03:00
Yuri Kuznetsov
06811d9fcc link manager: remove dynamic logic 2024-05-27 10:18:48 +03:00
Yuri Kuznetsov
9000dba6fd default team read only for non admins 2024-05-26 10:12:24 +03:00
Yuri Kuznetsov
b8734f68da ref 2024-05-24 17:47:29 +03:00
Yuri Kuznetsov
a6eb683770 logo-espo 2024-05-24 16:22:32 +03:00
Yuri Kuznetsov
00a8c7326d calendar no select 2024-05-24 15:30:52 +03:00
Yuri Kuznetsov
8faa5b91e2 color fixes 2024-05-24 14:21:39 +03:00
Yuri Kuznetsov
afeeba80f7 fix css 2024-05-24 14:16:44 +03:00
Yuri Kuznetsov
b6daa5accb espo theme colors change 2024-05-23 18:15:48 +03:00
Yuri Kuznetsov
7bf5c9ba11 color change 2024-05-23 13:00:18 +03:00
Yuri Kuznetsov
2433da0db7 theme changes 2024-05-22 19:50:40 +03:00
Yuri Kuznetsov
b820c880a7 theme changes 2024-05-22 16:38:28 +03:00
Yuri Kuznetsov
c7c31ee73a theme change 2024-05-22 16:13:18 +03:00
Yuri Kuznetsov
18ec05f63e color change 2024-05-22 16:02:14 +03:00
Yuri Kuznetsov
cf42c312cf fix style 2024-05-22 10:20:49 +03:00
Yuri Kuznetsov
d76bb5677b color fix 2024-05-22 10:12:02 +03:00
Yuri Kuznetsov
9afaf54dea theme changes 2024-05-22 09:43:41 +03:00
Yuri Kuznetsov
99490fdf4c theme changes 2024-05-21 18:59:14 +03:00
Yuri Kuznetsov
244edbbcac cleanup 2024-05-21 14:59:54 +03:00
Yuri Kuznetsov
2fab584c60 color changes 2024-05-21 14:51:39 +03:00
Yuri Kuznetsov
9d797ae17a fix color 2024-05-21 14:43:44 +03:00
Yuri Kuznetsov
15860096d3 rename subscription table 2024-05-21 14:28:09 +03:00
Yuri Kuznetsov
c0ac579b7f ref 2024-05-21 12:26:22 +03:00
Yuri Kuznetsov
50d5280d2c button style changes 2024-05-21 10:47:22 +03:00
Yuri Kuznetsov
b1c5526286 btn color change 2024-05-21 10:41:48 +03:00
Yuri Kuznetsov
384902f349 violet theme button style change 2024-05-21 10:08:10 +03:00
Yuri Kuznetsov
86e75a4d47 color fix 2024-05-21 09:56:58 +03:00
Yuri Kuznetsov
0401f0cce6 fix overlapping 2024-05-21 09:52:48 +03:00
Yuri Kuznetsov
5b54f1b579 color change 2024-05-20 19:33:02 +03:00
Yuri Kuznetsov
62370b6c8d Merge branch 'fix' 2024-05-20 11:41:43 +03:00
Yuri Kuznetsov
edc9d00be3 calendar all day fix 2024-05-20 11:41:31 +03:00
Yuri Kuznetsov
73d4250daa typo 2024-05-18 16:48:58 +03:00
Yuri Kuznetsov
aa052560bf cs 2024-05-18 16:47:18 +03:00
Yuri Kuznetsov
b35c2ca97f deprecation 2024-05-18 16:46:48 +03:00
Yuri Kuznetsov
d2946cacfa fix stream scroll 2024-05-18 13:55:49 +03:00
Yuri Kuznetsov
eaaceb74bd stored view impr 2024-05-18 11:47:18 +03:00
Yuri Kuznetsov
2138591b72 fix 2024-05-18 11:33:46 +03:00
Yuri Kuznetsov
3583b586c2 ref 2024-05-18 11:11:05 +03:00
Yuri Kuznetsov
965e5149f3 ref 2024-05-18 10:38:17 +03:00
Yuri Kuznetsov
be899bc0e2 reset scroll 2024-05-18 10:33:15 +03:00
Yuri Kuznetsov
e23e8854d7 cs 2024-05-18 09:59:14 +03:00
Yuri Kuznetsov
2b977e5456 record dashlets display records int 2024-05-18 09:38:38 +03:00
Yuri Kuznetsov
1c7f65db1a password settings layout change 2024-05-17 16:40:33 +03:00
Yuri Kuznetsov
cdda1e0514 typo 2024-05-17 15:00:25 +03:00
Yuri Kuznetsov
9fc955d7b5 portal auth token control 2024-05-17 14:49:39 +03:00
Yuri Kuznetsov
0372374e1f convert lead ui impr 2024-05-17 13:05:48 +03:00
Yuri Kuznetsov
c615f0d8db message 2024-05-17 12:52:57 +03:00
Yuri Kuznetsov
f4b9b3fd81 ref 2024-05-17 12:52:50 +03:00
Yuri Kuznetsov
da06eb9426 messages 2024-05-17 12:14:46 +03:00
Yuri Kuznetsov
2a3f3f2d04 update favicon 2024-05-17 11:28:48 +03:00
Yuri Kuznetsov
19a8535dbe favicon png 2024-05-17 11:23:28 +03:00
Yuri Kuznetsov
363a1c1b06 favicon 2024-05-17 09:48:29 +03:00
Yuri Kuznetsov
5c2b3f707b fix mail link surrounded by quotes 2024-05-17 09:27:04 +03:00
Yuri Kuznetsov
73f88bc097 fix messages 2024-05-16 16:00:20 +03:00
Yuri Kuznetsov
7ebcc6c71a kb active status list 2024-05-16 14:51:55 +03:00
Yuri Kuznetsov
af3baef0cb campaign disable custom options 2024-05-16 14:41:46 +03:00
Yuri Kuznetsov
14ea8886f6 document active statuses 2024-05-16 13:57:20 +03:00
Yuri Kuznetsov
852977d5db schema 2024-05-16 13:50:30 +03:00
Yuri Kuznetsov
4178406453 ref 2024-05-16 13:49:09 +03:00
Yuri Kuznetsov
149649be09 fix 2024-05-16 12:58:15 +03:00
Yuri Kuznetsov
2a17155f96 force assignment notifications 2024-05-16 12:48:46 +03:00
Yuri Kuznetsov
d9488ba71c ref 2024-05-16 12:42:43 +03:00
Yuri Kuznetsov
275c3aa492 remove addressCountryList param 2024-05-15 14:56:22 +03:00
Yuri Kuznetsov
9f5975eb5b autocomplete in modal fix 2024-05-15 14:52:44 +03:00
Yuri Kuznetsov
a1d5174372 address country list 2024-05-15 14:12:29 +03:00
Yuri Kuznetsov
547b77423a varchar custom autocomplete lookup function 2024-05-15 12:56:40 +03:00
Yuri Kuznetsov
a2c81e5891 orm reportsitory delete from db if no deleted 2024-05-15 10:49:36 +03:00
Yuri Kuznetsov
f1070612b4 ref cs 2024-05-15 10:43:17 +03:00
Yuri Kuznetsov
c5828a9feb schema 2024-05-15 10:30:10 +03:00
Yuri Kuznetsov
62b2314ff3 address fix params 2024-05-14 14:07:46 +03:00
Yuri Kuznetsov
dce9c437bf ref 2024-05-14 11:52:26 +03:00
Yuri Kuznetsov
e5a012cdc7 7_2 migration 2024-05-13 12:23:32 +03:00
Yuri Kuznetsov
17f18e36fd migration scripts 2024-05-13 10:56:52 +03:00
Yuri Kuznetsov
e99fca0252 group email account exclude from reply 2024-05-13 10:01:50 +03:00
Yuri Kuznetsov
316a865d02 imap notify wait period 2024-05-12 16:28:25 +03:00
Yuri Kuznetsov
0f6049b36f notify when imap not connected 2024-05-12 12:33:58 +03:00
Yuri Kuznetsov
63771d37b3 email mark as read from list view 2024-05-11 13:28:21 +03:00
Yuri Kuznetsov
9e50367303 ref 2024-05-11 13:20:57 +03:00
Yuri Kuznetsov
71b0dfec04 added validation faulure explanations 2024-05-11 10:19:49 +03:00
Yuri Kuznetsov
90c692648d validation failures changes 2024-05-11 10:19:30 +03:00
Yuri Kuznetsov
e968424b9d ref 2024-05-11 10:03:11 +03:00
Yuri Kuznetsov
ed77c8758c imap error exception 2024-05-10 14:39:27 +03:00
Yuri Kuznetsov
caa536f17a fix logging level 2024-05-10 14:05:32 +03:00
Yuri Kuznetsov
7463caf8ee ref 2024-05-10 13:48:22 +03:00
Yuri Kuznetsov
d4096b8dfb stream view list modal pagination 2024-05-10 13:24:09 +03:00
Yuri Kuznetsov
f73e4f2b47 Merge branch 'fix' 2024-05-10 12:52:40 +03:00
Yuri Kuznetsov
fc93a3d029 fix empty template 2024-05-10 12:52:35 +03:00
Yuri Kuznetsov
0bcbfdd223 fix empty template 2024-05-10 12:51:53 +03:00
Yuri Kuznetsov
6b1f8de16a fix typo 2024-05-10 10:56:14 +03:00
Yuri Kuznetsov
64741be18b fix typo 2024-05-10 10:55:26 +03:00
Yuri Kuznetsov
8ad8a42ecd docs 2024-05-10 10:53:10 +03:00
Yuri Kuznetsov
ead2d3829b docs 2024-05-10 10:51:12 +03:00
Yuri Kuznetsov
55be65d48c oidc authentication prompt param in UI 2024-05-10 10:17:25 +03:00
Yuri Kuznetsov
ecee6e7477 authentication layout change 2024-05-10 10:01:15 +03:00
Yuri Kuznetsov
c037870e7e tooltip 2024-05-10 09:53:09 +03:00
Yuri Kuznetsov
e86c4a9b6b ip address whitelist 2024-05-09 19:21:19 +03:00
Yuri Kuznetsov
586e28bd26 ref 2024-05-09 17:05:47 +03:00
Yuri Kuznetsov
3ca7cdffcc fix message 2024-05-09 16:39:38 +03:00
Yuri Kuznetsov
d36dc4bc1c fix docs 2024-05-09 16:38:32 +03:00
Yuri Kuznetsov
ede53970bf ref 2024-05-09 16:37:43 +03:00
Yuri Kuznetsov
0348778bc1 ref 2024-05-09 16:19:48 +03:00
Yuri Kuznetsov
3e2a3597ad schema 2024-05-09 16:02:28 +03:00
Yuri Kuznetsov
1c5afb52b3 schema 2024-05-09 15:32:35 +03:00
Yuri Kuznetsov
8601c8fc24 color changes 2024-05-09 13:00:48 +03:00
Yuri Kuznetsov
5b3b7b583d color fix 2024-05-09 12:10:28 +03:00
Yuri Kuznetsov
9f0827e478 ref 2024-05-09 11:56:59 +03:00
Yuri Kuznetsov
a0858acae3 color fix 2024-05-09 11:54:10 +03:00
Yuri Kuznetsov
49a49d6103 cs 2024-05-09 10:51:59 +03:00
Yuri Kuznetsov
7d9d248ab7 Merge branch 'fix' 2024-05-09 10:51:32 +03:00
Yuri Kuznetsov
acc0b71a7a fix url decode 2024-05-09 10:50:41 +03:00
Yuri Kuznetsov
1f3a0bb5dd allow tilde in URL 2024-05-09 10:35:24 +03:00
Yuri Kuznetsov
c5611a691f link multiple autocomplete fix 2024-05-08 21:19:21 +03:00
Yuri Kuznetsov
72927e34e2 autocomplete fix 2024-05-08 21:02:00 +03:00
Yuri Kuznetsov
6c2a114c47 ref 2024-05-08 12:53:15 +03:00
Yuri Kuznetsov
edb5f30ddd Merge branch 'fix' 2024-05-08 11:41:47 +03:00
Yuri Kuznetsov
94881de082 8.2.4 2024-05-08 11:32:01 +03:00
Yuri Kuznetsov
ce0baf450e light logo color change 2024-05-08 10:08:55 +03:00
Yuri Kuznetsov
3854141292 do not update pinned collection if the same 2024-05-08 09:33:06 +03:00
Yuri Kuznetsov
152bc76e57 sync pinned on websocket update 2024-05-08 09:28:56 +03:00
Yuri Kuznetsov
6d01717f7e style fix 2024-05-07 23:07:42 +03:00
Yuri Kuznetsov
b026ff8066 pinned icon 2024-05-07 23:04:48 +03:00
Yuri Kuznetsov
f9270a20d4 pin note check max count > 0 2024-05-07 22:46:15 +03:00
Yuri Kuznetsov
5196343479 pinned node sync more fields 2024-05-07 22:42:02 +03:00
Yuri Kuznetsov
3606051b91 entity manager columns swap 2024-05-07 19:41:23 +03:00
Yuri Kuznetsov
3fc4b487b3 pinned notes 2024-05-07 18:15:52 +03:00
Yuri Kuznetsov
692e0b43e3 ref 2024-05-07 11:38:18 +03:00
Yuri Kuznetsov
c273e26cf9 colorpicker style improvements 2024-05-06 17:01:32 +03:00
Yuri Kuznetsov
a26a48fac7 wysiwyg cell params 2024-05-06 15:40:37 +03:00
Yuri Kuznetsov
2a42ab8db5 wysiwyg table params 2024-05-06 14:34:54 +03:00
Yuri Kuznetsov
16d66aa642 ref 2024-05-05 10:08:54 +03:00
Yuri Kuznetsov
85d167aad4 Merge branch 'fix' 2024-05-04 23:03:14 +03:00
Yuri Kuznetsov
cbec1cbbe5 fix parser 2024-05-04 23:00:46 +03:00
Yuri Kuznetsov
1509f8d18f exception levels 2024-05-04 14:57:12 +03:00
Yuri Kuznetsov
e6d618f142 job fail critical log 2024-05-04 14:52:06 +03:00
Yuri Kuznetsov
455c51e806 ref 2024-05-04 14:47:39 +03:00
Yuri Kuznetsov
d9f553d47f formula error fix 2024-05-04 14:39:31 +03:00
Yuri Kuznetsov
5dc08ecf05 fix formula errors 2024-05-04 14:38:43 +03:00
Yuri Kuznetsov
2d791010dc log previous 2024-05-04 14:38:11 +03:00
Yuri Kuznetsov
cc8e2511e0 fix bubbling 2024-05-04 14:26:02 +03:00
Yuri Kuznetsov
92b0af358e fix log messages 2024-05-04 14:09:45 +03:00
Yuri Kuznetsov
9526614a3d fix message 2024-05-04 14:05:23 +03:00
Yuri Kuznetsov
9fca726329 fix legacy 2024-05-04 13:59:48 +03:00
Yuri Kuznetsov
83a9f53fe3 action history record bigint 2024-05-04 13:38:28 +03:00
Yuri Kuznetsov
da27b68312 messages 2024-05-04 13:37:44 +03:00
Yuri Kuznetsov
d2e90f7853 schema 2024-05-04 13:23:54 +03:00
Yuri Kuznetsov
0f7cd11ffe Merge branch 'f/log' 2024-05-04 13:09:11 +03:00
Yuri Kuznetsov
b006349528 app log dev 2024-05-04 13:05:08 +03:00
Yuri Kuznetsov
f03637ac64 type fix 2024-05-04 09:29:24 +03:00
Yuri Kuznetsov
e0909af3fe app log dev 2024-05-03 18:33:47 +03:00
Yuri Kuznetsov
1ed38e28cd side panel date fields if no by field 2024-05-03 17:35:50 +03:00
Yuri Kuznetsov
567db6b204 linkOnlyNotLinked param 2024-05-03 13:05:17 +03:00
Yuri Kuznetsov
04689b8ed0 user attribute safe 2024-05-03 10:27:22 +03:00
Yuri Kuznetsov
c007010a73 bar argument value exception message 2024-05-03 10:05:27 +03:00
Yuri Kuznetsov
498dd5478b ref 2024-05-03 09:52:59 +03:00
Yuri Kuznetsov
4e9e71b1bb userAttribute forbid list 2024-05-02 20:39:13 +03:00
Yuri Kuznetsov
06771f2f3e unsafe functions 2024-05-02 20:05:40 +03:00
Yuri Kuznetsov
bafe2dd471 formula base64 functions 2024-05-02 18:39:54 +03:00
Yuri Kuznetsov
4591d8f634 formula acl functions 2024-05-02 18:03:09 +03:00
Yuri Kuznetsov
f1e42f931b ref 2024-05-02 13:20:40 +03:00
Yuri Kuznetsov
8cf31f17cb email account active filter 2024-05-02 12:24:57 +03:00
Yuri Kuznetsov
40c8d0063d fill assigned user if read level is own 2024-05-02 10:59:58 +03:00
Yuri Kuznetsov
157f03b38f imap test connection error 2024-05-02 10:26:47 +03:00
Yuri Kuznetsov
ffa5cf44ea load parent fields fix 2024-05-02 10:10:12 +03:00
Yuri Kuznetsov
cc88ab9290 ref 2024-05-02 10:07:00 +03:00
dependabot[bot]
1a179c2373 Bump ejs from 3.1.8 to 3.1.10
Bumps [ejs](https://github.com/mde/ejs) from 3.1.8 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.8...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 09:08:46 +03:00
Yuri Kuznetsov
066f815d0c dashblet dropdown overflow fix 2024-05-01 13:37:04 +03:00
Yuri Kuznetsov
8078043f3c ref 2024-05-01 13:03:39 +03:00
Yuri Kuznetsov
06de056bb5 fix kanban dropdown overflow 2024-05-01 12:59:33 +03:00
Yuri Kuznetsov
630bdd7885 change start img 2024-04-30 22:15:08 +03:00
Yuri Kuznetsov
847440ad6c change install image 2024-04-30 15:49:51 +03:00
Yuri Kuznetsov
0271deddde fix test 2024-04-30 14:25:46 +03:00
Yuri Kuznetsov
65a385dfcf Merge branch 'fix' 2024-04-30 14:18:11 +03:00
Yuri Kuznetsov
71dd872618 calendar duplicate event fix 2024-04-30 14:15:39 +03:00
Yuri Kuznetsov
8025c1e101 mention frontend fix 2024-04-30 12:02:48 +03:00
Yuri Kuznetsov
fe15332531 ref 2024-04-29 19:25:16 +03:00
Yuri Kuznetsov
508699d0aa bool 2024-04-29 19:24:25 +03:00
Yuri Kuznetsov
0465f826f1 fix test 2024-04-29 15:26:21 +03:00
Yuri Kuznetsov
f78d38d592 migration script for reminders 2024-04-28 20:09:53 +03:00
Yuri Kuznetsov
db7142e4f9 reminded assigned users 2024-04-28 17:34:45 +03:00
Yuri Kuznetsov
e1b4fd6bdb reminderMaxCount 2024-04-28 14:43:49 +03:00
Yuri Kuznetsov
483ffb91fa ref 2024-04-28 14:33:23 +03:00
Yuri Kuznetsov
76a5721c72 skip past reminders 2024-04-28 14:27:24 +03:00
Yuri Kuznetsov
27c5bec755 reminders only own user 2024-04-28 14:15:39 +03:00
Yuri Kuznetsov
8e3016e301 rename 2024-04-28 12:37:50 +03:00
Yuri Kuznetsov
d93f08b18e reminder type fixes 2024-04-28 12:36:56 +03:00
Yuri Kuznetsov
701abe129b reminder links 2024-04-28 12:31:13 +03:00
Yuri Kuznetsov
e585ef0a0d ref 2024-04-28 10:10:53 +03:00
Yuri Kuznetsov
beaafccd2a log formatter interpolation. new line 2024-04-28 10:10:41 +03:00
Yuri Kuznetsov
cad2e0078a log refactoring 2024-04-27 20:21:42 +03:00
Yuri Kuznetsov
3dc35d0f16 formula log functions 2024-04-27 17:17:22 +03:00
Yuri Kuznetsov
2d73c62936 ref 2024-04-27 12:48:36 +03:00
Yuri Kuznetsov
dad5c3724e ref 2024-04-27 12:00:56 +03:00
Yuri Kuznetsov
d0f3373515 ref 2024-04-26 13:37:03 +03:00
Yuri Kuznetsov
bbe578ce0a ref 2024-04-26 11:54:11 +03:00
Yuri Kuznetsov
51a8a3a302 jsdocs deprecation 2024-04-26 11:25:19 +03:00
Yuri Kuznetsov
e3d3cae647 Merge branch 'fix' 2024-04-26 11:09:17 +03:00
Yuri Kuznetsov
bfed154feb language load off 2024-04-26 11:01:54 +03:00
Yuri Kuznetsov
bbe9281cb0 css fix, border radius 2024-04-25 21:32:30 +03:00
Yuri Kuznetsov
b2a6ababd2 note acl query with created at 2024-04-25 16:50:08 +03:00
Yuri Kuznetsov
343ad33a78 noteAclPeriod only admin 2024-04-25 16:35:41 +03:00
Yuri Kuznetsov
0a51e27d57 ref 2024-04-25 16:21:54 +03:00
Yuri Kuznetsov
dc1dc5de9b noteAclLimit adminOnly 2024-04-25 16:12:30 +03:00
Yuri Kuznetsov
868aa64e51 ref docs 2024-04-25 16:07:03 +03:00
Yuri Kuznetsov
0a9c0dcb20 fix doc 2024-04-25 16:04:33 +03:00
Yuri Kuznetsov
c1855e84a9 ref 2024-04-25 15:43:29 +03:00
Yuri Kuznetsov
4aa4b17c9f ref 2024-04-25 14:42:22 +03:00
Yuri Kuznetsov
689d59df2e ref 2024-04-25 14:26:02 +03:00
Yuri Kuznetsov
6fd55a6c2d quickSearchFullTextAppendWildcard 2024-04-25 13:40:25 +03:00
Yuri Kuznetsov
ae141c2415 no distinct 2024-04-25 13:25:13 +03:00
Yuri Kuznetsov
ab88abfb5e test filter applier ref and fix 2024-04-25 13:13:56 +03:00
Yuri Kuznetsov
90ab1de10f ref 2024-04-25 11:05:31 +03:00
Yuri Kuznetsov
e3bf337a07 calendar restrict drop 2024-04-25 10:47:26 +03:00
Yuri Kuznetsov
69329a87db rename 2024-04-24 13:45:47 +03:00
Yuri Kuznetsov
663eacb962 formula single quote fix 2024-04-24 13:45:24 +03:00
Yuri Kuznetsov
38ce78fced formula fix string double backslash and quote 2024-04-24 13:33:52 +03:00
Yuri Kuznetsov
ddf904fa0f formula backslash escaping 2024-04-24 13:22:22 +03:00
Yuri Kuznetsov
eba3138d16 formula string backslashes 2024-04-23 21:51:23 +03:00
Yuri Kuznetsov
05227281e7 style fix 2024-04-23 19:08:52 +03:00
Yuri Kuznetsov
16672722d4 has-attachment change 2024-04-23 19:07:00 +03:00
Yuri Kuznetsov
7fcc80025c ref 2024-04-23 18:53:02 +03:00
Yuri Kuznetsov
da264fcc71 navbar hide last divider 2024-04-23 18:49:42 +03:00
Yuri Kuznetsov
2b6bf419a1 fix add custom tabs 2024-04-23 18:10:50 +03:00
Yuri Kuznetsov
310f83e9c2 update navbar on preferences 2024-04-23 18:09:49 +03:00
Yuri Kuznetsov
45cdae005a add custom tabs 2024-04-23 17:43:09 +03:00
Yuri Kuznetsov
15b4ce9657 move migration 2024-04-23 17:09:26 +03:00
Yuri Kuznetsov
665dd2f242 mention permission 2024-04-23 16:58:46 +03:00
Yuri Kuznetsov
ecde690c3a post field ref 2024-04-23 16:56:57 +03:00
Yuri Kuznetsov
90728dfc18 autocomplete abort last fetch 2024-04-23 16:05:53 +03:00
Yuri Kuznetsov
d519318804 fix jsdocs 2024-04-23 15:45:15 +03:00
Yuri Kuznetsov
0d8a5ce5a1 skip full text search if request is too short and no operators 2024-04-23 15:07:39 +03:00
Yuri Kuznetsov
3dc29b1bae fix 2024-04-22 14:42:37 +03:00
Yuri Kuznetsov
0819b69d4a email inbox alias 2024-04-22 13:59:01 +03:00
Yuri Kuznetsov
8ac3d4e90b ref tests 2024-04-22 13:42:05 +03:00
Yuri Kuznetsov
a577950cc0 lead capture loadAdditionalFieldsAfterUpdate 2024-04-22 13:29:28 +03:00
Yuri Kuznetsov
3cf6edd051 Merge branch 'fix' 2024-04-22 12:38:47 +03:00
Yuri Kuznetsov
5a19b90b13 fix role empty stream level on ui 2024-04-22 12:38:33 +03:00
Yuri Kuznetsov
daa3980122 fix role empty stream level on ui 2024-04-22 12:38:12 +03:00
Yuri Kuznetsov
bab460c521 account name audited 2024-04-21 16:38:39 +03:00
Yuri Kuznetsov
64b3f55d7a email filter lower 2024-04-21 14:38:54 +03:00
Yuri Kuznetsov
ca053b57c5 orm: custom where value in complex expresssion, fix string expression unescaping 2024-04-21 14:37:40 +03:00
Yuri Kuznetsov
a07f6b032a sanitizer suppress list, allow empty attachment 2024-04-20 21:47:38 +03:00
Yuri Kuznetsov
ad0fbb577f ref 2024-04-20 21:34:49 +03:00
Yuri Kuznetsov
df55323a04 attachment multiple audited 2024-04-20 20:54:48 +03:00
Yuri Kuznetsov
a081237b2e link multiple maxCount 2024-04-20 20:50:03 +03:00
Yuri Kuznetsov
b604501eba fix 2024-04-20 20:48:15 +03:00
Yuri Kuznetsov
b4368862e0 fix readme 2024-04-20 18:06:28 +03:00
Yuri Kuznetsov
81043740dc fix readme 2024-04-20 18:02:24 +03:00
Yuri Kuznetsov
b6beb39192 update readme 2024-04-20 18:01:05 +03:00
Yurii Kuznietsov
687f66908d Update feature_request.md 2024-04-20 16:44:36 +03:00
Yuri Kuznetsov
a5ef17e06c login view setup handler 2024-04-20 15:42:45 +03:00
Yuri Kuznetsov
cb8a43da52 cron/cache admin notifications 2024-04-20 15:28:53 +03:00
Yuri Kuznetsov
a575a69704 move 2024-04-20 15:20:18 +03:00
Yuri Kuznetsov
7d6b1d7bcf glass theme style fix 2024-04-20 11:57:44 +03:00
Yuri Kuznetsov
31b875bbdd Merge branch 'fix' 2024-04-20 11:33:33 +03:00
Yuri Kuznetsov
33b0ca8824 fix add dashlet 2024-04-20 11:32:41 +03:00
Yuri Kuznetsov
e7d388f55f navbar event off 2024-04-20 11:29:04 +03:00
Yuri Kuznetsov
559ef609e0 navbar resize fix 2024-04-20 11:19:23 +03:00
Yuri Kuznetsov
c382cb0e7b npm updates 2024-04-20 10:54:39 +03:00
Yuri Kuznetsov
3912f937bd update summernote 2024-04-20 10:54:02 +03:00
Yuri Kuznetsov
7222cd6436 Merge branch 'fix' 2024-04-20 08:35:44 +03:00
Yuri Kuznetsov
540c58a564 fix add dashlet quick seach 2024-04-20 08:35:16 +03:00
Yuri Kuznetsov
6f7c1f72f6 fix add dashlet quick seach 2024-04-20 08:34:58 +03:00
Yuri Kuznetsov
5bf4ab368d ref 2024-04-19 14:55:49 +03:00
Yuri Kuznetsov
42645a9bc1 permission consts 2024-04-19 14:33:54 +03:00
Yuri Kuznetsov
5be0f5c71e cs 2024-04-19 14:18:51 +03:00
Yuri Kuznetsov
f029675856 acl permission consts 2024-04-19 14:18:27 +03:00
Yuri Kuznetsov
3a1f5bec0f default team ui impr 2024-04-19 12:31:48 +03:00
Yuri Kuznetsov
1356e7b9f8 jsdoc 2024-04-19 12:30:19 +03:00
Yuri Kuznetsov
ab10b6c036 validate user default team 2024-04-19 12:00:03 +03:00
Yuri Kuznetsov
ed0e5112a3 disable layout for avatar fields 2024-04-19 11:46:25 +03:00
Yuri Kuznetsov
46da1bb8a0 permission string fix 2024-04-19 11:42:37 +03:00
Yuri Kuznetsov
cd216007af assigned to self and default team if no assignment permission 2024-04-19 11:28:01 +03:00
Yuri Kuznetsov
bd96cf9e73 Merge branch 'fix' 2024-04-19 10:42:18 +03:00
Yuri Kuznetsov
67c0be5699 8.2.3 2024-04-19 10:21:05 +03:00
Yuri Kuznetsov
695dc2eb42 record service create: keep new 2024-04-19 09:30:49 +03:00
Yuri Kuznetsov
516bd037e7 jsdoc 2024-04-18 19:46:18 +03:00
Yuri Kuznetsov
f122d3d3bb jsdocs 2024-04-18 19:33:00 +03:00
Yuri Kuznetsov
e7b64bcc6f jsdocs 2024-04-18 19:24:07 +03:00
Yuri Kuznetsov
3df7349d2b cs ref 2024-04-18 19:14:29 +03:00
Yuri Kuznetsov
47b6471717 jsdoc 2024-04-18 19:12:01 +03:00
Yuri Kuznetsov
95280e8f7b jsdocs 2024-04-18 18:58:17 +03:00
Yuri Kuznetsov
296f73a407 jsdoc 2024-04-18 18:47:29 +03:00
Yuri Kuznetsov
918faa340b jsdoc, cs 2024-04-18 18:38:26 +03:00
Yuri Kuznetsov
4526224ae4 avatar color 2024-04-18 16:50:31 +03:00
Yuri Kuznetsov
e35b8e799f colorpicker field change 2024-04-18 16:49:49 +03:00
Yuri Kuznetsov
06fffdf1cf colorpicker change 2024-04-18 16:20:27 +03:00
Yuri Kuznetsov
ac133fb257 dynamic logic changes 2024-04-18 15:56:26 +03:00
Yuri Kuznetsov
2ab3274768 formula parser +/- after operator 2024-04-18 14:20:00 +03:00
Yuri Kuznetsov
b7f22352ed cs 2024-04-18 12:36:15 +03:00
Yuri Kuznetsov
7873340616 rename Ranges label 2024-04-18 12:00:06 +03:00
Yuri Kuznetsov
546a0440cd isFollowed bool 2024-04-18 11:01:49 +03:00
Yuri Kuznetsov
9783271a1d comment 2024-04-18 11:01:26 +03:00
Yuri Kuznetsov
35ad3177d3 cleanup 2024-04-18 10:47:02 +03:00
Yuri Kuznetsov
06b6be0c9e ref 2024-04-18 10:43:57 +03:00
Yuri Kuznetsov
a390d71ef0 complex expr field impr 2024-04-18 10:36:09 +03:00
Yuri Kuznetsov
39dafddb4b field-manager impr 2024-04-18 10:35:59 +03:00
Yuri Kuznetsov
2706f8681f ref, comment 2024-04-18 10:05:07 +03:00
Yuri Kuznetsov
44babbc240 cleanup stars 2024-04-17 16:56:43 +03:00
Yuri Kuznetsov
2fe1403cdb schema 2024-04-17 16:06:51 +03:00
Yuri Kuznetsov
665bfc3649 comment 2024-04-17 16:03:22 +03:00
Yuri Kuznetsov
dd3746d435 move to folder header text 2024-04-17 14:30:08 +03:00
Yuri Kuznetsov
b881c11672 star limit 2024-04-17 14:19:40 +03:00
Yuri Kuznetsov
2c0370f546 on empty autocomplete promise 2024-04-17 11:51:23 +03:00
Yuri Kuznetsov
6e1b3c64ec starred filter in entity manager list 2024-04-17 10:40:40 +03:00
Yuri Kuznetsov
17c3fe5699 enable stars, schema 2024-04-17 10:27:56 +03:00
Yuri Kuznetsov
8d20ba1ffd stars dev and change 2024-04-17 10:10:22 +03:00
Yuri Kuznetsov
0d124269f0 star button wider 2024-04-17 09:55:24 +03:00
Yuri Kuznetsov
1fcf326ecc btn style fix 2024-04-17 09:21:07 +03:00
Yuri Kuznetsov
76397085a3 star dev 2024-04-16 19:20:59 +03:00
Yuri Kuznetsov
1b4bdfaa12 note email sent when from user 2024-04-16 14:50:45 +03:00
Yuri Kuznetsov
040e6ddeb7 cs, cleanup 2024-04-16 14:30:43 +03:00
Yuri Kuznetsov
d148b53d52 schema 2024-04-16 14:11:29 +03:00
Yuri Kuznetsov
90f125567c autcomplete own teams 2024-04-16 14:10:04 +03:00
Yuri Kuznetsov
8cc72d1c7b empty query autocomplete fix, no value setting 2024-04-16 13:39:37 +03:00
Yuri Kuznetsov
747c1bbf12 getEmptyAutocompleteResult ref, fix 2024-04-16 12:51:12 +03:00
Yuri Kuznetsov
22b5f89991 docs fix 2024-04-16 12:45:33 +03:00
Yuri Kuznetsov
d19de50f7f Merge branch 'fix' 2024-04-16 12:38:20 +03:00
Yuri Kuznetsov
d1dd39bf5a email parent allow autocomplete on empty 2024-04-16 12:38:03 +03:00
Yuri Kuznetsov
d07f5b8e18 email parent allow autocomplete on empty 2024-04-16 12:37:33 +03:00
Yuri Kuznetsov
9fa9440d7b cleanup 2024-04-16 10:30:52 +03:00
Yuri Kuznetsov
042b3ae66e do not log exception file/line if created with static 2024-04-16 09:58:59 +03:00
Yuri Kuznetsov
c07004faf4 add method 2024-04-15 21:38:57 +03:00
Yuri Kuznetsov
b3905eedf4 notification list ux impr 2024-04-15 20:27:14 +03:00
Yuri Kuznetsov
9cffccbe73 ref 2024-04-15 18:03:05 +03:00
Yuri Kuznetsov
43322186b5 acl getPermissionLevel 2024-04-15 18:01:55 +03:00
Yuri Kuznetsov
a0ab01a77f ref 2024-04-15 17:57:24 +03:00
Yuri Kuznetsov
d7b9b65990 application id in body tag 2024-04-15 16:33:11 +03:00
Yuri Kuznetsov
e23a22d259 change app id 2024-04-15 16:28:56 +03:00
Yuri Kuznetsov
5d946a44f6 kanban isMuted 2024-04-15 13:23:41 +03:00
Yuri Kuznetsov
437f12245e date time field style fix 2024-04-15 12:49:59 +03:00
Yuri Kuznetsov
b1cefdfbbe datetime input group style fix 2024-04-14 16:08:31 +03:00
Yuri Kuznetsov
857ee7fb98 style fix 2024-04-13 09:56:03 +03:00
Yuri Kuznetsov
3b68d3dd13 css fix 2024-04-13 09:46:12 +03:00
Yuri Kuznetsov
4125b4f9a0 colors 2024-04-11 17:33:03 +03:00
Yuri Kuznetsov
913cad075a Merge branch 'fix' 2024-04-11 17:31:31 +03:00
Yuri Kuznetsov
8a542adb7e calendar update class names 2024-04-11 17:31:14 +03:00
Yuri Kuznetsov
481f352f5a calendar update class names 2024-04-11 17:30:37 +03:00
Yuri Kuznetsov
8ce2c7a40b light theme calendar colors 2024-04-11 17:19:20 +03:00
Yuri Kuznetsov
8dda7cf2ea link parent list change 2024-04-11 14:52:44 +03:00
Yuri Kuznetsov
01eacf875d list link mode color default 2024-04-11 14:28:38 +03:00
Yuri Kuznetsov
6da9ac6af1 enum label type 2024-04-11 13:17:07 +03:00
Yuri Kuznetsov
418ad2776d wysiwyg attributes order preserving 2024-04-11 12:10:46 +03:00
Yuri Kuznetsov
2a2c07af87 cleanup 2024-04-11 11:55:15 +03:00
Yuri Kuznetsov
ca898cf240 ref 2024-04-11 11:29:48 +03:00
Yuri Kuznetsov
05b6f83ba1 style fix 2024-04-11 11:00:39 +03:00
Yuri Kuznetsov
68ccaf8818 activities dashlet fix refresh 2024-04-11 10:53:59 +03:00
Yuri Kuznetsov
162e0ab546 ref 2024-04-11 10:32:59 +03:00
Yuri Kuznetsov
4c9df4da1b activities panel changes 2024-04-11 10:26:39 +03:00
Yuri Kuznetsov
d1c2e82abc wysiwyg fetch empty as null 2024-04-10 23:08:10 +03:00
Yuri Kuznetsov
c054cde199 currency fix 2024-04-10 23:03:16 +03:00
Yuri Kuznetsov
f113905edd cleanup 2024-04-10 22:50:45 +03:00
Yuri Kuznetsov
b4e2b91c31 ref 2024-04-10 22:48:11 +03:00
Yuri Kuznetsov
3568345343 no join loader load all links 2024-04-10 14:35:32 +03:00
Yuri Kuznetsov
4f1223e9a0 Merge branch 'fix' 2024-04-10 13:23:38 +03:00
Yuri Kuznetsov
c6a172a6d0 style fix 2024-04-10 13:23:27 +03:00
Yuri Kuznetsov
1da70019fb fix tests 2024-04-10 11:09:43 +03:00
Yuri Kuznetsov
a84962d96d formula func variable aware 2024-04-10 10:54:33 +03:00
Yuri Kuznetsov
98253fb0a8 customizable check 2024-04-10 10:09:01 +03:00
Yuri Kuznetsov
7887e4c7e3 style fix 2024-04-09 21:23:28 +03:00
Yuri Kuznetsov
abe383d532 call rows actions fix 2024-04-09 20:56:51 +03:00
Yuri Kuznetsov
a8078c3fc6 row actions dividers 2024-04-09 20:35:36 +03:00
Yuri Kuznetsov
9fd07d2fab notify usage 2024-04-09 19:42:36 +03:00
Yuri Kuznetsov
f42fbd0098 ref 2024-04-09 19:40:38 +03:00
Yuri Kuznetsov
d431699dbe css fix 2024-04-09 16:51:50 +03:00
Yuri Kuznetsov
9b883d3e59 convert currency item order 2024-04-09 16:45:39 +03:00
Yuri Kuznetsov
955e53304f modal dropdown dividers 2024-04-09 16:14:26 +03:00
Yuri Kuznetsov
ed7085e952 detail view dropdown dividers 2024-04-09 15:55:46 +03:00
Yuri Kuznetsov
e274907260 schema 2024-04-09 15:29:00 +03:00
Yuri Kuznetsov
96520162d3 link load names test 2024-04-09 13:34:41 +03:00
Yuri Kuznetsov
bb091a0301 foreign attributes unset and foregn names load 2024-04-09 13:07:45 +03:00
Yuri Kuznetsov
7253082497 console command allowed flags 2024-04-09 11:34:16 +03:00
Yuri Kuznetsov
0e446c6f08 console allowed options 2024-04-09 11:25:24 +03:00
Yuri Kuznetsov
7af208c2a0 after-upgrade pass isUpgrade 2024-04-09 10:49:00 +03:00
Yuri Kuznetsov
60e0c03a1e Merge branch 'fix' 2024-04-09 10:43:12 +03:00
Yuri Kuznetsov
89f75bfb37 oidc prevent errors when user is not allowed 2024-04-09 09:24:23 +03:00
Yuri Kuznetsov
b83a66cf49 bg lang 2024-04-08 12:51:16 +03:00
Yuri Kuznetsov
3cd6deb732 pagination previous step and modal detail pagination fix 2024-04-08 12:07:14 +03:00
Yuri Kuznetsov
59fff4cc80 fix detail pagination 2024-04-08 11:40:57 +03:00
Yuri Kuznetsov
35e7b5c5ee move 2024-04-07 18:50:17 +03:00
Yuri Kuznetsov
e7415a2317 ref 2024-04-07 18:44:44 +03:00
Yuri Kuznetsov
69c5af4d97 Merge branch 'fix' 2024-04-07 18:31:07 +03:00
Yuri Kuznetsov
dca1d34685 reminder usersColumns check 2024-04-07 18:30:55 +03:00
Yuri Kuznetsov
105922b026 email create filter from address 2024-04-07 17:18:48 +03:00
Yuri Kuznetsov
1dd6ea1bac jsdocs 2024-04-07 17:06:16 +03:00
Yuri Kuznetsov
54f1fb27a1 email filter change layout 2024-04-07 15:13:43 +03:00
Yuri Kuznetsov
27353b1fbf attendeeLinkMap 2024-04-07 13:26:37 +03:00
Yuri Kuznetsov
afc34a9730 add docs 2024-04-07 11:36:09 +03:00
Yuri Kuznetsov
5b4e3cbc66 cleanup 2024-04-07 11:27:21 +03:00
Yuri Kuznetsov
5b763b7519 rename 2024-04-07 11:27:01 +03:00
Yuri Kuznetsov
fb07cec466 fix 2024-04-07 11:26:20 +03:00
Yuri Kuznetsov
04ea13d8e2 list view docs 2024-04-07 11:26:15 +03:00
Yuri Kuznetsov
9208775f34 list view focus 2024-04-06 18:04:14 +03:00
Yuri Kuznetsov
308c480317 migrations beta versions 2024-04-06 16:16:36 +03:00
Yuri Kuznetsov
85af141f9f Merge branch 'f/migrations' 2024-04-06 12:46:04 +03:00
Yuri Kuznetsov
d7a88b382d migrations fix and dev 2024-04-06 12:45:55 +03:00
Yuri Kuznetsov
8133682288 ref 2024-04-06 09:58:48 +03:00
Yuri Kuznetsov
0b035b89c0 user fetch issue 2024-04-06 09:55:56 +03:00
Yuri Kuznetsov
d5930542d9 ref 2024-04-06 09:51:56 +03:00
Yuri Kuznetsov
831dfc5bfd fix sndAccessInfo fetch 2024-04-06 09:51:48 +03:00
Yuri Kuznetsov
078808604c ref 2024-04-06 09:38:39 +03:00
Yuri Kuznetsov
35f8c5652a avater flicker fix 2024-04-06 09:35:25 +03:00
Yuri Kuznetsov
14d8f88985 ref 2024-04-06 09:32:17 +03:00
Yuri Kuznetsov
2ae0a118b6 disable note in link manager 2024-04-05 23:11:44 +03:00
Yuri Kuznetsov
1a51759b0f fix user acl 2024-04-05 20:18:21 +03:00
Yuri Kuznetsov
acc2eb1a55 migrations dev 2024-04-05 15:48:20 +03:00
Yuri Kuznetsov
e513a72504 migratinos dev 2024-04-05 14:58:53 +03:00
Yuri Kuznetsov
a8fa8bfa00 ref 2024-04-05 14:14:31 +03:00
Yuri Kuznetsov
e8473e5ce0 migrations dev 2024-04-05 13:34:49 +03:00
Yuri Kuznetsov
5a6583b93b migrations dev 2024-04-05 13:19:43 +03:00
Yuri Kuznetsov
977b9adeff migrations dev 2024-04-05 12:17:00 +03:00
Yuri Kuznetsov
382ba22cd7 fix command test 2024-04-05 11:25:23 +03:00
Yuri Kuznetsov
03e16b81e5 upgrade ref and test fixes 2024-04-05 11:21:09 +03:00
Yuri Kuznetsov
455bb2a298 migrations ref 2024-04-05 11:01:02 +03:00
Yuri Kuznetsov
0b19bd4bb5 upgrade ref 2024-04-05 10:47:10 +03:00
Yuri Kuznetsov
48b0063c69 middle table prefix server side check 2024-04-05 09:51:22 +03:00
Yuri Kuznetsov
9395fa886e grid layout css fix 2024-04-05 09:39:22 +03:00
Yuri Kuznetsov
a6c95ff0d5 layouts title 2024-04-05 09:37:10 +03:00
Yuri Kuznetsov
0a115a3603 command noSystemUser param 2024-04-04 20:53:33 +03:00
Yuri Kuznetsov
fcb3158a06 readme 2024-04-04 20:27:47 +03:00
Yuri Kuznetsov
77f60734f6 upgrade ref 2024-04-04 16:19:56 +03:00
Eymen Elkum
1c10ddd7b8 add missing & to google static url 2024-04-04 13:53:06 +03:00
Yuri Kuznetsov
d8dd048a89 migrations dev 2024-04-04 13:26:32 +03:00
Yuri Kuznetsov
42812f8bb2 ref 2024-04-04 11:42:07 +03:00
Yuri Kuznetsov
b5a9690b63 Merge branch 'fix' 2024-04-04 10:44:59 +03:00
Yuri Kuznetsov
b741898ce9 Merge branch 'fix' 2024-04-03 14:44:58 +03:00
Yuri Kuznetsov
b89c28fe96 Merge branch 'fix' 2024-04-02 17:32:42 +03:00
Yuri Kuznetsov
1c68c805aa Merge branch 'fix' 2024-04-02 15:42:14 +03:00
Yuri Kuznetsov
cef7a919b8 suppress inspection 2024-04-02 14:39:22 +03:00
Yuri Kuznetsov
3117db022f type fix 2024-04-02 14:38:40 +03:00
Yuri Kuznetsov
7b642e40b0 phone ext langth frontend validation 2024-04-02 14:21:53 +03:00
Yuri Kuznetsov
89ff3dcf15 phone number extensions 2024-04-02 13:47:23 +03:00
Yuri Kuznetsov
79c8d25a80 comments 2024-04-01 19:44:47 +03:00
Yuri Kuznetsov
58d926be82 orm base entity writtenMap change 2024-04-01 19:40:04 +03:00
Yuri Kuznetsov
3044f83690 orderDisabled parameter 2024-04-01 17:05:57 +03:00
Yuri Kuznetsov
f2d5b2685e jsdoc 2024-04-01 16:10:27 +03:00
Yuri Kuznetsov
17abe18d01 validation before check create access 2024-04-01 15:32:07 +03:00
Yuri Kuznetsov
09880ff8f1 import error file and line 2024-04-01 15:23:14 +03:00
Yuri Kuznetsov
23f4686577 send invitation check status backend 2024-04-01 13:22:12 +03:00
794 changed files with 21324 additions and 6268 deletions

View File

@@ -1,6 +1,6 @@
---
name: Feature request
about: Feature requests are not desired at the moment. Need to polish the system. For high-level features, consider creating feature requests on the forum. For low-level (framework) here on GitHub.
about: For high-level features, consider creating feature requests on the forum. For low-level (framework) here on GitHub.
title: ''
labels: ''
assignees: ''

View File

@@ -2,37 +2,41 @@
[![PHPStan level 8](https://img.shields.io/badge/PHPStan-level%208-brightgreen)](#espocrm)
[EspoCRM is an Open Source CRM](https://www.espocrm.com) (Customer Relationship Management)
software that allows you to see, enter and evaluate all your company relationships regardless
of the type. People, companies or opportunities all in an easy and intuitive interface.
It's a web application with a frontend designed as a single page application and a REST API
backend written in PHP.
[Download](https://www.espocrm.com/download/) the latest release from our website. Release notes
and release packages are available at [Releases](https://github.com/espocrm/espocrm/releases) on GitHub.
[EspoCRM](https://www.espocrm.com) is a free, open-source CRM platform designed to help organizations build and maintain strong customer relationships.
It provides a wide range of tools to store, organize, and manage leads, contacts, sales opportunities, marketing campaigns,
support cases, and more all business information in a simple and intuitive interface.
![Screenshot](https://user-images.githubusercontent.com/1006792/226094559-995dfd2a-a18f-4619-a21b-79a4e671990a.png)
### Architecture
EspoCRM is a web application with a frontend designed as a single-page application and a REST API
backend written in PHP.
### Demo
You can try the CRM on the online [demo](https://www.espocrm.com/demo/).
You can try the CRM on an online [demo](https://www.espocrm.com/demo/).
### Requirements
* PHP 8.1 - 8.3;
* MySQL 5.7 (and later), or MariaDB 10.2 (and later);
* PostgreSQL 15 (and later) (yet experimental, officially supported soon).
* PostgreSQL 15 (and later) (beta, official support soon).
For more information about server configuration see [this article](https://docs.espocrm.com/administration/server-configuration/).
For more information about server configuration, see [this article](https://docs.espocrm.com/administration/server-configuration/).
### Documentation
### Why EspoCRM?
See the [documentation](https://docs.espocrm.com) for administrators, users and developers.
* **Open-source transparency**. EspoCRMs source code is open and accessible, so anyone can inspect it and see how data is being managed within the CRM.
* **Customization freedom**. You can develop features, create custom entities, fields, relationships, buttons to make the CRM fit your specific needs.
* **Clean user interface**. EspoCRM has a uncluttered, minimalist and very fast user inteface that is easy to navigate and has a short learning curve.
* **Straightforward REST API**. It can be easily integrated with other applications using a REST API.
### Bug reporting
### Who is EspoCRM for?
Create a [GitHub issue](https://github.com/espocrm/espocrm/issues/new/choose) or post on our [forum](https://forum.espocrm.com/forum/bug-reports).
* **Startups, small & medium-sized businesses**. Its an affordable solution that is flexible and fully customizable.
* **Developers & tech enthusiasts**. You can extend functionalities, build extensions, and create custom integrations.
* **Anyone seeking a free CRM**. If you're looking for a user-friendly and secure CRM platform, it can be a good option.
### Installing stable version
@@ -43,11 +47,29 @@ See installation instructions:
* [Installation with Docker](https://docs.espocrm.com/administration/docker/installation/)
* [Installation with Traefik](https://docs.espocrm.com/administration/docker/traefik/)
### Download
[Download](https://www.espocrm.com/download/) the latest release from our website. You can also download the latest and previous release packages from GitHub [releases](https://github.com/espocrm/espocrm/releases).
### Release notes
Release notes are available at GitHub [releases](https://github.com/espocrm/espocrm/releases).
### Documentation
See the [documentation](https://docs.espocrm.com) for administrators, users and developers.
### Bug reporting
Create a [GitHub issue](https://github.com/espocrm/espocrm/issues/new/choose) or post on our [forum](https://forum.espocrm.com/forum/bug-reports).
### Development
See the [developer documentation](https://docs.espocrm.com/development/).
We highly recommend using IDE for development. The backend codebase follows SOLID principles, utilizes interfaces, static typing and generics. We recommend to start learning EspoCRM from the Dependency Injection article in the documentation.
We highly recommend using an IDE for development. The backend codebase follows SOLID principles, utilizes interfaces, static typing and generics. We recommend to start learning EspoCRM from the Dependency Injection article in the documentation.
Metadata plays an integral role in the EspoCRM application. All possible parameters are described with a JSON Schema, meaning you will have autocompletion in the IDE. You can also find the full metadata reference in the documentation.
### Community & Support
@@ -55,11 +77,11 @@ If you have a question regarding some features, need help or customizations, wan
### License
EspoCRM is published under the GNU AGPLv3 [license](https://raw.githubusercontent.com/espocrm/espocrm/master/LICENSE.txt).
EspoCRM is an open-source project licensed under [GNU AGPLv3](https://raw.githubusercontent.com/espocrm/espocrm/master/LICENSE.txt).
### Contributing
Before we can merge your pull request, you need to accept our CLA [here](https://github.com/espocrm/cla). See [contributing guidelines](https://github.com/espocrm/espocrm/blob/master/.github/CONTRIBUTING.md).
Before we can merge your pull request, you need to accept our CLA [here](https://github.com/espocrm/cla). See the [contributing guidelines](https://github.com/espocrm/espocrm/blob/master/.github/CONTRIBUTING.md).
Branches:

View File

@@ -29,6 +29,7 @@
namespace Espo\Classes\Acl\Note;
use Espo\Core\Acl\Permission;
use Espo\Core\Acl\Table;
use Espo\Entities\Note;
use Espo\Entities\User;
@@ -143,7 +144,7 @@ class AccessChecker implements AccessEntityCREDChecker
}
if ($entity->getTargetType() === Note::TARGET_PORTALS) {
return $this->aclManager->getPermissionLevel($user, 'portal') === Table::LEVEL_YES;
return $this->aclManager->getPermissionLevel($user, Permission::PORTAL) === Table::LEVEL_YES;
}
return false;

View File

@@ -29,6 +29,7 @@
namespace Espo\Classes\Acl\Portal;
use Espo\Core\Acl\Permission;
use Espo\Entities\Portal;
use Espo\Entities\User;
use Espo\Core\Acl\AccessEntityCREDChecker;
@@ -45,18 +46,12 @@ class AccessChecker implements AccessEntityCREDChecker
{
use DefaultAccessCheckerDependency;
private DefaultAccessChecker $defaultAccessChecker;
private AclManager $aclManager;
public function __construct(DefaultAccessChecker $defaultAccessChecker, AclManager $aclManager)
{
$this->defaultAccessChecker = $defaultAccessChecker;
$this->aclManager = $aclManager;
}
public function __construct(private DefaultAccessChecker $defaultAccessChecker, private AclManager $aclManager)
{}
public function check(User $user, ScopeData $data): bool
{
$level = $this->aclManager->getPermissionLevel($user, 'portal');
$level = $this->aclManager->getPermissionLevel($user, Permission::PORTAL);
return $level === Table::LEVEL_YES;
}

View File

@@ -29,6 +29,7 @@
namespace Espo\Classes\Acl\User;
use Espo\Core\Acl\Permission;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\Core\Acl\AccessEntityCREDSChecker;
@@ -60,8 +61,6 @@ class AccessChecker implements AccessEntityCREDSChecker
return false;
}
/** @var User $entity */
if ($entity->isSuperAdmin() && !$user->isSuperAdmin()) {
return false;
}
@@ -71,10 +70,8 @@ class AccessChecker implements AccessEntityCREDSChecker
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
{
/** @var User $entity */
if ($entity->isPortal()) {
if ($this->aclManager->getPermissionLevel($user, 'portal') === Table::LEVEL_YES) {
if ($this->aclManager->getPermissionLevel($user, Permission::PORTAL) === Table::LEVEL_YES) {
return true;
}
@@ -90,8 +87,6 @@ class AccessChecker implements AccessEntityCREDSChecker
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
{
/** @var User $entity */
if ($entity->isSystem()) {
return false;
}
@@ -111,8 +106,6 @@ class AccessChecker implements AccessEntityCREDSChecker
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
{
/** @var User $entity */
if (!$user->isAdmin()) {
return false;
}
@@ -130,8 +123,7 @@ class AccessChecker implements AccessEntityCREDSChecker
public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool
{
/** @var User $entity */
return $this->aclManager->checkUserPermission($user, $entity, 'user');
/** @noinspection PhpRedundantOptionalArgumentInspection */
return $this->aclManager->checkUserPermission($user, $entity, Permission::USER);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\AppParams;
use Espo\Core\Utils\Address\CountryDataProvider;
use Espo\Tools\App\AppParam;
class AddressCountryData implements AppParam
{
public function __construct(
private CountryDataProvider $provider
) {}
/**
* @return array{list: string[], preferredList: string[]}
*/
public function get(): array
{
return $this->provider->get();
}
}

View File

@@ -0,0 +1,65 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\Cleanup;
use Espo\Core\Cleanup\Cleanup;
use Espo\Core\Field\DateTime;
use Espo\Core\Utils\Config;
use Espo\Entities\AppLogRecord;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\DeleteBuilder;
class AppLog implements Cleanup
{
private const PERIOD = '30 days';
public function __construct(
private EntityManager $entityManager,
private Config $config
) {}
public function process(): void
{
$query = DeleteBuilder::create()
->from(AppLogRecord::ENTITY_TYPE)
->where(['createdAt<' => $this->getBefore()->toString()])
->build();
$this->entityManager->getQueryExecutor()->execute($query);
}
private function getBefore(): DateTime
{
/** @var string $period */
$period = $this->config->get('cleanupAppLogPeriod') ?? self::PERIOD;
return DateTime::createNow()->modify('-' . $period);
}
}

View File

@@ -0,0 +1,137 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\Cleanup;
use Espo\Core\Cleanup\Cleanup;
use Espo\Core\Utils\Acl\UserAclManagerProvider;
use Espo\Entities\StarSubscription;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\DeleteBuilder;
use Espo\Tools\Stars\StarService;
/**
* @noinspection PhpUnused
*/
class Stars implements Cleanup
{
public function __construct(
private EntityManager $entityManager,
private UserAclManagerProvider $userAclManagerProvider,
private StarService $service
) {}
public function process(): void
{
foreach ($this->getEntityTypeList() as $entityType) {
$this->processEntityType($entityType);
}
}
/**
* @return string[]
*/
private function getEntityTypeList(): array
{
$groups = $this->entityManager->getRDBRepositoryByClass(StarSubscription::class)
->group('entityType')
->select('entityType')
->find();
$list = [];
foreach ($groups as $group) {
$list[] = $group->get('entityType');
}
return $list;
}
private function processEntityType(string $entityType): void
{
if (
!$this->service->isEnabled($entityType) ||
!$this->entityManager->hasRepository($entityType)
) {
$deleteQuery = DeleteBuilder::create()
->from(StarSubscription::ENTITY_TYPE)
->where(['entityType' => $entityType])
->build();
$this->entityManager->getQueryExecutor()->execute($deleteQuery);
return;
}
$stars = $this->entityManager
->getRDBRepositoryByClass(StarSubscription::class)
->where(['entityType' => $entityType])
->sth()
->find();
foreach ($stars as $star) {
$entityId = $star->get('entityId');
$userId = $star->get('userId');
if ($userId === null || $entityId === null) {
continue;
}
$entity = $this->entityManager->getEntityById($entityType, $entityId);
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
if (!$entity || !$user) {
$this->unstar($userId, $entityType, $entityId);
continue;
}
$aclManager = $this->userAclManagerProvider->get($user);
if (!$aclManager->checkEntityRead($user, $entity)) {
$this->unstar($userId, $entityType, $entityId);
}
}
}
private function unstar(string $userId, string $entityType, string $entityId): void
{
$deleteQuery = DeleteBuilder::create()
->from(StarSubscription::ENTITY_TYPE)
->where([
'userId' => $userId,
'entityType' => $entityType,
'entityId' => $entityId,
])
->build();
$this->entityManager->getQueryExecutor()->execute($deleteQuery);
}
}

View File

@@ -33,7 +33,7 @@ use Espo\Core\Cleanup\Cleanup;
use Espo\Core\Field\DateTime;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\Entities\Subscription;
use Espo\Entities\StreamSubscription;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\Part\Condition as Cond;
@@ -41,19 +41,11 @@ class Subscribers implements Cleanup
{
private const PERIOD = '2 months';
private Metadata $metadata;
private EntityManager $entityManager;
private Config $config;
public function __construct(
Metadata $metadata,
EntityManager $entityManager,
Config $config
) {
$this->metadata = $metadata;
$this->entityManager = $entityManager;
$this->config = $config;
}
private Metadata $metadata,
private EntityManager $entityManager,
private Config $config
) {}
public function process(): void
{
@@ -105,7 +97,7 @@ class Subscribers implements Cleanup
$query = $this->entityManager
->getQueryBuilder()
->delete()
->from(Subscription::ENTITY_TYPE, 'subscription')
->from(StreamSubscription::ENTITY_TYPE, 'subscription')
->join(
$entityType,
'entity',

View File

@@ -0,0 +1,56 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\FieldSanitizers;
use Espo\Core\FieldSanitize\Sanitizer;
use Espo\Core\FieldSanitize\Sanitizer\Data;
/**
* @noinspection PhpUnused
*/
class StringUpperCase implements Sanitizer
{
public function sanitize(Data $data, string $field): void
{
if (!$data->has($field)) {
return;
}
$value = $data->get($field);
if (!is_string($value)) {
return;
}
$value = mb_strtoupper($value);
$data->set($field, $value);
}
}

View File

@@ -36,20 +36,17 @@ use Espo\Core\ORM\Entity as CoreEntity;
use stdClass;
/**
* @noinspection PhpUnused
*/
class LinkMultipleType
{
private Metadata $metadata;
private Defs $defs;
private const COLUMN_TYPE_ENUM = 'enum';
private const COLUMN_TYPE_VARCHAR = 'varchar';
private const COLUMN_TYPE_BOOL = 'bool';
public function __construct(Metadata $metadata, Defs $defs)
{
$this->metadata = $metadata;
$this->defs = $defs;
}
public function __construct(private Metadata $metadata, private Defs $defs)
{}
public function checkRequired(Entity $entity, string $field): bool
{
@@ -63,6 +60,7 @@ class LinkMultipleType
return count($idList) > 0;
}
/** @noinspection PhpUnused */
public function checkPattern(Entity $entity, string $field): bool
{
/** @var ?mixed[] $idList */
@@ -93,6 +91,27 @@ class LinkMultipleType
return true;
}
/** @noinspection PhpUnused */
public function checkMaxCount(Entity $entity, string $field, ?int $maxCount): bool
{
if ($maxCount === null) {
return true;
}
$list = $entity->get($field . 'Ids');
if (!is_array($list)) {
return true;
}
if (count($list) > $maxCount) {
return false;
}
return true;
}
/** @noinspection PhpUnused */
public function checkColumnsValid(Entity $entity, string $field): bool
{
if (!$entity instanceof CoreEntity) {

View File

@@ -31,6 +31,7 @@ namespace Espo\Classes\FieldValidators;
use Brick\PhoneNumber\PhoneNumber;
use Brick\PhoneNumber\PhoneNumberParseException;
use Espo\Core\PhoneNumber\Util;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs;
@@ -203,6 +204,22 @@ class PhoneType
return true;
}
$ext = null;
if ($this->config->get('phoneNumberExtensions')) {
[$number, $ext] = Util::splitExtension($number);
}
if ($ext) {
if (!preg_match('/[0-9]+/', $ext)) {
return false;
}
if (strlen($ext) > 6) {
return false;
}
}
try {
$numberObj = PhoneNumber::parse($number);
}

View File

@@ -0,0 +1,90 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\FieldValidators\Settings\AuthIpAddressWhitelist;
use Espo\Core\FieldValidation\Validator;
use Espo\Core\FieldValidation\Validator\Data;
use Espo\Core\FieldValidation\Validator\Failure;
use Espo\ORM\Entity;
/**
* @implements Validator<Entity>
*/
class Valid implements Validator
{
public function validate(Entity $entity, string $field, Data $data): ?Failure
{
$list = $entity->get($field);
if (!is_array($list)) {
return null;
}
foreach ($list as $item) {
if (!is_string($item)) {
continue;
}
if (!$this->isValid($item)) {
return Failure::create();
}
}
return null;
}
private function isValid(string $item): bool
{
$address = $item;
if (count(explode('/', $item)) > 1) {
[$address, $mask] = explode('/', $item, 2);
if (!is_numeric($mask)) {
return false;
}
$mask = (int) $mask;
if ($mask < 0 || $mask > 128) {
return false;
}
}
if (
filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false &&
filter_var($address, FILTER_VALIDATE_IP) === false
) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\FieldValidators\User\DefaultTeam;
use Espo\Core\FieldValidation\Validator;
use Espo\Core\FieldValidation\Validator\Data;
use Espo\Core\FieldValidation\Validator\Failure;
use Espo\Entities\User;
use Espo\ORM\Entity;
/**
* @implements Validator<User>
*/
class IsUserTeam implements Validator
{
public function validate(Entity $entity, string $field, Data $data): ?Failure
{
if (!$entity->getDefaultTeam()) {
return null;
}
if (in_array($entity->getDefaultTeam()->getId(), $entity->getTeamIdList())) {
return null;
}
return Failure::create();
}
}

View File

@@ -30,6 +30,7 @@
namespace Espo\Classes\Jobs;
use Espo\Entities\AuthToken;
use Espo\Entities\Portal;
use Espo\Core\Job\JobDataLess;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Utils\Config;
@@ -50,27 +51,69 @@ class AuthTokenControl implements JobDataLess
public function run(): void
{
$authTokenLifetime = (int) ($this->config->get('authTokenLifetime', 0) * 60);
$authTokenMaxIdleTime = (int) ($this->config->get('authTokenMaxIdleTime', 0) * 60);
$lifetime = (int) $this->config->get('authTokenLifetime', 0) * 60;
$maxIdleTime = (int) $this->config->get('authTokenMaxIdleTime', 0) * 60;
if (!$authTokenLifetime && !$authTokenMaxIdleTime) {
$portalIds = [];
/** @var iterable<Portal> $portals */
$portals = $this->entityManager
->getRDBRepositoryByClass(Portal::class)
->find();
foreach ($portals as $portal) {
$portalIds[] = $portal->getId();
}
$this->process(null, $lifetime, $maxIdleTime, $portalIds);
foreach ($portals as $portal) {
$itemLifetime = $portal->get('authTokenLifetime') !== null ?
(int) $portal->get('authTokenLifetime') * 60 :
$lifetime;
$itemMaxIdleTime = $portal->get('authTokenMaxIdleTime') !== null ?
(int) $portal->get('authTokenMaxIdleTime') * 60 :
$maxIdleTime;
$this->process($portal->getId(), $itemLifetime, $itemMaxIdleTime);
}
}
/**
* @param string[] $ignorePortalIds
*/
private function process(?string $portalId, int $lifetime, int $maxIdleTime, array $ignorePortalIds = []): void
{
if (!$lifetime && !$maxIdleTime) {
return;
}
$whereClause = [
'isActive' => true,
];
$whereClause = ['isActive' => true];
if ($authTokenLifetime) {
if ($portalId) {
$whereClause['portalId'] = $portalId;
}
if (!$portalId && $ignorePortalIds !== []) {
$whereClause[] = [
'OR' => [
['portalId' => null],
['portalId!=' => $ignorePortalIds],
]
];
}
if ($lifetime) {
$dt = new DateTime();
$dt->modify("-$authTokenLifetime minutes");
$dt->modify("-$lifetime minutes");
$whereClause['createdAt<'] = $dt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
}
if ($authTokenMaxIdleTime) {
if ($maxIdleTime) {
$dt = new DateTime();
$dt->modify("-$authTokenMaxIdleTime minutes");
$dt->modify("-$maxIdleTime minutes");
$whereClause['lastAccess<'] = $dt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
}

View File

@@ -29,39 +29,31 @@
namespace Espo\Classes\Jobs;
use Espo\Core\Exceptions\Error;
use Espo\Core\Mail\Account\PersonalAccount\Service;
use Espo\Core\Job\Job;
use Espo\Core\Job\Job\Data;
use RuntimeException;
use Throwable;
class CheckEmailAccounts implements Job
{
private $service;
public function __construct(Service $service)
{
$this->service = $service;
}
public function __construct(private Service $service)
{}
public function run(Data $data): void
{
$targetId = $data->getTargetId();
if (!$targetId) {
throw new Error("No target.");
throw new RuntimeException("No target.");
}
try {
$this->service->fetch($targetId);
}
catch (Throwable $e) {
throw new Error(
'Job CheckEmailAccounts ' . $targetId . ': [' . $e->getCode() . '] ' . $e->getMessage() . ' ' .
$e->getFile() . ':' . $e->getLine()
);
throw new RuntimeException("CheckInboundEmails job failed, $targetId; {$e->getMessage()}", 0, $e);
}
}
}

View File

@@ -29,37 +29,31 @@
namespace Espo\Classes\Jobs;
use Espo\Core\Exceptions\Error;
use Espo\Core\Mail\Account\GroupAccount\Service;
use Espo\Core\Job\Job;
use Espo\Core\Job\Job\Data;
use RuntimeException;
use Throwable;
class CheckInboundEmails implements Job
{
private $service;
public function __construct(Service $service)
{
$this->service = $service;
}
public function __construct(private Service $service)
{}
public function run(Data $data): void
{
$targetId = $data->getTargetId();
if (!$targetId) {
throw new Error("No target.");
throw new RuntimeException("No target.");
}
try {
$this->service->fetch($targetId);
}
catch (Throwable $e) {
throw new Error(
'Job CheckInboundEmails ' . $targetId . ': [' . $e->getCode() . '] ' .$e->getMessage()
);
throw new RuntimeException("CheckInboundEmails job failed, $targetId; {$e->getMessage()}", 0, $e);
}
}
}

View File

@@ -65,12 +65,12 @@ class MassDelete implements MassAction
$entityType = $params->getEntityType();
if (!$this->acl->check($entityType, Acl\Table::ACTION_DELETE)) {
throw new Forbidden("No delete access for '{$entityType}'.");
throw new Forbidden("No delete access for '$entityType'.");
}
if (
!$params->hasIds() &&
$this->acl->getPermissionLevel('massUpdatePermission') !== Acl\Table::LEVEL_YES
$this->acl->getPermissionLevel(Acl\Permission::MASS_UPDATE) !== Acl\Table::LEVEL_YES
) {
throw new Forbidden("No mass-update permission.");
}

View File

@@ -51,7 +51,7 @@ use Espo\Tools\MassUpdate\Data as MassUpdateData;
class MassUpdate implements MassAction
{
private const PERMISSION = 'massUpdatePermission';
private const PERMISSION = Acl\Permission::MASS_UPDATE;
/** @var string[] */
private array $notAllowedAttributeList = [

View File

@@ -30,6 +30,7 @@
namespace Espo\Classes\RecordHooks\Note;
use Espo\Core\Acl;
use Espo\Core\Acl\Permission;
use Espo\Core\Acl\Table as AclTable;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
@@ -90,7 +91,7 @@ class AssignmentCheck implements SaveHook
}
}
$messagePermission = $this->acl->getPermissionLevel('message');
$messagePermission = $this->acl->getPermissionLevel(Permission::MESSAGE);
if ($messagePermission === AclTable::LEVEL_NO) {
if (
@@ -126,14 +127,14 @@ class AssignmentCheck implements SaveHook
throw new BadRequest("No portal IDs.");
}
if ($this->acl->getPermissionLevel('portal') !== AclTable::LEVEL_YES) {
if ($this->acl->getPermissionLevel(Permission::PORTAL) !== AclTable::LEVEL_YES) {
throw new Forbidden('Not permitted to post to portal users.');
}
}
if (
$targetType === Note::TARGET_USERS &&
$this->acl->getPermissionLevel('portal') !== AclTable::LEVEL_YES
$this->acl->getPermissionLevel(Permission::PORTAL) !== AclTable::LEVEL_YES
) {
if ($hasPortalTargetUser) {
throw new Forbidden('Not permitted to post to portal users.');

View File

@@ -70,6 +70,7 @@ class BeforeCreate implements SaveHook
$targetType = $entity->getTargetType();
$entity->clear('isPinned');
$entity->clear('isGlobal');
switch ($targetType) {
@@ -87,7 +88,7 @@ class BeforeCreate implements SaveHook
$entity->clear('usersIds');
$entity->clear('teamsIds');
$entity->clear('portalsIds');
$entity->set('usersIds', [$this->user->getId()]);
$entity->setUsersIds([$this->user->getId()]);
$entity->set('isForSelf', true);
break;

View File

@@ -32,7 +32,6 @@ namespace Espo\Classes\RecordHooks\Note;
use Espo\Core\Exceptions\ForbiddenSilent;
use Espo\Core\Record\Hook\SaveHook;
use Espo\Entities\Note;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\Tools\Stream\NoteUtil;
@@ -43,18 +42,27 @@ use Espo\Tools\Stream\NoteUtil;
class BeforeUpdate implements SaveHook
{
public function __construct(
private User $user,
private NoteUtil $noteUtil
private NoteUtil $noteUtil,
) {}
public function process(Entity $entity): void
{
if (!$this->isEditableType($entity)) {
throw new ForbiddenSilent("Note is not editable.");
}
if ($entity->isPost()) {
$this->noteUtil->handlePostText($entity);
}
if (!$entity->isPost() && !$this->user->isAdmin()) {
throw new ForbiddenSilent("Only 'Post' type allowed.");
if (!$entity->isPost()) {
$entity->clear('post');
$entity->clear('attachmentsIds');
}
}
private function isEditableType(Note $entity): bool
{
return $entity->getType() == Note::TYPE_POST;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\Select\AddressCountry;
use Espo\Core\Select\Order\Item;
use Espo\Core\Select\Order\Orderer;
use Espo\ORM\Query\Part\Order;
use Espo\ORM\Query\SelectBuilder;
class PreferredNameOrderer implements Orderer
{
public function apply(SelectBuilder $queryBuilder, Item $item): void
{
$queryBuilder
->order('isPreferred', $item->getOrder() === Order::ASC ? Order::DESC : Order::ASC)
->order('name', $item->getOrder());
}
}

View File

@@ -0,0 +1,49 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\Select\AppLogRecord\PrimaryFilters;
use Espo\Core\Select\Primary\Filter;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
use Psr\Log\LogLevel;
class Errors implements Filter
{
public function apply(QueryBuilder $queryBuilder): void
{
$queryBuilder->where([
'level' => [
ucfirst(LogLevel::ERROR),
ucfirst(LogLevel::EMERGENCY),
ucfirst(LogLevel::CRITICAL),
ucfirst(LogLevel::ALERT),
]
]);
}
}

View File

@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\AccessControlFilters;
use Espo\Core\Select\AccessControl\Filter;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
@@ -46,6 +47,6 @@ class OnlyOwn implements Filter
{
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
$queryBuilder->where(['emailUser.userId' => $this->user->getId()]);
$queryBuilder->where([Email::ALIAS_INBOX . '.userId' => $this->user->getId()]);
}
}

View File

@@ -52,15 +52,15 @@ class OnlyTeam implements Filter
'entityTeam.entityType' => Email::ENTITY_TYPE,
'entityTeam.deleted' => false,
])
->leftJoin(Email::RELATIONSHIP_EMAIL_USER, 'emailUser', [
'emailUser.emailId:' => 'id',
'emailUser.deleted' => false,
'emailUser.userId' => $this->user->getId(),
->leftJoin(Email::RELATIONSHIP_EMAIL_USER, Email::ALIAS_INBOX, [
Email::ALIAS_INBOX . '.emailId:' => 'id',
Email::ALIAS_INBOX . '.deleted' => false,
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
])
->where([
'OR' => [
'entityTeam.teamId' => $this->user->getTeamIdList(),
'emailUser.userId' => $this->user->getId(),
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
]
])
->build();

View File

@@ -31,6 +31,7 @@ namespace Espo\Classes\Select\Email\AccessControlFilters;
use Espo\Core\Select\AccessControl\Filter;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
@@ -46,10 +47,9 @@ class PortalOnlyAccount implements Filter
$queryBuilder->distinct();
$orGroup = [
'emailUser.userId' => $this->user->getId(),
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
];
/** @var string[] $accountIdList */
$accountIdList = $this->user->getLinkMultipleIdList('accounts');
if (count($accountIdList)) {

View File

@@ -31,6 +31,7 @@ namespace Espo\Classes\Select\Email\AccessControlFilters;
use Espo\Core\Select\AccessControl\Filter;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
@@ -46,7 +47,7 @@ class PortalOnlyContact implements Filter
$queryBuilder->distinct();
$orGroup = [
'emailUser.userId' => $this->user->getId(),
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
];
$contactId = $this->user->get('contactId');

View File

@@ -97,7 +97,7 @@ class Main implements AdditionalApplier
];
foreach ($itemList as $item) {
$queryBuilder->select('emailUser.' . $item, $item);
$queryBuilder->select(Email::ALIAS_INBOX . '.' . $item, $item);
}
}

View File

@@ -31,6 +31,7 @@ namespace Espo\Classes\Select\Email\BoolFilters;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Core\Select\Bool\Filter;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\Part\Where\OrGroupBuilder;
use Espo\ORM\Query\Part\WhereClause;
@@ -46,7 +47,7 @@ class OnlyMy implements Filter
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
$item = WhereClause::fromRaw([
'emailUser.userId' => $this->user->getId(),
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
]);
$orGroupBuilder->add($item);

View File

@@ -36,14 +36,14 @@ class JoinHelper
{
public function joinEmailUser(QueryBuilder $queryBuilder, string $userId): void
{
if ($queryBuilder->hasLeftJoinAlias('emailUser')) {
if ($queryBuilder->hasLeftJoinAlias(Email::ALIAS_INBOX)) {
return;
}
$queryBuilder->leftJoin(Email::RELATIONSHIP_EMAIL_USER, 'emailUser', [
'emailUser.emailId:' => 'id',
'emailUser.deleted' => false,
'emailUser.userId' => $userId,
$queryBuilder->leftJoin(Email::RELATIONSHIP_EMAIL_USER, Email::ALIAS_INBOX, [
Email::ALIAS_INBOX . '.emailId:' => 'id',
Email::ALIAS_INBOX . '.deleted' => false,
Email::ALIAS_INBOX . '.userId' => $userId,
]);
}
}

View File

@@ -69,9 +69,9 @@ class InFolder implements ItemConverter
$this->joinEmailUser($queryBuilder);
$whereClause = [
'emailUser.inTrash' => false,
'emailUser.folderId' => null,
'emailUser.userId' => $this->user->getId(),
Email::ALIAS_INBOX . '.inTrash' => false,
Email::ALIAS_INBOX . '.folderId' => null,
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
[
'status' => [
Email::STATUS_ARCHIVED,
@@ -118,7 +118,7 @@ class InFolder implements ItemConverter
[
'status!=' => Email::STATUS_DRAFT,
],
'emailUser.inTrash' => false,
Email::ALIAS_INBOX . '.inTrash' => false,
]);
}
@@ -127,8 +127,8 @@ class InFolder implements ItemConverter
$this->joinEmailUser($queryBuilder);
return WhereClause::fromRaw([
'emailUser.userId' => $this->user->getId(),
'emailUser.isImportant' => true,
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
Email::ALIAS_INBOX . '.isImportant' => true,
]);
}
@@ -137,8 +137,8 @@ class InFolder implements ItemConverter
$this->joinEmailUser($queryBuilder);
return WhereClause::fromRaw([
'emailUser.userId' => $this->user->getId(),
'emailUser.inTrash' => true,
Email::ALIAS_INBOX . '.userId' => $this->user->getId(),
Email::ALIAS_INBOX . '.inTrash' => true,
]);
}
@@ -164,15 +164,15 @@ class InFolder implements ItemConverter
return WhereClause::fromRaw([
'groupFolderId' => $groupFolderId,
'OR' => [
'emailUser.id' => null,
'emailUser.inTrash' => false,
Email::ALIAS_INBOX . '.id' => null,
Email::ALIAS_INBOX . '.inTrash' => false,
]
]);
}
return WhereClause::fromRaw([
'emailUser.inTrash' => false,
'emailUser.folderId' => $folderId,
Email::ALIAS_INBOX . '.inTrash' => false,
Email::ALIAS_INBOX . '.folderId' => $folderId,
'groupFolderId' => null,
]);
}

View File

@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
use Espo\Core\Select\Where\Item;
use Espo\Core\Select\Where\ItemConverter;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\Part\WhereClause;
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
@@ -47,7 +48,7 @@ class InTrashIsFalse implements ItemConverter
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
return WhereClause::fromRaw([
'emailUser.inTrash' => false,
Email::ALIAS_INBOX . '.inTrash' => false,
]);
}
}

View File

@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
use Espo\Core\Select\Where\Item;
use Espo\Core\Select\Where\ItemConverter;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\Part\WhereClause;
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
@@ -47,7 +48,7 @@ class InTrashIsTrue implements ItemConverter
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
return WhereClause::fromRaw([
'emailUser.inTrash' => true,
Email::ALIAS_INBOX . '.inTrash' => true,
]);
}
}

View File

@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
use Espo\Core\Select\Where\Item;
use Espo\Core\Select\Where\ItemConverter;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\Part\WhereClause;
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
@@ -47,7 +48,7 @@ class IsImportantIsFalse implements ItemConverter
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
return WhereClause::fromRaw([
'emailUser.isImportant' => false,
Email::ALIAS_INBOX . '.isImportant' => false,
]);
}
}

View File

@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
use Espo\Core\Select\Where\Item;
use Espo\Core\Select\Where\ItemConverter;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\Part\WhereClause;
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
@@ -47,7 +48,7 @@ class IsImportantIsTrue implements ItemConverter
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
return WhereClause::fromRaw([
'emailUser.isImportant' => true,
Email::ALIAS_INBOX . '.isImportant' => true,
]);
}
}

View File

@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
use Espo\Core\Select\Where\Item;
use Espo\Core\Select\Where\ItemConverter;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\Part\WhereClause;
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
@@ -47,7 +48,7 @@ class IsNotReadIsFalse implements ItemConverter
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
return WhereClause::fromRaw([
'emailUser.isRead' => true,
Email::ALIAS_INBOX . '.isRead' => true,
]);
}
}

View File

@@ -32,6 +32,7 @@ namespace Espo\Classes\Select\Email\Where\ItemConverters;
use Espo\Core\Select\Where\Item;
use Espo\Core\Select\Where\ItemConverter;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\Part\WhereClause;
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
@@ -47,7 +48,7 @@ class IsNotReadIsTrue implements ItemConverter
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
return WhereClause::fromRaw([
'emailUser.isRead' => false,
Email::ALIAS_INBOX . '.isRead' => false,
'OR' => [
'sentById' => null,
'sentById!=' => $this->user->getId()

View File

@@ -0,0 +1,42 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\Select\EmailAccount\PrimaryFilters;
use Espo\Core\Select\Primary\Filter;
use Espo\Entities\EmailFilter;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
class Active implements Filter
{
public function apply(QueryBuilder $queryBuilder): void
{
$queryBuilder->where(['status' => EmailFilter::STATUS_ACTIVE]);
}
}

View File

@@ -29,6 +29,7 @@
namespace Espo\Classes\Select\User\AccessControlFilters;
use Espo\Core\Acl\Permission;
use Espo\ORM\Query\SelectBuilder;
use Espo\Core\Acl\Table;
use Espo\Core\AclManager;
@@ -51,7 +52,7 @@ class Mandatory implements Filter
]);
}
if ($this->aclManager->getPermissionLevel($this->user, 'portalPermission') !== Table::LEVEL_YES) {
if ($this->aclManager->getPermissionLevel($this->user, Permission::PORTAL) !== Table::LEVEL_YES) {
$queryBuilder->where([
'OR' => [
'type!=' => User::TYPE_PORTAL,

View File

@@ -29,6 +29,7 @@
namespace Espo\Classes\Select\User\AccessControlFilters;
use Espo\Core\Acl\Permission;
use Espo\ORM\Query\SelectBuilder;
use Espo\Core\Acl\Table;
use Espo\Core\AclManager;
@@ -43,7 +44,7 @@ class OnlyOwn implements Filter
public function apply(SelectBuilder $queryBuilder): void
{
if ($this->aclManager->getPermissionLevel($this->user, 'portalPermission') === Table::LEVEL_YES) {
if ($this->aclManager->getPermissionLevel($this->user, Permission::PORTAL) === Table::LEVEL_YES) {
$queryBuilder->where([
'OR' => [
'id' => $this->user->getId(),

View File

@@ -51,7 +51,7 @@ class OnlyTeam implements Filter
'id' => $this->user->getId(),
];
if ($this->aclManager->getPermissionLevel($this->user, 'portalPermission') === Table::LEVEL_YES) {
if ($this->aclManager->getPermissionLevel($this->user, 'portal') === Table::LEVEL_YES) {
$orGroup['type'] = User::TYPE_PORTAL;
}

View File

@@ -165,7 +165,7 @@ class GoogleMaps implements Helper
$url = "https://maps.googleapis.com/maps/api/staticmap?" .
'center=' . $addressEncoded .
'format=' . $format .
'&format=' . $format .
'&size=' . $size .
'&key=' . $apiKey;

View File

@@ -0,0 +1,50 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
use Espo\Core\Controllers\RecordBase;
use Espo\Tools\Address\CountryDefaultsPopulator;
class AddressCountry extends RecordBase
{
protected function checkAccess(): bool
{
return $this->user->isAdmin();
}
public function postActionPopulateDefaults(): bool
{
$populate = $this->injectableFactory->create(CountryDefaultsPopulator::class);
$populate->populate();
return true;
}
}

View File

@@ -36,7 +36,7 @@ use Espo\Core\Container;
use Espo\Core\DataManager;
use Espo\Core\Api\Request;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\AdminNotificationManager;
use Espo\Tools\AdminNotifications\Manager;
use Espo\Core\Utils\SystemRequirements;
use Espo\Core\Utils\ScheduledJob;
use Espo\Core\Upgrades\UpgradeManager;
@@ -51,7 +51,7 @@ class Admin
private Container $container,
private Config $config,
private User $user,
private AdminNotificationManager $adminNotificationManager,
private Manager $adminNotificationManager,
private SystemRequirements $systemRequirements,
private ScheduledJob $scheduledJob,
private DataManager $dataManager

View File

@@ -29,10 +29,14 @@
namespace Espo\Controllers;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Utils\Json;
class ApiIndex
{
public function getActionIndex(): string
public function getActionIndex(Request $request, Response $response): void
{
return "EspoCRM REST API";
$response->writeBody(Json::encode("EspoCRM REST API"));
}
}

View File

@@ -0,0 +1,76 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Controllers\Record;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use stdClass;
class AppLogRecord extends Record
{
protected function checkAccess(): bool
{
if (!$this->user->isAdmin()) {
return false;
}
if (!$this->config->get('restrictedMode')) {
return true;
}
if ($this->config->get('appLogAdminAllowed')) {
return true;
}
return $this->user->isSuperAdmin();
}
public function postActionCreate(Request $request, Response $response): stdClass
{
throw new Forbidden();
}
public function putActionUpdate(Request $request, Response $response): stdClass
{
throw new Forbidden();
}
public function postActionCreateLink(Request $request): bool
{
throw new Forbidden();
}
public function deleteActionRemoveLink(Request $request): bool
{
throw new Forbidden();
}
}

View File

@@ -36,17 +36,26 @@ use Espo\Core\Acl;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Exceptions\NotFound;
use Espo\Tools\DataPrivacy\Erasor;
class DataPrivacy
{
/**
* @throws Forbidden
*/
public function __construct(private Erasor $erasor, private Acl $acl)
{
if ($this->acl->getPermissionLevel('dataPrivacyPermission') === Acl\Table::LEVEL_NO) {
if ($this->acl->getPermissionLevel(Acl\Permission::DATA_PRIVACY) === Acl\Table::LEVEL_NO) {
throw new Forbidden();
}
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
*/
public function postActionErase(Request $request, Response $response): void
{
$data = $request->getParsedBody();

View File

@@ -35,6 +35,7 @@ use Espo\Core\Mail\Account\Storage\Params as StorageParams;
use Espo\Core\Controllers\Record;
use Espo\Core\Api\Request;
use Espo\Core\Mail\Exceptions\ImapError;
class InboundEmail extends Record
{
@@ -45,7 +46,8 @@ class InboundEmail extends Record
/**
* @return string[]
* @throws \Espo\Core\Exceptions\Error
* @throws Error
* @throws ImapError
*/
public function postActionGetFolders(Request $request): array
{

View File

@@ -29,6 +29,7 @@
namespace Espo\Controllers;
use Espo\Core\Acl\Permission;
use Espo\Core\Acl\Table;
use Espo\Core\Controllers\Record;
@@ -36,7 +37,7 @@ class Portal extends Record
{
protected function checkAccess(): bool
{
$level = $this->acl->getPermissionLevel('portal');
$level = $this->acl->getPermissionLevel(Permission::PORTAL);
return $level === Table::LEVEL_YES;
}

View File

@@ -68,19 +68,28 @@ class Stream
throw new BadRequest();
}
if ($id === null && $scope !== UserEntity::ENTITY_TYPE) {
throw new BadRequest("No ID.");
}
$searchParams = $this->fetchSearchParams($request);
$result = $scope === UserEntity::ENTITY_TYPE ?
$this->userRecordService->find($id, $searchParams) :
$this->service->find($scope, $id ?? '', $searchParams);
if ($scope === UserEntity::ENTITY_TYPE) {
$collection = $this->userRecordService->find($id, $searchParams);
return (object) [
'total' => $collection->getTotal(),
'list' => $collection->getValueMapList(),
];
}
if ($id === null) {
throw new BadRequest();
}
$collection = $this->service->find($scope, $id, $searchParams);
$pinnedCollection = $this->service->getPinned($scope, $id);
return (object) [
'total' => $result->getTotal(),
'list' => $result->getValueMapList(),
'total' => $collection->getTotal(),
'list' => $collection->getValueMapList(),
'pinnedList' => $pinnedCollection->getValueMapList(),
];
}

View File

@@ -31,6 +31,7 @@ namespace Espo\Core;
use Espo\Core\Acl\Exceptions\NotImplemented;
use Espo\Core\Acl\GlobalRestriction;
use Espo\Core\Acl\Permission;
use Espo\Core\Acl\Table;
use Espo\ORM\Entity;
@@ -283,7 +284,7 @@ class Acl
*
* @param User|string $target User entity or user ID.
*/
public function checkUserPermission($target, string $permissionType = 'user'): bool
public function checkUserPermission($target, string $permissionType = Permission::USER): bool
{
return $this->aclManager->checkUserPermission($this->user, $target, $permissionType);
}

View File

@@ -87,7 +87,7 @@ class DefaultAccessChecker implements
return $this->scopeChecker->check($data, $action, $checkerData);
}
private function checkScope(User $user, ScopeData $data, ?string $action = null): bool
private function checkScope(ScopeData $data, ?string $action = null): bool
{
$checkerData = ScopeCheckerData
::createBuilder()
@@ -100,27 +100,27 @@ class DefaultAccessChecker implements
public function check(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $data);
return $this->checkScope($data);
}
public function checkCreate(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $data, Table::ACTION_CREATE);
return $this->checkScope($data, Table::ACTION_CREATE);
}
public function checkRead(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $data, Table::ACTION_READ);
return $this->checkScope($data, Table::ACTION_READ);
}
public function checkEdit(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $data, Table::ACTION_EDIT);
return $this->checkScope($data, Table::ACTION_EDIT);
}
public function checkDelete(User $user, ScopeData $data): bool
{
if ($this->checkScope($user, $data, Table::ACTION_DELETE)) {
if ($this->checkScope($data, Table::ACTION_DELETE)) {
return true;
}
@@ -137,7 +137,7 @@ class DefaultAccessChecker implements
public function checkStream(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $data, Table::ACTION_STREAM);
return $this->checkScope($data, Table::ACTION_STREAM);
}
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool

View File

@@ -99,7 +99,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
return false;
}
$assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission');
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
if (
$assignmentPermission === Table::LEVEL_YES ||
@@ -157,7 +157,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
protected function isPermittedTeams(User $user, Entity $entity): bool
{
$assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission');
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
if (!in_array($assignmentPermission, [Table::LEVEL_TEAM, Table::LEVEL_NO])) {
return true;
@@ -219,7 +219,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
private function isPermittedTeamsEmpty(User $user, CoreEntity $entity): bool
{
$assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission');
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
if ($assignmentPermission !== Table::LEVEL_TEAM) {
return true;
@@ -259,7 +259,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
return false;
}
$assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission');
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
if (
$assignmentPermission === Table::LEVEL_YES ||

View File

@@ -53,7 +53,7 @@ class OwnerUserFieldProvider
/**
* Get an entity field that stores an owner-user (or multiple users).
* Must be link or linkMulitple field. NULL means no owner.
* Must be a link or linkMultiple field. NULL means no owner.
*/
public function get(string $entityType): ?string
{

View File

@@ -0,0 +1,43 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl;
class Permission
{
public const ASSIGNMENT = 'assignment';
public const USER = 'user';
public const PORTAL = 'portal';
public const MASS_UPDATE = 'massUpdate';
public const EXPORT = 'export';
public const AUDIT = 'audit';
public const DATA_PRIVACY = 'dataPrivacy';
public const MESSAGE = 'message';
public const MENTION = 'mention';
}

View File

@@ -29,6 +29,7 @@
namespace Espo\Core;
use Espo\Core\Acl\Permission;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
@@ -69,7 +70,7 @@ use InvalidArgumentException;
*/
class AclManager
{
protected const PERMISSION_ASSIGNMENT = 'assignment';
protected const PERMISSION_ASSIGNMENT = Permission::ASSIGNMENT;
/** @var array<string, AccessChecker> */
private $accessCheckerHashMap = [];
@@ -572,7 +573,7 @@ class AclManager
*
* @param User|string $target User entity or user ID.
*/
public function checkUserPermission(User $user, $target, string $permissionType = 'user'): bool
public function checkUserPermission(User $user, $target, string $permissionType = Permission::USER): bool
{
$permission = $this->getPermissionLevel($user, $permissionType);

View File

@@ -195,8 +195,12 @@ class Auth
throw new BadRequest("Auth: Bad authorization string provided.");
}
/** @var array{string, string} */
return explode(':', $stringDecoded, 2);
[$username, $password] = explode(':', $stringDecoded, 2);
$username = trim($username);
$password = trim($password);
return [$username, $password];
}
private function handleSecondStepRequired(Response $response, Result $result): void
@@ -237,7 +241,9 @@ class Auth
$response->writeBody($e->getBody());
}
$this->log->notice("Auth: " . $e->getMessage());
if ($e->getMessage()) {
$this->log->notice("Auth exception: {message}", ['message' => $e->getMessage()]);
}
return;
}
@@ -314,6 +320,14 @@ class Auth
$username = $request->getServerParam('PHP_AUTH_USER');
$password = $request->getServerParam('PHP_AUTH_PW');
if (is_string($username)) {
$username = trim($username);
}
if (is_string($password)) {
$password = trim($password);
}
return [$username, $password];
}

View File

@@ -37,9 +37,11 @@ use Espo\Core\Exceptions\HasBody;
use Espo\Core\Exceptions\HasLogLevel;
use Espo\Core\Exceptions\HasLogMessage;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
use LogicException;
use Psr\Log\LogLevel;
use RuntimeException;
use Throwable;
/**
@@ -80,7 +82,7 @@ class ErrorOutput
NotFound::class,
];
public function __construct(private Log $log, private Config $config)
public function __construct(private Log $log)
{}
public function process(
@@ -112,6 +114,11 @@ class ErrorOutput
): void {
$message = $exception->getMessage();
if ($exception->getPrevious() && $exception->getPrevious()->getMessage()) {
$message .= " " . $exception->getPrevious()->getMessage();
}
$statusCode = $exception->getCode();
if ($exception instanceof HasLogMessage) {
@@ -122,30 +129,12 @@ class ErrorOutput
$this->processRoute($route, $request, $exception);
}
$logLevel = $exception instanceof HasLogLevel ?
$exception->getLogLevel() :
Log::LEVEL_ERROR;
$level = $this->getLevel($exception);
$messageLineFile =
'line: ' . $exception->getLine() . ', ' .
'file: ' . $exception->getFile();
$logMessageItemList = [];
if ($message) {
$logMessageItemList[] = "$message";
}
$logMessageItemList[] = $request->getMethod() . ' ' . $request->getResourcePath();
$logMessageItemList[] = $messageLineFile;
$logMessage = "($statusCode) " . implode("; ", $logMessageItemList);
if ($this->toPrintTrace()) {
$logMessage .= " :: " . $exception->getTraceAsString();
}
$this->log->log($logLevel, $logMessage);
$this->log->log($level, $message, [
'exception' => $exception,
'request' => $request,
]);
if (!in_array($statusCode, $this->allowedStatusCodeList)) {
$statusCode = 500;
@@ -224,6 +213,11 @@ class ErrorOutput
$requestBodyString = $this->clearPasswords($request->getBodyContents() ?? '');
$message = $exception->getMessage();
if ($exception->getPrevious() && $exception->getPrevious()->getMessage()) {
$message .= " " . $exception->getPrevious()->getMessage();
}
$statusCode = $exception->getCode();
$routeParams = $request->getRouteParams();
@@ -250,12 +244,7 @@ class ErrorOutput
$logMessage .= implode("; ", $logMessageItemList);
$this->log->log('debug', $logMessage);
}
private function toPrintTrace(): bool
{
return (bool) $this->config->get('logger.printTrace');
$this->log->debug($logMessage);
}
private function toPrintExceptionStatusReason(Throwable $exception): bool
@@ -269,4 +258,21 @@ class ErrorOutput
return false;
}
private function getLevel(Throwable $exception): string
{
if ($exception instanceof HasLogLevel) {
return $exception->getLogLevel();
}
if ($exception instanceof LogicException) {
return LogLevel::ALERT;
}
if ($exception instanceof RuntimeException) {
return LogLevel::CRITICAL;
}
return LogLevel::ERROR;
}
}

View File

@@ -40,7 +40,6 @@ use Exception;
class Command implements Runner
{
use Cli;
use SetupSystemUser;
public function __construct(private ConsoleCommandManager $commandManager)
{}

View File

@@ -95,6 +95,7 @@ class Authentication
* Process logging in.
*
* @throws ServiceUnavailable
* @throws Forbidden
*/
public function login(AuthenticationData $data, Request $request, Response $response): Result
{
@@ -107,16 +108,21 @@ class Authentication
$method &&
!$this->configDataProvider->authenticationMethodIsApi($method)
) {
$this->log
->warning("AUTH: Trying to use not allowed authentication method '$method'.");
$this->log->warning("Auth: Trying to use not allowed authentication method '{method}'.", [
'method' => $method,
]);
return $this->processFail(Result::fail(FailReason::METHOD_NOT_ALLOWED), $data, $request);
}
$this->hookManager->processBeforeLogin($data, $request);
try {
$this->hookManager->processBeforeLogin($data, $request);
} catch (Forbidden $e) {
$this->processForbidden($e);
}
if (!$method && $password === null) {
$this->log->error("AUTH: Trying to login w/o password.");
$this->log->error("Auth: Trying to login w/o password.");
return Result::fail(FailReason::NO_PASSWORD);
}
@@ -157,7 +163,9 @@ class Authentication
if (($byTokenAndUsername || $byTokenOnly) && !$authToken) {
if ($username) {
$this->log->info("AUTH: Trying to login as user '$username' by token but token is not found.");
$this->log->info("Auth: Trying to login as user '{username}' by token but token is not found.", [
'username' => $username,
]);
}
return $this->processFail(Result::fail(FailReason::TOKEN_NOT_FOUND), $data, $request);
@@ -213,15 +221,7 @@ class Authentication
return $this->processFail(Result::fail(FailReason::DENIED), $data, $request);
}
if ($this->isPortal()) {
$user->set('portalId', $this->getPortal()->getId());
}
if (!$this->isPortal()) {
$user->loadLinkMultipleField('teams');
}
$user->set('ipAddress', $this->util->obtainIpFromRequest($request));
$this->prepareUser($user, $request);
[$loggedUser, $anotherUserFailReason] = $this->getLoggedUser($request, $user);
@@ -241,10 +241,16 @@ class Authentication
$result = $this->processTwoFactor($result, $request);
if ($result->isFail()) {
return $this->processFail($result, $data, $request);
return $this->processTwoFactorFail($result, $data, $request, $authLogRecord);
}
}
try {
$this->hookManager->processOnLogin($result, $data, $request);
} catch (Forbidden $e) {
$this->processForbidden($e, $authLogRecord);
}
if (
!$result->isSecondStepRequired() &&
$request->getHeader(self::HEADER_ESPO_AUTHORIZATION)
@@ -344,13 +350,13 @@ class Authentication
private function processAuthTokenCheck(AuthToken $authToken): bool
{
if ($this->isPortal() && $authToken->getPortalId() !== $this->getPortal()->getId()) {
$this->log->info("AUTH: Trying to login to portal with a token not related to portal.");
$this->log->info("Auth: Trying to login to portal with a token not related to portal.");
return false;
}
if (!$this->isPortal() && $authToken->getPortalId()) {
$this->log->info("AUTH: Trying to login to crm with a token related to portal.");
$this->log->info("Auth: Trying to login to crm with a token related to portal.");
return false;
}
@@ -361,8 +367,9 @@ class Authentication
private function processUserCheck(User $user, ?AuthLogRecord $authLogRecord): bool
{
if (!$user->isActive()) {
$this->log
->info("AUTH: Trying to login as user '" . $user->getUserName() . "' which is not active.");
$this->log->info("Auth: Trying to login as user '{username}' which is not active.", [
'username' => $user->getUserName(),
]);
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_INACTIVE_USER);
@@ -370,8 +377,9 @@ class Authentication
}
if ($user->isSystem()) {
$this->log
->info("AUTH: Trying to login to crm as a system user '{$user->getUserName()}'.");
$this->log->info("Auth: Trying to login to crm as a system user '{username}'.", [
'username' => $user->getUserName(),
]);
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_IS_SYSTEM_USER);
@@ -379,8 +387,9 @@ class Authentication
}
if (!$user->isAdmin() && !$this->isPortal() && $user->isPortal()) {
$this->log
->info("AUTH: Trying to login to crm as a portal user '" . $user->getUserName() . "'.");
$this->log->info("Auth: Trying to login to crm as a portal user '{username}'.", [
'username' => $user->getUserName(),
]);
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_IS_PORTAL_USER);
@@ -388,8 +397,9 @@ class Authentication
}
if ($this->isPortal() && !$user->isPortal()) {
$this->log->info(
"AUTH: Trying to login to portal as user '" . $user->getUserName() . "' which is not portal user.");
$this->log->info("Auth: Trying to login to portal as user '{username}' which is not portal user.", [
'username' => $user->getUserName(),
]);
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_IS_NOT_PORTAL_USER);
@@ -403,9 +413,12 @@ class Authentication
->isRelated($user);
if (!$isPortalRelatedToUser) {
$this->log->info(
"AUTH: Trying to login to portal as user '" . $user->getUserName() . "' ".
"which is portal user but does not belong to portal.");
$msg = "Auth: Trying to login to portal as user '{username}' " .
"which is portal user but does not belong to portal.";
$this->log->info($msg, [
'username' => $user->getUserName(),
]);
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_USER_IS_NOT_IN_PORTAL);
@@ -771,4 +784,56 @@ class Authentication
return [$loggedUser, null];
}
private function prepareUser(User $user, Request $request): void
{
if ($this->isPortal()) {
$user->set('portalId', $this->getPortal()->getId());
}
if (!$this->isPortal()) {
$user->loadLinkMultipleField('teams');
}
$user->set('ipAddress', $this->util->obtainIpFromRequest($request));
}
/**
* @throws Forbidden
*/
private function processForbidden(Forbidden $exception, ?AuthLogRecord $authLogRecord = null): never
{
$this->log->warning('Auth: Forbidden. {message}', [
'message' => $exception->getMessage(),
'exception' => $exception,
]);
if ($authLogRecord) {
$authLogRecord
->setIsDenied()
->setDenialReason(AuthLogRecord::DENIAL_REASON_FORBIDDEN);
$this->entityManager->saveEntity($authLogRecord);
}
throw new Forbidden();
}
private function processTwoFactorFail(
Result $result,
AuthenticationData $data,
Request $request,
?AuthLogRecord $authLogRecord
): Result {
if ($authLogRecord) {
$authLogRecord
->setIsDenied()
->setDenialReason(AuthLogRecord::DENIAL_REASON_WRONG_CODE);
$this->entityManager->saveEntity($authLogRecord);
}
return $this->processFail($result, $data, $request);
}
}

View File

@@ -147,4 +147,25 @@ class ConfigDataProvider
return $list;
}
public function ipAddressCheck(): bool
{
return (bool) $this->config->get('authIpAddressCheck');
}
/**
* @return string[]
*/
public function getIpAddressWhitelist(): array
{
return $this->config->get('authIpAddressWhitelist') ?? [];
}
/**
* @return string[]
*/
public function getIpAddressCheckExcludedUserIdList(): array
{
return $this->config->get('authIpAddressCheckExcludedUsersIds') ?? [];
}
}

View File

@@ -36,11 +36,12 @@ use Espo\Core\Exceptions\ServiceUnavailable;
/**
* Before logging in, before credentials are checked.
*
* @throws Forbidden
* @throws ServiceUnavailable
*/
interface BeforeLogin
{
/**
* @throws Forbidden
* @throws ServiceUnavailable
*/
public function process(AuthenticationData $data, Request $request): void;
}

View File

@@ -35,7 +35,6 @@ use Espo\Core\Authentication\AuthenticationData;
use Espo\Core\Api\Request;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Authentication\ConfigDataProvider;
use Espo\Core\Utils\Log;
use Espo\ORM\EntityManager;
use Espo\Entities\AuthLogRecord;
@@ -51,7 +50,6 @@ class FailedAttemptsLimit implements BeforeLogin
public function __construct(
private ConfigDataProvider $configDataProvider,
private EntityManager $entityManager,
private Log $log,
private Util $util
) {}
@@ -70,6 +68,8 @@ class FailedAttemptsLimit implements BeforeLogin
return;
}
$isSecondStep = $request->getHeader('Espo-Authorization-Code') !== null;
$failedAttemptsPeriod = $this->configDataProvider->getFailedAttemptsPeriod();
$maxFailedAttempts = $this->configDataProvider->getMaxFailedAttemptNumber();
@@ -82,14 +82,21 @@ class FailedAttemptsLimit implements BeforeLogin
throw new RuntimeException($e->getMessage());
}
$ip = $this->util->obtainIpFromRequest($request);
$ipAddress = $this->util->obtainIpFromRequest($request);
$where = [
'requestTime>' => $requestTimeFrom->format('U'),
'ipAddress' => $ip,
'isDenied' => true,
];
if ($isSecondStep) {
$where['username'] = $data->getUsername();
}
if (!$isSecondStep) {
$where['ipAddress'] = $ipAddress;
}
$wasFailed = (bool) $this->entityManager
->getRDBRepository(AuthLogRecord::ENTITY_TYPE)
->select(['id'])
@@ -109,8 +116,12 @@ class FailedAttemptsLimit implements BeforeLogin
return;
}
$this->log->warning("AUTH: Max failed login attempts exceeded for IP '$ip'.");
if ($isSecondStep) {
$username = $data->getUsername() ?? '';
throw new Forbidden("Max failed login attempts exceeded.");
throw new Forbidden("Max failed 2FA login attempts exceeded for username '$username'.");
}
throw new Forbidden("Max failed login attempts exceeded for IP address $ipAddress.");
}
}

View File

@@ -0,0 +1,87 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\Hook\Hooks;
use Espo\Core\Api\Request;
use Espo\Core\Api\Util;
use Espo\Core\Authentication\AuthenticationData;
use Espo\Core\Authentication\ConfigDataProvider;
use Espo\Core\Authentication\Hook\OnLogin;
use Espo\Core\Authentication\Result;
use Espo\Core\Authentication\Util\IpAddressUtil;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Utils\Config;
class IpAddressWhitelist implements OnLogin
{
public function __construct(
private ConfigDataProvider $configDataProvider,
private Util $util,
private Config $config,
private IpAddressUtil $ipAddressUtil
) {}
public function process(Result $result, AuthenticationData $data, Request $request): void
{
if (!$this->configDataProvider->ipAddressCheck()) {
return;
}
$ipAddress = $this->util->obtainIpFromRequest($request);
if (
$ipAddress &&
$this->ipAddressUtil->isInWhitelist($ipAddress, $this->configDataProvider->getIpAddressWhitelist())
) {
return;
}
$user = $result->getUser();
if ($user && $user->isPortal()) {
return;
}
if ($user && $user->isSuperAdmin() && $this->config->get('restrictedMode')) {
return;
}
if (
$user &&
in_array($user->getId(), $this->configDataProvider->getIpAddressCheckExcludedUserIdList())
) {
return;
}
$username = $user ? $user->getUserName() : '?';
throw new Forbidden("Not allowed IP address $ipAddress, user: $username.");
}
}

View File

@@ -29,6 +29,8 @@
namespace Espo\Core\Authentication\Hook;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\ServiceUnavailable;
use Espo\Core\Utils\Metadata;
use Espo\Core\InjectableFactory;
use Espo\Core\Authentication\AuthenticationData;
@@ -41,6 +43,10 @@ class Manager
public function __construct(private Metadata $metadata, private InjectableFactory $injectableFactory)
{}
/**
* @throws ServiceUnavailable
* @throws Forbidden
*/
public function processBeforeLogin(AuthenticationData $data, Request $request): void
{
foreach ($this->getBeforeLoginHookList() as $hook) {
@@ -48,6 +54,16 @@ class Manager
}
}
/**
* @throws Forbidden
*/
public function processOnLogin(Result $result, AuthenticationData $data, Request $request): void
{
foreach ($this->getOnLoginHookList() as $hook) {
$hook->process($result, $data, $request);
}
}
public function processOnFail(Result $result, AuthenticationData $data, Request $request): void
{
foreach ($this->getOnFailHookList() as $hook) {
@@ -102,6 +118,21 @@ class Manager
return $list;
}
/**
* @return OnLogin[]
*/
private function getOnLoginHookList(): array
{
$list = [];
foreach ($this->getHookClassNameList('onLogin') as $className) {
/** @var class-string<OnLogin> $className */
$list[] = $this->injectableFactory->create($className);
}
return $list;
}
/**
* @return OnResult[]
*/

View File

@@ -0,0 +1,46 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\Hook;
use Espo\Core\Authentication\AuthenticationData;
use Espo\Core\Api\Request;
use Espo\Core\Authentication\Result;
use Espo\Core\Exceptions\Forbidden;
/**
* Once a login result is ready.
*/
interface OnLogin
{
/**
* @throws Forbidden
*/
public function process(Result $result, AuthenticationData $data, Request $request): void;
}

View File

@@ -132,9 +132,8 @@ class LdapLogin implements Login
if ($user) {
return Result::success($user);
}
else {
return Result::fail(FailReason::WRONG_CREDENTIALS);
}
return Result::fail(FailReason::WRONG_CREDENTIALS);
}
if (!$password || $username == '**logout') {
@@ -158,9 +157,10 @@ class LdapLogin implements Login
catch (Exception $e) {
$options = $this->utils->getLdapClientOptions();
$this->log->error(
'LDAP: Could not connect to LDAP server [' . $options['host'] . '], details: ' . $e->getMessage()
);
$this->log->error("LDAP: Could not connect to LDAP server host. {message}", [
'host' => $options['host'],
'message' => $e->getMessage()
]);
/** @var string $username */
@@ -170,7 +170,9 @@ class LdapLogin implements Login
return Result::fail();
}
$this->log->info('LDAP: Administrator [' . $username . '] was logged in by Espo method.');
$this->log->info("LDAP: Administrator '{username}' was logged in by Espo method.", [
'username' => $username,
]);
}
$userDn = null;
@@ -182,15 +184,16 @@ class LdapLogin implements Login
$userDn = $this->findLdapUserDnByUsername($username);
}
catch (Exception $e) {
$this->log->error(
'Error while finding DN for [' . $username . '], details: ' . $e->getMessage() . '.'
);
$this->log->error("Error while finding DN for '{username}'. {message}", [
'username' => $username,
'message' => $e->getMessage(),
]);
}
if (!isset($userDn)) {
$this->log->error(
'LDAP: Authentication failed for user [' . $username . '], details: user is not found.'
);
$this->log->error("LDAP: Authentication failed for '{username}'; user is not found.", [
'username' => $username,
]);
$adminUser = $this->adminLogin($username, $password);
@@ -198,18 +201,24 @@ class LdapLogin implements Login
return Result::fail();
}
$this->log->info('LDAP: Administrator [' . $username . '] was logged in by Espo method.');
$this->log->info("LDAP: Administrator '{username}' was logged in by Espo method.", [
'username' => $username,
]);
}
$this->log->debug('User [' . $username . '] is found with this DN ['.$userDn.'].');
$this->log->debug("User '{username}' with DN '{dn}' is found .", [
'username' => $username,
'dn' => $userDn,
]);
try {
$ldapClient->bind($userDn, $password);
}
catch (Exception $e) {
$this->log->error(
'LDAP: Authentication failed for user [' . $username . '], details: ' . $e->getMessage()
);
$this->log->error("LDAP: Authentication failed for '{username}'. {message}", [
'username' => $username,
'message' => $e->getMessage(),
]);
return Result::fail();
}
@@ -229,9 +238,9 @@ class LdapLogin implements Login
if (!isset($user)) {
if (!$this->utils->getOption('createEspoUser')) {
$this->log->warning(
"LDAP: Authentication success for user $username, but user is not created in EspoCRM."
);
$this->log->warning("LDAP: '{username}' authenticated, but user is not created in Espo.", [
'username' => $username,
]);
return Result::fail(FailReason::USER_NOT_FOUND);
}
@@ -259,7 +268,7 @@ class LdapLogin implements Login
$this->client = $this->clientFactory->create($options);
}
catch (Exception $e) {
$this->log->error('LDAP error: ' . $e->getMessage());
$this->log->error("LDAP error. {message}", ['message' => $e->getMessage()]);
}
}
@@ -290,7 +299,10 @@ class LdapLogin implements Login
if (strtolower($username) !== strtolower($tokenUsername)) {
$ip = $this->util->obtainIpFromRequest($request);
$this->log->alert('Unauthorized access attempt for user [' . $username . '] from IP [' . $ip . ']');
$this->log->alert("Unauthorized access attempt for user '{username}' from IP '{ip}'.", [
'username' => $username,
'ip' => $ip,
]);
return null;
}
@@ -325,11 +337,11 @@ class LdapLogin implements Login
*/
private function createUser(array $userData, bool $isPortal = false): ?User
{
$this->log->info('Creating new user...');
$this->log->info("LDAP: Creating new user.");
$data = [];
$this->log->debug('LDAP: user data: ' . print_r($userData, true));
$this->log->debug("LDAP: user data: {userData}", ['userData' => print_r($userData, true)]);
$ldapFields = $this->loadFields('ldap');
@@ -337,7 +349,10 @@ class LdapLogin implements Login
$ldap = strtolower($ldap);
if (isset($userData[$ldap][0])) {
$this->log->debug('LDAP: Create a user with [' . $espo . '] = [' . $userData[$ldap][0] . '].');
$this->log->debug("LDAP: Create a user with [{user1}] = [{user2}].", [
'user1' => $espo,
'user2' => $userData[$ldap][0],
]);
$data[$espo] = $userData[$ldap][0];
}
@@ -410,7 +425,7 @@ class LdapLogin implements Login
/** @noinspection PhpRedundantOptionalArgumentInspection */
$result = $ldapClient->search($searchString, null, Ldap::SEARCH_SCOPE_SUB);
$this->log->debug('LDAP: user search string: "' . $searchString . '"');
$this->log->debug("LDAP: user search string: {string}.", ['string' => $searchString]);
foreach ($result as $item) {
return $item["dn"];

View File

@@ -43,7 +43,7 @@ use Espo\ORM\EntityManager;
/**
* Compatible only with default Espo auth tokens.
*
* @todo Use a token-sessionId map to retrieve tokens.
* @todo Use a token-sessionId map to retrieve tokens. Send sid claim in id_token.
*/
class BackchannelLogout
{

View File

@@ -214,7 +214,7 @@ class ConfigDataProvider
public function getAuthorizationPrompt(): string
{
return $this->config->get('oidcAuthorizationPrompt') ?? 'consent';
return $this->object->get('oidcAuthorizationPrompt') ?? 'consent';
}
public function getAuthorizationMaxAge(): ?int

View File

@@ -51,6 +51,10 @@ class DefaultUserProvider implements UserProvider
{
$user = $this->findUser($payload);
if ($user === false) {
return null;
}
if ($user) {
$this->syncUser($user, $payload);
@@ -60,7 +64,10 @@ class DefaultUserProvider implements UserProvider
return $this->tryToCreateUser($payload);
}
private function findUser(Payload $payload): ?User
/**
* @return User|false|null
*/
private function findUser(Payload $payload): User|bool|null
{
$usernameClaim = $this->configDataProvider->getUsernameClaim();
@@ -82,24 +89,26 @@ class DefaultUserProvider implements UserProvider
return null;
}
if (!$user->isActive()) {
return null;
}
$userId = $user->getId();
if (!$user->isActive()) {
$this->log->info("Oidc: User $userId found but it's not active.");
return false;
}
$isPortal = $this->applicationState->isPortal();
if (!$isPortal && !$user->isRegular() && !$user->isAdmin()) {
$this->log->info("Oidc: User $userId found but it's neither regular user not admin.");
$this->log->info("Oidc: User $userId found but it's neither regular user nor admin.");
return null;
return false;
}
if ($isPortal && !$user->isPortal()) {
$this->log->info("Oidc: User $userId found but it's not portal user.");
return null;
return false;
}
if ($isPortal) {
@@ -108,20 +117,20 @@ class DefaultUserProvider implements UserProvider
if (!$user->getPortals()->hasId($portalId)) {
$this->log->info("Oidc: User $userId found but it's not related to current portal.");
return null;
return false;
}
}
if ($user->isSuperAdmin()) {
$this->log->info("Oidc: User $userId found but it's super-admin, not allowed.");
return null;
return false;
}
if ($user->isAdmin() && !$this->configDataProvider->allowAdminUser()) {
$this->log->info("Oidc: User $userId found but it's admin, not allowed.");
return null;
return false;
}
return $user;

View File

@@ -0,0 +1,59 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\Util;
use CIDRmatch\CIDRmatch;
class IpAddressUtil
{
/**
* @param string $ipAddress An IP address.
* @param string[] $whitelist A whitelist. IPs or IP ranges in CIDR notation.
*/
public function isInWhitelist(string $ipAddress, array $whitelist): bool
{
$cidrMatch = new CIDRmatch();
foreach ($whitelist as $whiteIpAddress) {
if ($ipAddress === $whiteIpAddress) {
return true;
}
if (
str_contains($whiteIpAddress, '/') &&
$cidrMatch->match($ipAddress, $whiteIpAddress)
) {
return true;
}
}
return false;
}
}

View File

@@ -29,6 +29,8 @@
namespace Espo\Core\Console;
use Espo\Core\ApplicationUser;
use Espo\Core\Console\Exceptions\InvalidArgument;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\Util;
@@ -44,12 +46,14 @@ class CommandManager
private const DEFAULT_COMMAND = 'Help';
private const DEFAULT_COMMAND_FLAG = 'help';
public function __construct(private InjectableFactory $injectableFactory, private Metadata $metadata)
{}
public function __construct(
private InjectableFactory $injectableFactory,
private Metadata $metadata,
private ApplicationUser $applicationUser
) {}
/**
* @param array<int, string> $argv
*
* @return int<0, 255> Exit-status.
*/
public function run(array $argv): int
@@ -73,8 +77,12 @@ class CommandManager
throw new CommandNotSpecified("Command name is not specified.");
}
$this->checkParams($command, $params);
$io = new IO();
$this->setupUser($command);
$commandObj = $this->createCommand($command);
if (!$commandObj instanceof Command) {
@@ -146,4 +154,59 @@ class CommandManager
{
return Params::fromArgs(array_slice($argv, 1));
}
private function setupUser(string $command): void
{
$noSystemUser = $this->metadata->get(['app', 'consoleCommands', lcfirst($command), 'noSystemUser']);
if ($noSystemUser) {
return;
}
$this->applicationUser->setupSystemUser();
}
private function checkParams(string $command, Params $params): void
{
$this->checkOptions($command, $params);
$this->checkFlags($command, $params);
}
private function checkOptions(string $command, Params $params): void
{
$allowedOptions = $this->metadata->get(['app', 'consoleCommands', lcfirst($command), 'allowedOptions']);
if (!is_array($allowedOptions)) {
return;
}
$notAllowedOptions = array_diff(array_keys($params->getOptions()), $allowedOptions);
if ($notAllowedOptions === []) {
return;
}
$msg = sprintf("Not allowed options: %s.", implode(', ', $notAllowedOptions));
throw new InvalidArgument($msg);
}
private function checkFlags(string $command, Params $params): void
{
$allowedFlags = $this->metadata->get(['app', 'consoleCommands', lcfirst($command), 'allowedFlags']);
if (!is_array($allowedFlags)) {
return;
}
$notAllowedFlags = array_diff($params->getFlagList(), $allowedFlags);
if ($notAllowedFlags === []) {
return;
}
$msg = sprintf("Not allowed flags: %s.", implode(', ', $notAllowedFlags));
throw new InvalidArgument($msg);
}
}

View File

@@ -0,0 +1,54 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Console\Commands;
use Espo\Core\Console\Command;
use Espo\Core\Console\Command\Params;
use Espo\Core\Console\IO;
use Espo\Core\Exceptions\Error;
use Espo\Core\Upgrades\Migration\Runner;
/**
* @noinspection PhpUnused
*/
class Migrate implements Command
{
public function __construct(
private Runner $runner
) {}
/**
* @throws Error
*/
public function run(Params $params, IO $io): void
{
$this->runner->run($io);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Console\Commands;
use Espo\Core\Console\Command;
use Espo\Core\Console\Command\Params;
use Espo\Core\Console\IO;
use Espo\Core\Upgrades\Migration\AfterUpgradeRunner;
use RuntimeException;
/**
* @noinspection PhpUnused
*/
class MigrationVersionStep implements Command
{
public function __construct(
private AfterUpgradeRunner $afterUpgradeRunner
) {}
public function run(Params $params, IO $io): void
{
$step = $params->getOption('step');
if (!$step) {
throw new RuntimeException("No step parameter.");
}
$this->afterUpgradeRunner->run($step);
$io->writeLine("Done.");
$io->setExitStatus(0);
}
}

View File

@@ -63,13 +63,7 @@ class UpgradeStep implements Command
$stepName = $options['step'];
$upgradeId = $options['id'];
$result = $this->runUpgradeStep($stepName, ['id' => $upgradeId]);
if (!$result) {
echo "false";
return;
}
$this->runUpgradeStep($stepName, ['id' => $upgradeId]);
echo "true";
}
@@ -77,7 +71,7 @@ class UpgradeStep implements Command
/**
* @param array<string, mixed> $params
*/
private function runUpgradeStep(string $stepName, array $params): bool
private function runUpgradeStep(string $stepName, array $params): void
{
$app = new Application();
@@ -86,12 +80,10 @@ class UpgradeStep implements Command
$upgradeManager = new UpgradeManager($app->getContainer());
try {
$result = $upgradeManager->runInstallStep($stepName, $params);
$upgradeManager->runInstallStep($stepName, $params);
}
catch (Exception $e) {
die("Error: " . $e->getMessage());
}
return $result;
}
}

View File

@@ -308,7 +308,9 @@ class Container implements ContainerInterface
return $loader;
}
assert($this->configuration !== null);
if ($this->configuration === null) {
throw new RuntimeException("Container configuration is not ready.");
}
return $this->configuration->getLoaderClassName($id);
}

View File

@@ -36,6 +36,7 @@ use Espo\Core\Binding\BindingContainer;
use Espo\Core\Binding\BindingLoader;
use Espo\Core\Binding\EspoBindingLoader;
use Espo\Core\Loaders\ApplicationState as ApplicationStateLoader;
use Espo\Core\Utils\File\Manager as FileManager;
use Espo\Core\Utils\Config\ConfigFileManager;
use Espo\Core\Utils\Config;
@@ -71,6 +72,7 @@ class ContainerBuilder
'log' => LogLoader::class,
'dataManager' => DataManagerLoader::class,
'metadata' => MetadataLoader::class,
'applicationState' => ApplicationStateLoader::class,
];
public function withBindingLoader(BindingLoader $bindingLoader): self

View File

@@ -37,6 +37,16 @@ class ConfigDataProvider
public function __construct(private Config $config)
{}
/**
* Get decimal places.
*
* @since 8.3.0
*/
public function getDecimalPlaces(): ?int
{
return $this->config->get('currencyDecimalPlaces');
}
/**
* Get a system default currency.
*/

View File

@@ -220,6 +220,7 @@ class Starter
$clientManager->setBasePath($this->clientManager->getBasePath());
$clientManager->setApiUrl('api/v1/portal-access/' . $portalId);
$clientManager->setApplicationId($portalId);
$params = RunnerParams::fromArray([
'entryPoint' => $entryPoint,

View File

@@ -41,20 +41,13 @@ use Espo\ORM\Defs as OrmDefs;
*/
class NotJoinedLoader implements LoaderInterface
{
private OrmDefs $ormDefs;
/** @var array<string, string[]> */
private array $fieldListCacheMap = [];
private EntityManager $entityManager;
/**
* @var array<string, string[]>
*/
private $fieldListCacheMap = [];
public function __construct(OrmDefs $ormDefs, EntityManager $entityManager)
{
$this->ormDefs = $ormDefs;
$this->entityManager = $entityManager;
}
public function __construct(
private OrmDefs $ormDefs,
private EntityManager $entityManager
) {}
public function process(Entity $entity, Params $params): void
{
@@ -92,12 +85,25 @@ class NotJoinedLoader implements LoaderInterface
->findOne();
if (!$foreignEntity) {
/** @noinspection PhpRedundantOptionalArgumentInspection */
$entity->set($nameAttribute, null);
return;
}
$entity->set($nameAttribute, $foreignEntity->get('name'));
$name = $foreignEntity->get('name');
if ($name === null) {
$foreignEntity = $this->entityManager
->getRDBRepository($foreignEntityType)
->getById($id);
if ($foreignEntity) {
$name = $foreignEntity->get('name');
}
}
$entity->set($nameAttribute, $name);
}
/**
@@ -118,9 +124,10 @@ class NotJoinedLoader implements LoaderInterface
continue;
}
if (!$relationDefs->getParam('noJoin')) {
// Commented to load name of leads w/o person name.
/*if (!$relationDefs->getParam('noJoin')) {
continue;
}
}*/
if (!$relationDefs->hasForeignEntityType()) {
continue;

View File

@@ -29,6 +29,7 @@
namespace Espo\Core\FieldProcessing\LinkParent;
use Espo\Core\ORM\Type\FieldType;
use Espo\ORM\Entity;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\FieldProcessing\Loader as LoaderInterface;
@@ -72,18 +73,12 @@ class Loader implements LoaderInterface
$entityDefs = $this->ormDefs->getEntity($entityType);
foreach ($entityDefs->getFieldList() as $fieldDefs) {
if ($fieldDefs->getType() !== 'linkParent') {
if ($fieldDefs->getType() !== FieldType::LINK_PARENT) {
continue;
}
$name = $fieldDefs->getName();
if (!$entityDefs->hasRelation($fieldDefs->getName())) {
// Otherwise, loadParentNameField produces an error.
// @todo Revise.
continue;
}
$list[] = $name;
}

View File

@@ -29,10 +29,9 @@
namespace Espo\Core\FieldProcessing\Reminder;
use Espo\Entities\User;
use Espo\Modules\Crm\Entities\Reminder;
use Espo\ORM\Collection;
use Espo\ORM\Entity;
use Espo\Core\FieldProcessing\Loader as LoaderInterface;
use Espo\Core\FieldProcessing\Loader\Params;
use Espo\Core\ORM\EntityManager;
@@ -43,8 +42,10 @@ use Espo\Core\ORM\EntityManager;
*/
class Loader implements LoaderInterface
{
public function __construct(private EntityManager $entityManager)
{}
public function __construct(
private EntityManager $entityManager,
private User $user
) {}
public function process(Entity $entity, Params $params): void
{
@@ -65,19 +66,20 @@ class Loader implements LoaderInterface
}
/**
* @return \stdClass[]
* @return object{seconds: int, type: string}[]
*/
private function fetchReminderDataList(Entity $entity): array
{
$list = [];
/** @var Collection<Reminder> $collection */
/** @var iterable<Reminder> $collection */
$collection = $this->entityManager
->getRDBRepository(Reminder::ENTITY_TYPE)
->select(['seconds', 'type'])
->where([
'entityType' => $entity->getEntityType(),
'entityId' => $entity->getId(),
'userId' => $this->user->getId(),
])
->distinct()
->order('seconds')

View File

@@ -29,9 +29,13 @@
namespace Espo\Core\FieldProcessing\Reminder;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\Core\Field\DateTime;
use Espo\Core\Utils\Id\RecordIdGenerator;
use Espo\Core\Utils\Metadata;
use Espo\Entities\Preferences;
use Espo\Entities\User;
use Espo\Modules\Crm\Entities\Reminder;
use Espo\Modules\Crm\Entities\Task;
use Espo\ORM\Entity;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\FieldProcessing\Saver as SaverInterface;
@@ -39,8 +43,6 @@ use Espo\Core\FieldProcessing\Saver\Params;
use Espo\Core\ORM\EntityManager;
use stdClass;
use DateInterval;
use DateTime;
/**
* @internal This class should not be removed as it's used by custom entities.
@@ -53,177 +55,377 @@ class Saver implements SaverInterface
public function __construct(
private EntityManager $entityManager,
private RecordIdGenerator $idGenerator
private RecordIdGenerator $idGenerator,
private User $user,
private Metadata $metadata
) {}
/**
* @param CoreEntity $entity
*/
public function process(Entity $entity, Params $params): void
{
$entityType = $entity->getEntityType();
$hasReminder = $this->entityManager
->getDefs()
->getEntity($entityType)
->hasField('reminders');
if (!$hasReminder) {
if (!$this->hasRemindersField($entityType)) {
return;
}
$dateAttribute = $this->entityManager
->getDefs()
->getEntity($entityType)
->getField('reminders')
->getParam('dateField') ??
$this->dateAttribute;
$dateAttribute = $this->getDateAttribute($entityType);
$toProcess =
$entity->isNew() ||
$entity->isAttributeChanged('assignedUserId') ||
($entity->hasLinkMultipleField('assignedUsers') && $entity->isAttributeChanged('assignedUsersIds')) ||
($entity->hasLinkMultipleField('users') && $entity->isAttributeChanged('usersIds')) ||
$entity->isAttributeChanged($dateAttribute) ||
$entity->has('reminders');
if ($this->toRemove($entity)) {
$this->deleteAll($entity);
if (!$toProcess) {
return;
}
$reminderTypeList = $this->entityManager
->getDefs()
->getEntity(Reminder::ENTITY_TYPE)
->getField('type')
->getParam('options') ?? [];
$reminderList = $entity->has('reminders') ?
$entity->get('reminders') :
$this->getEntityReminderDataList($entity);
if (!$entity->isNew()) {
$query = $this->entityManager
->getQueryBuilder()
->delete()
->from(Reminder::ENTITY_TYPE)
->where([
'entityId' => $entity->getId(),
'entityType' => $entityType,
'deleted' => false,
])
->build();
$this->entityManager->getQueryExecutor()->execute($query);
}
if (empty($reminderList)) {
if (!$this->toProcess($entity, $dateAttribute)) {
return;
}
$dateValue = $entity->get($dateAttribute);
$typeList = $this->getTypeList();
if (!$entity->has($dateAttribute)) {
$reloadedEntity = $this->entityManager->getEntity($entityType, $entity->getId());
$onlyRemindersFieldChanged = $this->onlyRemindersFieldChanged($entity, $dateAttribute);
if ($reloadedEntity) {
$dateValue = $reloadedEntity->get($dateAttribute);
}
if (!$entity->isNew() && !$onlyRemindersFieldChanged) {
$this->deleteAll($entity);
}
if (!$dateValue) {
if (!$entity->isNew() && $onlyRemindersFieldChanged) {
$this->deleteAllForUser($entity);
}
$startString = $this->getStartString($entity, $dateAttribute);
if (!$startString) {
return;
}
if ($entity->hasLinkMultipleField('users')) {
$userIdList = $entity->getLinkMultipleIdList('users');
}
else if ($entity->hasLinkMultipleField('assignedUsers')) {
$userIdList = $entity->getLinkMultipleIdList('assignedUsers');
}
else {
$userIdList = [];
$userIdList = $this->getUserIdList($entity);
if ($entity->get('assignedUserId')) {
$userIdList[] = $entity->get('assignedUserId');
}
}
if (empty($userIdList)) {
if ($userIdList === []) {
return;
}
$dateValueObj = new DateTime($dateValue);
if ($onlyRemindersFieldChanged && in_array($this->user->getId(), $userIdList)) {
$userIdList = [$this->user->getId()];
}
foreach ($reminderList as $item) {
$remindAt = clone $dateValueObj;
$seconds = intval($item->seconds);
$type = $item->type;
$start = DateTime::fromString($startString);
if (!in_array($type , $reminderTypeList)) {
continue;
}
foreach ($userIdList as $userId) {
$reminderList = $userId === $this->user->getId() ?
$this->getReminderList($entity, $typeList) :
$this->getPreferencesReminderList($typeList, $userId, $entityType);
$remindAt->sub(new DateInterval('PT' . $seconds . 'S'));
foreach ($userIdList as $userId) {
$reminderId = $this->idGenerator->generate();
$query = $this->entityManager
->getQueryBuilder()
->insert()
->into(Reminder::ENTITY_TYPE)
->columns([
'id',
'entityId',
'entityType',
'type',
'userId',
'remindAt',
'startAt',
'seconds',
])
->values([
'id' => $reminderId,
'entityId' => $entity->getId(),
'entityType' => $entityType,
'type' => $type,
'userId' => $userId,
'remindAt' => $remindAt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
'startAt' => $dateValue,
'seconds' => $seconds,
])
->build();
$this->entityManager->getQueryExecutor()->execute($query);
foreach ($reminderList as $item) {
$this->createReminder($entity, $userId, $start, $item);
}
}
}
/**
* @return stdClass[]
* @return object{seconds: int, type: string}[]
*/
private function getEntityReminderDataList(Entity $entity): array
private function getEntityReminderDataList(CoreEntity $entity): array
{
$reminderDataList = [];
$dataList = [];
$reminderCollection = $this->entityManager
/** @var iterable<Reminder> $collection */
$collection = $this->entityManager
->getRDBRepository(Reminder::ENTITY_TYPE)
->select(['seconds', 'type'])
->where([
'entityType' => $entity->getEntityType(),
'entityId' => $entity->getId(),
'userId' => $this->user->getId(),
])
->distinct()
->order('seconds')
->find();
foreach ($reminderCollection as $reminder) {
$reminderDataList[] = (object) [
'seconds' => $reminder->get('seconds'),
'type' => $reminder->get('type'),
foreach ($collection as $reminder) {
$dataList[] = (object) [
'seconds' => $reminder->getSeconds(),
'type' => $reminder->getType(),
];
}
return $reminderDataList;
return $dataList;
}
private function getDateAttribute(string $entityType): string
{
return $this->entityManager
->getDefs()
->getEntity($entityType)
->getField('reminders')
->getParam('dateField') ??
$this->dateAttribute;
}
private function hasRemindersField(string $entityType): bool
{
return $this->entityManager
->getDefs()
->getEntity($entityType)
->hasField('reminders');
}
private function isNewOrChanged(CoreEntity $entity, string $dateAttribute): bool
{
return $entity->isNew() ||
$this->toReCreate($entity) ||
$entity->isAttributeChanged('assignedUserId') ||
($entity->hasLinkMultipleField('assignedUsers') && $entity->isAttributeChanged('assignedUsersIds')) ||
($entity->hasLinkMultipleField('users') && $entity->isAttributeChanged('usersIds')) ||
$entity->isAttributeChanged($dateAttribute);
}
private function toProcess(CoreEntity $entity, string $dateAttribute): bool
{
return $this->isNewOrChanged($entity, $dateAttribute) || $entity->has('reminders');
}
private function onlyRemindersFieldChanged(CoreEntity $entity, string $dateAttribute): bool
{
if ($this->isNewOrChanged($entity, $dateAttribute)) {
return false;
}
return $entity->isAttributeChanged('reminders');
}
/**
* @return string[]
*/
private function getTypeList(): array
{
return $this->entityManager
->getDefs()
->getEntity(Reminder::ENTITY_TYPE)
->getField('type')
->getParam('options') ?? [];
}
private function deleteAll(CoreEntity $entity): void
{
$query = $this->entityManager
->getQueryBuilder()
->delete()
->from(Reminder::ENTITY_TYPE)
->where([
'entityId' => $entity->getId(),
'entityType' => $entity->getEntityType(),
])
->build();
$this->entityManager->getQueryExecutor()->execute($query);
}
private function deleteAllForUser(CoreEntity $entity): void
{
$query = $this->entityManager
->getQueryBuilder()
->delete()
->from(Reminder::ENTITY_TYPE)
->where([
'entityId' => $entity->getId(),
'entityType' => $entity->getEntityType(),
'userId' => $this->user->getId(),
])
->build();
$this->entityManager->getQueryExecutor()->execute($query);
}
/**
* @return string[]
*/
private function getUserIdList(CoreEntity $entity): array
{
if ($entity->hasLinkMultipleField('users')) {
return $entity->getLinkMultipleIdList('users');
}
if ($entity->hasLinkMultipleField('assignedUsers')) {
return $entity->getLinkMultipleIdList('assignedUsers');
}
$userIdList = [];
if ($entity->get('assignedUserId')) {
$userIdList[] = $entity->get('assignedUserId');
}
return $userIdList;
}
private function getStartString(CoreEntity $entity, string $dateAttribute): ?string
{
$dateValue = $entity->get($dateAttribute);
if (!$entity->has($dateAttribute)) {
$reloadedEntity = $this->entityManager->getEntityById($entity->getEntityType(), $entity->getId());
if ($reloadedEntity) {
$dateValue = $reloadedEntity->get($dateAttribute);
}
}
return $dateValue;
}
/**
* @param string[] $typeList
* @return object{seconds: int, type: string}[]
*/
private function getReminderList(CoreEntity $entity, array $typeList): array
{
if ($entity->has('reminders')) {
/** @var ?stdClass[] $list */
$list = $entity->get('reminders');
if ($list === null) {
return [];
}
return $this->sanitizeList($list, $typeList);
}
return $this->getEntityReminderDataList($entity);
}
/**
* @param string[] $typeList
* @return object{seconds: int, type: string}[]
*/
private function getPreferencesReminderList(array $typeList, string $userId, string $entityType): array
{
$preferences = $this->entityManager->getRepositoryByClass(Preferences::class)->getById($userId);
if (!$preferences) {
return [];
}
$param = 'defaultReminders';
// @todo Refactor.
if ($entityType === Task::ENTITY_TYPE) {
$param = 'defaultRemindersTask';
}
/** @var stdClass[] $list */
$list = $preferences->get($param) ?? [];
return $this->sanitizeList($list, $typeList);
}
/**
* @param stdClass[] $list
* @param string[] $typeList
* @return object{seconds: int, type: string}[]
*/
private function sanitizeList(array $list, array $typeList): array
{
$result = [];
foreach ($list as $item) {
$seconds = ($item->seconds ?? null);
$type = ($item->type ?? null);
if (!is_int($seconds) || !in_array($type, $typeList)) {
continue;
}
$result[] = (object) [
'seconds' => $seconds,
'type' => $type,
];
}
return $result;
}
/**
* @param object{seconds: int, type: string} $item
*/
private function createReminder(
CoreEntity $entity,
string $userId,
DateTime $start,
object $item
): void {
$seconds = $item->seconds;
$type = $item->type;
$remindAt = $start->addSeconds(- $seconds);
if ($remindAt->isLessThan(DateTime::createNow())) {
return;
}
$query = $this->entityManager
->getQueryBuilder()
->insert()
->into(Reminder::ENTITY_TYPE)
->columns([
'id',
'entityId',
'entityType',
'type',
'userId',
'remindAt',
'startAt',
'seconds',
])
->values([
'id' => $this->idGenerator->generate(),
'entityId' => $entity->getId(),
'entityType' => $entity->getEntityType(),
'type' => $type,
'userId' => $userId,
'remindAt' => $remindAt->toString(),
'startAt' => $start->toString(),
'seconds' => $seconds,
])
->build();
$this->entityManager->getQueryExecutor()->execute($query);
}
private function toRemove(CoreEntity $entity): bool
{
if (!$entity->isAttributeChanged('status')) {
return false;
}
$entityType = $entity->getEntityType();
$status = $entity->get('status');
$ignoreStatusList = [
...($this->metadata->get("scopes.$entityType.completedStatusList") ?? []),
...($this->metadata->get("scopes.$entityType.canceledStatusList") ?? []),
];
return in_array($status, $ignoreStatusList);
}
private function toReCreate(CoreEntity $entity): bool
{
if (!$entity->isAttributeChanged('status')) {
return false;
}
$entityType = $entity->getEntityType();
$statusFetched = $entity->getFetched('status');
$status = $entity->get('status');
$ignoreStatusList = [
...($this->metadata->get("scopes.$entityType.completedStatusList") ?? []),
...($this->metadata->get("scopes.$entityType.canceledStatusList") ?? []),
];
return in_array($statusFetched, $ignoreStatusList) && !in_array($status, $ignoreStatusList);
}
}

View File

@@ -0,0 +1,59 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\FieldProcessing\Stars;
use Espo\Core\FieldProcessing\Loader;
use Espo\Core\FieldProcessing\Loader\Params;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\Tools\Stars\StarService;
/**
* @implements Loader<Entity>
*/
class StarLoader implements Loader
{
public function __construct(
private StarService $service,
private User $user
) {}
public function process(Entity $entity, Params $params): void
{
if (
!$entity->hasAttribute('isStarred') ||
!$this->service->isEnabled($entity->getEntityType())
) {
return;
}
$entity->set('isStarred', $this->service->isStarred($entity, $this->user));
}
}

View File

@@ -29,8 +29,9 @@
namespace Espo\Core\FieldProcessing\Wysiwyg;
use Espo\Core\ORM\Type\FieldType;
use Espo\Entities\Attachment;
use Espo\ORM\Entity;
use Espo\Core\FieldProcessing\Saver as SaverInterface;
use Espo\Core\FieldProcessing\Saver\Params;
use Espo\Core\ORM\EntityManager;
@@ -40,15 +41,11 @@ use Espo\Core\ORM\EntityManager;
*/
class Saver implements SaverInterface
{
private EntityManager $entityManager;
/** @var array<string, string[]> */
private $fieldListMapCache = [];
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function __construct(private EntityManager $entityManager)
{}
public function process(Entity $entity, Params $params): void
{
@@ -86,13 +83,13 @@ class Saver implements SaverInterface
}
foreach ($matches[1] as $id) {
$attachment = $this->entityManager->getEntity('Attachment', $id);
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($id);
if (!$attachment) {
continue;
}
if ($attachment->get('relatedId')) {
if ($attachment->getRelated()) {
continue;
}
@@ -123,7 +120,7 @@ class Saver implements SaverInterface
foreach ($entityDefs->getFieldNameList() as $name) {
$defs = $entityDefs->getField($name);
if ($defs->getType() !== 'wysiwyg') {
if ($defs->getType() !== FieldType::WYSIWYG) {
continue;
}

View File

@@ -120,7 +120,16 @@ class SanitizeManager
/** @var class-string<Sanitizer>[] $fieldClassNameList */
$fieldClassNameList = $this->metadata->get("entityDefs.$entityType.fields.$field.sanitizerClassNameList") ?? [];
$ignoreList = $this->metadata->get("entityDefs.$entityType.fields.$field.sanitizerSuppressClassNameList") ?? [];
return array_merge($classNameList, $fieldClassNameList);
$list = array_merge($classNameList, $fieldClassNameList);
if ($ignoreList === []) {
return $list;
}
$list = array_diff($list, $ignoreList);
return array_values($list);
}
}

View File

@@ -85,7 +85,9 @@ class Argument implements Evaluatable
}
if ($this->data instanceof Variable) {
return new ArgumentList([$this->data->getName()]);
$value = new Value($this->data->getName());
return new ArgumentList([$value]);
}
if ($this->data instanceof Attribute) {

View File

@@ -32,6 +32,7 @@ namespace Espo\Core\Formula;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\ExecutionException;
use Espo\Core\Formula\Exceptions\SyntaxError;
use Espo\Core\Formula\Exceptions\UnsafeFunction;
use Espo\Core\Formula\Functions\Base as DeprecatedBaseFunction;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Formula\Parser\Ast\Attribute;
@@ -46,6 +47,8 @@ use stdClass;
/**
* Creates an instance of Processor and executes a script.
*
* @internal
*/
class Evaluator
{
@@ -56,10 +59,12 @@ class Evaluator
/**
* @param array<string, class-string<BaseFunction|Func|DeprecatedBaseFunction>> $functionClassNameMap
* @param string[] $unsafeFunctionList
*/
public function __construct(
private InjectableFactory $injectableFactory,
private array $functionClassNameMap = []
private array $functionClassNameMap = [],
private array $unsafeFunctionList = []
) {
$this->attributeFetcher = $injectableFactory->create(AttributeFetcher::class);
$this->parser = new Parser();
@@ -74,6 +79,31 @@ class Evaluator
*/
public function process(string $expression, ?Entity $entity = null, ?stdClass $variables = null): mixed
{
return $this->processInternal($expression, $entity, $variables, false);
}
/**
* Process expression in safe mode.
*
* @throws SyntaxError
* @throws Error
*/
public function processSafe(string $expression, ?Entity $entity = null, ?stdClass $variables = null): mixed
{
return $this->processInternal($expression, $entity, $variables, true);
}
/**
* @throws SyntaxError
* @throws Error
*/
private function processInternal(
string $expression,
?Entity $entity,
?stdClass $variables,
bool $safeMode,
): mixed {
$processor = new Processor(
$this->injectableFactory,
$this->attributeFetcher,
@@ -84,6 +114,10 @@ class Evaluator
$item = $this->getParsedExpression($expression);
if ($safeMode) {
$this->checkIsSafe($item->getData());
}
try {
$result = $processor->process($item);
}
@@ -107,4 +141,24 @@ class Evaluator
return new Argument($this->parsedHash[$expression]);
}
/**
* @throws UnsafeFunction
*/
private function checkIsSafe(mixed $data): void
{
if (!$data instanceof Node) {
return;
}
$name = $data->getType();
if (in_array($name, $this->unsafeFunctionList)) {
throw new UnsafeFunction("$name is not safe.");
}
foreach ($data->getChildNodes() as $subData) {
$this->checkIsSafe($subData);
}
}
}

View File

@@ -35,16 +35,18 @@ namespace Espo\Core\Formula\Exceptions;
class BadArgumentValue extends Error
{
private ?int $position = null;
private ?string $logMessage = null;
/**
* Create.
*
* @param int $position An argument position.
*/
public static function create(int $position): self
public static function create(int $position, ?string $message = null): self
{
$obj = new self();
$obj->position = $position;
$obj->logMessage = $message;
return $obj;
}
@@ -53,6 +55,12 @@ class BadArgumentValue extends Error
{
$position = (string) ($this->position ?? '?');
return "Bad argument value on position {$position}.";
$message = "Bad argument value on position $position.";
if ($this->logMessage) {
$message .= " " . $this->logMessage;
}
return $message;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Exceptions;
class UnsafeFunction extends Error
{
}

View File

@@ -0,0 +1,47 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula;
use Espo\Core\Formula\Exceptions\Error;
/**
* A function aware of variables.
*
* @since 8.3.0
*/
interface FuncVariablesAware
{
/**
* Process.
*
* @throws Error
*/
public function process(EvaluatedArgumentList $arguments, Variables $variables): mixed;
}

View File

@@ -30,7 +30,6 @@
namespace Espo\Core\Formula;
use Espo\Core\Formula\Exceptions\UnknownFunction;
use Espo\Core\Formula\Functions\Base;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\ORM\Entity;
@@ -59,8 +58,12 @@ class FunctionFactory
/**
* @throws UnknownFunction
*/
public function create(string $name, ?Entity $entity = null, ?stdClass $variables = null): Func|BaseFunction|Base
{
public function create(
string $name,
?Entity $entity = null,
?stdClass $variables = null
): Func|FuncVariablesAware|BaseFunction|Base {
if ($this->classNameMap && array_key_exists($name, $this->classNameMap)) {
$className = $this->classNameMap[$name];
}
@@ -77,7 +80,7 @@ class FunctionFactory
$typeName = implode('\\', $arr);
/** @var class-string<Func|BaseFunction|Base> $className */
/** @var class-string<Func|FuncVariablesAware|BaseFunction|Base> $className */
$className = 'Espo\\Core\\Formula\\Functions\\' . $typeName . 'Type';
}
@@ -85,7 +88,12 @@ class FunctionFactory
throw new UnknownFunction("Unknown function: " . $name);
}
if ((new ReflectionClass($className))->implementsInterface(Func::class)) {
$class = new ReflectionClass($className);
if (
$class->implementsInterface(Func::class) ||
$class->implementsInterface(FuncVariablesAware::class)
) {
return $this->injectableFactory->create($className);
}

View File

@@ -29,10 +29,7 @@
namespace Espo\Core\Formula\Functions;
use Espo\Core\Formula\{
Functions\BaseFunction,
ArgumentList,
};
use Espo\Core\Formula\ArgumentList;
class AssignType extends BaseFunction
{

View File

@@ -0,0 +1,73 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\EnvGroup;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\BadArgumentValue;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Entities\User;
class UserAttributeSafeType implements Func
{
/** @var string[] */
private array $allowedAttributeList = [
'id',
'userName',
'name',
'teamsIds',
'defaultTeamId',
'type',
];
public function __construct(
private User $user
) {}
public function process(EvaluatedArgumentList $arguments): mixed
{
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
$attribute = $arguments[0];
if (!is_string($attribute)) {
throw BadArgumentType::create(1, 'string');
}
if (!in_array($attribute, $this->allowedAttributeList)) {
throw BadArgumentValue::create(1, "No allowed attribute.");
}
return $this->user->get($attribute);
}
}

View File

@@ -29,28 +29,39 @@
namespace Espo\Core\Formula\Functions\EnvGroup;
use Espo\Core\Formula\{
Functions\BaseFunction,
ArgumentList,
};
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Entities\User;
use Espo\Core\Di;
class UserAttributeType extends BaseFunction implements
Di\UserAware
class UserAttributeType implements Func
{
use Di\UserSetter;
/** @var string[] */
private array $forbiddenAttributeList = [
'password',
'apiKey',
'secretKey',
];
public function process(ArgumentList $args)
public function __construct(
private User $user
) {}
public function process(EvaluatedArgumentList $arguments): mixed
{
if (count($args) < 1) {
$this->throwTooFewArguments();
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
$attribute = $this->evaluate($args[0]);
$attribute = $arguments[0];
if (!is_string($attribute)) {
$this->throwBadArgumentType(1, 'string');
throw BadArgumentType::create(1, 'string');
}
if (in_array($attribute, $this->forbiddenAttributeList)) {
return null;
}
return $this->user->get($attribute);

View File

@@ -0,0 +1,92 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\ExtGroup\AclGroup;
use Espo\Core\Acl\Table;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Utils\Acl\UserAclManagerProvider;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
/**
* @noinspection PhpUnused
*/
class CheckEntityType implements Func
{
public function __construct(
private UserAclManagerProvider $userAclManagerProvider,
private EntityManager $entityManager,
) {}
public function process(EvaluatedArgumentList $arguments): bool
{
if (count($arguments) < 3) {
throw TooFewArguments::create(3);
}
$userId = $arguments[0];
$entityType = $arguments[1];
$id = $arguments[2];
$action = $arguments[3] ?? Table::ACTION_READ;
if (!is_string($userId)) {
throw BadArgumentType::create(1, 'string');
}
if (!is_string($entityType)) {
throw BadArgumentType::create(2, 'string');
}
if (!is_string($id)) {
throw BadArgumentType::create(3, 'string');
}
if (!is_string($action)) {
throw BadArgumentType::create(4, 'string');
}
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
if (!$user) {
return false;
}
$entity = $this->entityManager->getEntityById($entityType, $id);
if (!$entity) {
return false;
}
return $this->userAclManagerProvider->get($user)->checkEntity($user, $entity, $action);
}
}

View File

@@ -0,0 +1,80 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\ExtGroup\AclGroup;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Utils\Acl\UserAclManagerProvider;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
/**
* @noinspection PhpUnused
*/
class CheckScopeType implements Func
{
public function __construct(
private UserAclManagerProvider $userAclManagerProvider,
private EntityManager $entityManager,
) {}
public function process(EvaluatedArgumentList $arguments): bool
{
if (count($arguments) < 2) {
throw TooFewArguments::create(2);
}
$userId = $arguments[0];
$scope = $arguments[1];
$action = $arguments[2] ?? null;
if (!is_string($userId)) {
throw BadArgumentType::create(1, 'string');
}
if (!is_string($scope)) {
throw BadArgumentType::create(2, 'string');
}
if ($action !== null && !is_string($action)) {
throw BadArgumentType::create(3, 'string');
}
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
if (!$user) {
return false;
}
return $this->userAclManagerProvider->get($user)->checkScope($user, $scope, $action);
}
}

View File

@@ -0,0 +1,96 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\ExtGroup\AclGroup;
use Espo\Core\Acl\Table;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\BadArgumentValue;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Utils\Acl\UserAclManagerProvider;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
/**
* @noinspection PhpUnused
*/
class GetLevelType implements Func
{
public function __construct(
private UserAclManagerProvider $userAclManagerProvider,
private EntityManager $entityManager,
) {}
public function process(EvaluatedArgumentList $arguments): string
{
if (count($arguments) < 3) {
throw TooFewArguments::create(3);
}
$userId = $arguments[0];
$scope = $arguments[1];
$action = $arguments[2];
if (!is_string($userId)) {
throw BadArgumentType::create(1, 'string');
}
if (!is_string($scope)) {
throw BadArgumentType::create(2, 'string');
}
if (!is_string($action)) {
throw BadArgumentType::create(3, 'string');
}
if (
!in_array($action, [
Table::ACTION_READ,
Table::ACTION_CREATE,
Table::ACTION_EDIT,
Table::ACTION_STREAM,
Table::ACTION_DELETE,
])
) {
throw BadArgumentValue::create(3);
}
/** @var Table::ACTION_* $action */
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
if (!$user) {
return Table::LEVEL_NO;
}
return $this->userAclManagerProvider->get($user)->getLevel($user, $scope, $action);
}
}

View File

@@ -0,0 +1,76 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\ExtGroup\AclGroup;
use Espo\Core\Acl\Table;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Utils\Acl\UserAclManagerProvider;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
/**
* @noinspection PhpUnused
*/
class GetPermissionLevelType implements Func
{
public function __construct(
private UserAclManagerProvider $userAclManagerProvider,
private EntityManager $entityManager,
) {}
public function process(EvaluatedArgumentList $arguments): string
{
if (count($arguments) < 2) {
throw TooFewArguments::create(2);
}
$userId = $arguments[0];
$permission = $arguments[1];
if (!is_string($userId)) {
throw BadArgumentType::create(1, 'string');
}
if (!is_string($permission)) {
throw BadArgumentType::create(2, 'string');
}
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
if (!$user) {
return Table::LEVEL_NO;
}
return $this->userAclManagerProvider->get($user)->getPermissionLevel($user, $permission);
}
}

Some files were not shown because too many files have changed in this diff Show More