Compare commits

...

568 Commits

Author SHA1 Message Date
Yuri Kuznetsov 66a6f70a30 v 2023-04-07 22:18:02 +03:00
Yuri Kuznetsov d7a629c5a3 int/float fix 2023-04-07 22:17:25 +03:00
Yuri Kuznetsov 506e96e333 float preventing same decimal mark and th separator 2023-04-07 17:38:06 +03:00
Yuri Kuznetsov 76a9177c46 v 2023-04-07 16:42:17 +03:00
Yuri Kuznetsov a8baac3f92 missing counterpart link warning 2023-04-07 16:40:57 +03:00
Yuri Kuznetsov 8d9d073c16 fix currency decimal places null 2023-04-07 10:10:36 +03:00
Yuri Kuznetsov 177ecdb70b relation issue msg 2023-04-06 20:40:48 +03:00
Yuri Kuznetsov 89ce80d5b2 fix empty type 2023-04-06 19:19:44 +03:00
Yuri Kuznetsov 7f07175bee v 2023-04-06 19:08:54 +03:00
Yuri Kuznetsov 8bca35934c error msg 2023-04-06 19:05:28 +03:00
Yuri Kuznetsov 65bef1df65 fix link-parent not-storable 2023-04-06 18:57:38 +03:00
Yuri Kuznetsov 922a2e835c 2fa portals 2023-04-06 15:31:28 +03:00
Yuri Kuznetsov 048c83def8 upgrade check 2023-04-05 13:06:42 +03:00
Yuri Kuznetsov cf4a8c6c11 ref 2023-04-04 16:35:37 +03:00
Yuri Kuznetsov 7259142117 ref 2023-04-04 15:12:36 +03:00
Yuri Kuznetsov 746398c5ca cs 2023-04-04 15:10:19 +03:00
Yuri Kuznetsov 6a61772e43 cs 2023-04-04 15:09:21 +03:00
Yuri Kuznetsov 2412fb5151 cs 2023-04-04 15:08:44 +03:00
Yuri Kuznetsov 5e5a229366 cleanup 2023-04-04 10:21:48 +03:00
Yuri Kuznetsov 5d27a7a828 linkForeignAccessCheckDisabled 2023-04-04 10:15:48 +03:00
Yuri Kuznetsov 82cc4a7f03 link check allow defaults 2023-04-04 10:00:57 +03:00
Yuri Kuznetsov d2033c53fc link-check fix 2023-04-04 09:30:08 +03:00
Yuri Kuznetsov 86043a5ce5 link check portal bypass 2023-04-03 21:46:22 +03:00
SuchAFuriousDeath 1fae4ac84b integration-order (#2686)
Co-authored-by: tompro <tomas.prochazka@apertia.cz>
2023-04-03 15:46:15 +03:00
Yuri Kuznetsov 6301ba491c formula set link multiple column 2023-04-03 13:43:50 +03:00
Yuri Kuznetsov defe9965d4 getColumnById 2023-04-03 13:20:34 +03:00
Yuri Kuznetsov 937ad48841 fix 2023-04-03 13:20:11 +03:00
Yuri Kuznetsov 781030df60 ref 2023-04-03 12:40:50 +03:00
Yuri Kuznetsov 9b009962a5 print fix 2023-04-03 09:45:46 +03:00
Rabii Brahimi 255b6637ff Update Delete to allow to remove recordDefs and selectDefs (#2684)
When a custom entity of type event is deleted - its recordDefs and selectDefs files are not removed. 
Hope this doesn't break anything and just add ability to remove the two files.
2023-04-03 09:35:27 +03:00
Yuri Kuznetsov 28d1c052d2 formula parser fixes 2023-03-31 22:42:44 +03:00
Yuri Kuznetsov 828d8da741 fix link check 2023-03-31 10:38:32 +03:00
Eymen Elkum 54688795e2 rtl vertical theme (#2678)
Co-authored-by: Eymen Elkum <eymen@eblasoft.com.tr>
2023-03-30 11:33:23 +03:00
Yuri Kuznetsov 6414a13c6f template assigned user required false 2023-03-30 10:20:59 +03:00
Yuri Kuznetsov c48f17449b v 2023-03-30 09:58:39 +03:00
Yuri Kuznetsov cbf65feb09 before-upgrade script 2023-03-30 09:46:53 +03:00
Yuri Kuznetsov f1a3021c1c fix email applier 2023-03-29 14:14:10 +03:00
Arkadiy Asuratov b70327874e ensure notes by portal users are always public (#2681) 2023-03-29 11:24:22 +03:00
Yuri Kuznetsov 9345ada934 attachment size bigint 2023-03-29 08:17:09 +03:00
Yuri Kuznetsov 92c66a3b9a group email folder mass remove disabled 2023-03-29 08:13:22 +03:00
Yuri Kuznetsov c78f84650c fix dbType 2023-03-29 08:05:59 +03:00
Eymen Elkum cf1692a8a4 border-radios fixes for rtl theme (#2676)
* border-radios fixes for rtl theme

* RTL: improve the fix of rounded borders

* RTL: use css variable for radios

---------

Co-authored-by: Eymen Elkum <eymen@eblasoft.com.tr>
2023-03-29 08:04:07 +03:00
Yuri Kuznetsov 94321b3500 read-only side/bottom panels fix 2023-03-29 07:53:35 +03:00
Yuri Kuznetsov c1a1e1094a Update SECURITY.md 2023-03-24 20:16:52 +02:00
Yuri Kuznetsov c2fefd6227 cs 2023-03-24 19:47:49 +02:00
Yuri Kuznetsov 99946c8da5 working time calendar icon 2023-03-24 11:46:31 +02:00
Yuri Kuznetsov 86c2478721 style fix 2023-03-23 14:54:02 +02:00
Yuri Kuznetsov 5331332103 duplicate fix 2023-03-23 14:33:28 +02:00
Yuri Kuznetsov 0e380e1c10 mass update empty fix 2023-03-23 14:18:03 +02:00
Yuri Kuznetsov c54dbdc169 cleanup 2023-03-23 14:18:03 +02:00
Yuri Kuznetsov f88261a40a Update README.md 2023-03-21 19:39:24 +02:00
Yuri Kuznetsov 7ec8d1f69c revert 2023-03-21 19:11:32 +02:00
Yuri Kuznetsov 77ce0a3b4e Update README.md 2023-03-21 18:01:13 +02:00
Yuri Kuznetsov 134bde3370 Update README.md 2023-03-21 18:00:42 +02:00
Yuri Kuznetsov 811c841420 Update README.md 2023-03-21 17:41:27 +02:00
Yuri Kuznetsov 48751813fb fix warning 2023-03-21 16:07:23 +02:00
Yuri Kuznetsov 79c182d173 kanban records per page 2023-03-21 15:42:40 +02:00
Yuri Kuznetsov 87a612a1bc change user interface settings layout 2023-03-21 15:41:52 +02:00
Yuri Kuznetsov 388da3db70 name duplicate checker 2023-03-21 14:34:32 +02:00
Yuri Kuznetsov 60fdeafd77 cs 2023-03-21 13:12:19 +02:00
Yuri Kuznetsov 2057534f19 robots.txt 2023-03-21 08:16:16 +02:00
Yuri Kuznetsov b85f60b855 ref cs 2023-03-20 13:36:50 +02:00
Yuri Kuznetsov b4d02130fb typo 2023-03-20 13:32:51 +02:00
Yuri Kuznetsov 9a9d924b19 typo 2023-03-20 13:32:10 +02:00
Yuri Kuznetsov 1fb06f60e0 Merge branch 'fix' 2023-03-20 09:46:12 +02:00
Yuri Kuznetsov 9d65855868 fix array-is-not-empty 2023-03-20 09:43:25 +02:00
Yuri Kuznetsov 8cf2e270a5 fix record-modal helper 2023-03-20 09:34:55 +02:00
Yuri Kuznetsov 07ebd43ac6 max log message length 10000 2023-03-20 09:34:55 +02:00
Yuri Kuznetsov b1447a2922 Update README.md 2023-03-19 10:23:51 +02:00
Yuri Kuznetsov 66b85bf4e9 Update CONTRIBUTING.md 2023-03-19 10:22:19 +02:00
Yuri Kuznetsov e9b58926b4 Update CONTRIBUTING.md 2023-03-19 10:22:13 +02:00
Yuri Kuznetsov 75724b28b2 Update CONTRIBUTING.md 2023-03-19 10:21:06 +02:00
Yuri Kuznetsov 79f0730257 Update CONTRIBUTING.md 2023-03-19 10:14:21 +02:00
Yuri Kuznetsov 13abed67f2 Update README.md 2023-03-19 09:55:13 +02:00
Yuri Kuznetsov 84b98c1cfe Update README.md 2023-03-18 12:19:39 +02:00
Yuri Kuznetsov bc557706ca Update README.md 2023-03-18 12:18:53 +02:00
SuchAFuriousDeath 2ef0f5f80e fixed duplicate key (#2667)
Co-authored-by: tompro <tomas.prochazka@apertia.cz>
2023-03-18 12:01:23 +02:00
Yuri Kuznetsov 33db2270d6 Update README.md 2023-03-18 10:28:02 +02:00
Yuri Kuznetsov 6810ee37ac Update README.md 2023-03-18 09:10:40 +02:00
Yuri Kuznetsov 9e25804f89 update moment timezone 2023-03-17 13:44:29 +02:00
Yuri Kuznetsov 42d30a3f8e date picker in modal 2023-03-17 11:03:26 +02:00
Yuri Kuznetsov 524f94cd54 fix xlsx 2023-03-17 10:23:44 +02:00
Yuri Kuznetsov 739230c4b9 fix currency factory 2023-03-17 10:12:51 +02:00
Yuri Kuznetsov caeadc2f28 css fix 2023-03-16 12:58:08 +02:00
Yuri Kuznetsov fd330f991c ref 2023-03-16 11:02:32 +02:00
David e09bf8fa31 fix remove row (#2663)
Co-authored-by: David Moškoř <david.moskor@apertia.cz>
2023-03-16 09:01:40 +02:00
Yuri Kuznetsov 2ea60e66ba not storable select foreign 2023-03-15 18:51:40 +02:00
Yuri Kuznetsov f184e34838 extensions sortable by name 2023-03-15 18:07:30 +02:00
Eymen Elkum d721e9b448 entityType instead of scope (#2661) 2023-03-15 12:45:41 +02:00
Yuri Kuznetsov e1241eddb7 css fix 2023-03-15 10:33:00 +02:00
Yuri Kuznetsov c693654e80 fix 2023-03-15 09:54:42 +02:00
Yuri Kuznetsov 9d0f3dadad fix multi-enum validation popover 2023-03-15 08:19:37 +02:00
Yuri Kuznetsov 2c4033f363 formula currency convert 2023-03-14 11:15:23 +02:00
Yuri Kuznetsov 3c62414c8d doc 2023-03-14 11:01:27 +02:00
Yuri Kuznetsov 9dddd0b92f dark theme table bold 700 2023-03-13 17:27:51 +02:00
Yuri Kuznetsov 2284a3e2e5 orm: map function 2023-03-13 13:43:51 +02:00
Yuri Kuznetsov 60d83b138a cs 2023-03-13 11:13:23 +02:00
Yuri Kuznetsov c94d41a79c types, rename 2023-03-13 11:08:53 +02:00
Yuri Kuznetsov fc78cd28a6 docs 2023-03-13 10:47:09 +02:00
Yuri Kuznetsov b0e01a1fcb select order no complex expressions 2023-03-13 10:43:58 +02:00
Yuri Kuznetsov 41222f8e9e docs 2023-03-13 10:19:57 +02:00
Yuri Kuznetsov 64baaa5253 wysiwyg iframe shortcuts support 2023-03-13 09:53:42 +02:00
Yuri Kuznetsov 35a0f14d28 id generator usage 2023-03-13 08:59:09 +02:00
Yuri Kuznetsov 9bb2197717 ref, id generator usage 2023-03-13 08:54:38 +02:00
Yuri Kuznetsov 1607240f5d readme change 2023-03-12 19:59:32 +02:00
Yuri Kuznetsov 8be1af0671 orm: all/any operators 2023-03-12 17:17:43 +02:00
Yuri Kuznetsov 58ac0800f9 ref 2023-03-12 16:38:22 +02:00
Yuri Kuznetsov 2acac3d0b0 type 2023-03-11 22:46:01 +02:00
Yuri Kuznetsov 5e44fc2d40 Merge branch 'fix' 2023-03-10 18:26:02 +02:00
Yuri Kuznetsov 2b08f83ac2 fix email plain text encoding issue 2023-03-10 18:25:44 +02:00
Yuri Kuznetsov cc1bfce3dd fix email plain text encoding issue 2023-03-10 18:24:08 +02:00
Yuri Kuznetsov 08647b3ed6 ref 2023-03-10 17:31:41 +02:00
Yuri Kuznetsov b97f4ee124 cleanup 2023-03-10 17:20:44 +02:00
Yuri Kuznetsov d4e73f500f ref 2023-03-10 17:20:32 +02:00
Yuri Kuznetsov 5e13e6cf99 ref 2023-03-10 14:32:40 +02:00
Yuri Kuznetsov 3fd06a89f1 fix 2023-03-10 14:12:25 +02:00
Yuri Kuznetsov 6678500d1b cleanup 2023-03-10 13:47:23 +02:00
Yuri Kuznetsov 1a6f236dc7 fix route cache file 2023-03-10 13:39:57 +02:00
Yuri Kuznetsov f57a95349a bind client manager 2023-03-10 13:10:10 +02:00
Yuri Kuznetsov b3496268e0 orm: where clause value as expression 2023-03-10 12:47:11 +02:00
Yuri Kuznetsov fa38ece181 ref 2023-03-10 12:33:19 +02:00
Yuri Kuznetsov 5866f02eca orm: row constructor 2023-03-10 12:22:11 +02:00
Yuri Kuznetsov 6edce56ca7 orm: comparison sub-query 2023-03-10 12:03:46 +02:00
Yuri Kuznetsov 3dc239acc5 ref 2023-03-10 11:30:59 +02:00
Yuri Kuznetsov 385c70845a test 2023-03-10 11:03:02 +02:00
Yuri Kuznetsov 4f65a46434 orm where clause subquery as instance 2023-03-10 10:41:04 +02:00
Yuri Kuznetsov a9c4689500 cs 2023-03-09 20:42:30 +02:00
Yuri Kuznetsov 75b544a995 cs ref 2023-03-09 19:53:56 +02:00
Yuri Kuznetsov 451d5e5659 cleanup 2023-03-09 19:12:58 +02:00
Yuri Kuznetsov 134f5862dd ref 2023-03-09 19:12:33 +02:00
Yuri Kuznetsov b59279ab16 cs 2023-03-09 15:50:17 +02:00
Yuri Kuznetsov 0621c8aefc email to task: subject in description 2023-03-09 15:37:18 +02:00
Yuri Kuznetsov 3229ba1043 foreign field helper 2023-03-09 13:35:16 +02:00
Yuri Kuznetsov 06868b8b57 Merge branch 'fix' 2023-03-09 12:05:40 +02:00
Yuri Kuznetsov fce1d49407 v 2023-03-09 11:59:30 +02:00
Yuri Kuznetsov 8c2cf02891 cs 2023-03-09 11:59:29 +02:00
Yuri Kuznetsov ccdafc67b5 ref 2023-03-09 11:59:29 +02:00
Yuri Kuznetsov b6a470c52e ref 2023-03-09 11:59:29 +02:00
Yuri Kuznetsov 76c63bede4 field view skip re-render in edit mode 2023-03-09 11:59:29 +02:00
dependabot[bot] 2bebe4b045 Bump phpseclib/phpseclib from 3.0.16 to 3.0.19 (#2655)
Bumps [phpseclib/phpseclib](https://github.com/phpseclib/phpseclib) from 3.0.16 to 3.0.19.
- [Release notes](https://github.com/phpseclib/phpseclib/releases)
- [Changelog](https://github.com/phpseclib/phpseclib/blob/master/CHANGELOG.md)
- [Commits](https://github.com/phpseclib/phpseclib/compare/3.0.16...3.0.19)

---
updated-dependencies:
- dependency-name: phpseclib/phpseclib
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-09 10:04:41 +02:00
Yuri Kuznetsov 9ee1b41b52 import phone number starts with + fix 2023-03-09 09:12:12 +02:00
Yuri Kuznetsov f18a3043eb open spout export sanitize 2023-03-09 09:12:12 +02:00
Yuri Kuznetsov 01d5f1a07f global search check name exists 2023-03-09 09:12:12 +02:00
Yuri Kuznetsov 06b31d537f note no globals search 2023-03-09 09:12:12 +02:00
David 989a04dddf fix inline edit close (#2653)
Co-authored-by: David Moškoř <david.moskor@apertia.cz>
2023-03-09 09:11:18 +02:00
Yuri Kuznetsov 055e968660 match exctract 2023-03-08 11:16:28 +02:00
Yuri Kuznetsov 585512773d formula record delete 2023-03-08 10:32:30 +02:00
Yuri Kuznetsov 71f4abeb97 formula test fix 2023-03-08 10:19:55 +02:00
Yuri Kuznetsov 4f973e057e only middle join test 2023-03-08 10:03:15 +02:00
Yuri Kuznetsov 416bf152f0 prevent thousand separator null 2023-03-08 09:30:28 +02:00
Yuri Kuznetsov 3f473617c0 cs 2023-03-08 09:30:28 +02:00
IgorA100 56766e2246 Localization of email templates (ru_RU ) (#2649)
* Create body.tpl

* Create subject.tpl

* Create subject.tpl

* Create body.tpl

* Update body.tpl

* Create subject.tpl

* Create body.tpl

* Create body.tpl

* Create subject.tpl

* Update body.tpl

* Update body.tpl

* Create body.tpl

* Create subject.tpl
2023-03-07 18:56:01 +02:00
Yuri Kuznetsov a92e44bd6c fix msg 2023-03-07 18:53:17 +02:00
Yuri Kuznetsov 369f3ba9a5 switch complex expr 2023-03-07 18:52:49 +02:00
Yuri Kuznetsov 2d62c902cb Merge branch 'fix' 2023-03-07 17:26:01 +02:00
Yuri Kuznetsov 5985b6d93f fix meeting/call set held 2023-03-07 17:09:36 +02:00
Yuri Kuznetsov 587bda5c73 cs 2023-03-07 17:09:36 +02:00
Yuri Kuznetsov 230a9aced0 ref 2023-03-07 17:09:36 +02:00
IgorA100 023b0ea892 Trying to localize Email templates (#2647)
* Create body.tpl

* Create subject.tpl

* Create subject.tpl

* Create body.tpl

* Update body.tpl
2023-03-07 17:09:16 +02:00
Yuri Kuznetsov 0afd5e1d73 inline-edit-close reset attributes before re-rendering 2023-03-07 14:12:30 +02:00
Yuri Kuznetsov f824d8eaf5 rebuild config metadata support default 2023-03-07 10:19:51 +02:00
Yuri Kuznetsov abab4e2061 fix entityTeam name 2023-03-07 09:58:52 +02:00
Eymen Elkum 36c6883743 Fix formatter enum translation (#2642)
* email template formatter consider enum translation

* htmlizer default value for enum translation & rename variable

---------

Co-authored-by: Eymen Elkum <eymen@eblasoft.com.tr>
2023-03-07 09:58:36 +02:00
Yuri Kuznetsov 9c44f79b4d Merge branch 'fix' 2023-03-07 09:35:10 +02:00
Yuri Kuznetsov 6afd616e42 change poeditor link 2023-03-06 15:22:14 +02:00
Yuri Kuznetsov 7aabbc5d28 fix query composer in array issue 2023-03-06 10:05:55 +02:00
Yuri Kuznetsov 9589b0b70a portal url not sortable 2023-03-06 09:59:48 +02:00
Yuri Kuznetsov 8822104227 contrinuting 2023-03-05 20:23:03 +02:00
Yuri Kuznetsov e34e6b5d9a test 2023-03-05 18:14:11 +02:00
Yuri Kuznetsov 6341492965 ref 2023-03-05 17:25:23 +02:00
Yuri Kuznetsov aeece9acda authlogdisabled 2023-03-05 17:09:16 +02:00
Yuri Kuznetsov f5823d04dd fix 2023-03-05 17:07:29 +02:00
Yuri Kuznetsov 28df6738e2 cs fix 2023-03-05 16:45:38 +02:00
Yuri Kuznetsov 0e371ebe04 discard applierClassNameMap 2023-03-05 16:37:39 +02:00
Yuri Kuznetsov 6fe4034237 readme fix 2023-03-05 14:15:21 +02:00
Yuri Kuznetsov 48c3ea5f13 ref 2023-03-04 19:43:13 +02:00
Anthony Andriano da5b1cf005 bug fix: tcpdf total page numbers for groups (#2632)
The total page number was not correct for groups. The page number for a non-group was used, which produced the wrong result.
2023-03-04 19:42:58 +02:00
Yuri Kuznetsov 2655f0d9c5 ref 2023-03-04 10:59:39 +02:00
Yuri Kuznetsov e11e3d6168 cleanup 2023-03-04 10:34:09 +02:00
Eymen Elkum 90806c0e82 pdf consider enum translation param (#2631) 2023-03-04 09:54:08 +02:00
Yuri Kuznetsov ca9f25636b ref 2023-03-03 17:33:51 +02:00
Yuri Kuznetsov abcc290a7c pg encoding 2023-03-03 15:09:00 +02:00
Yuri Kuznetsov 091b64dd3d ref 2023-03-03 14:41:11 +02:00
Yuri Kuznetsov 8fd8434d68 ref 2023-03-03 11:56:30 +02:00
Yuri Kuznetsov 9ef5c0bc1b fix test 2023-03-03 10:43:15 +02:00
Yuri Kuznetsov 8adfb80558 orm defs try get methods 2023-03-03 10:42:41 +02:00
Yuri Kuznetsov 5ccbf49773 fix link check attachments 2023-03-03 10:27:43 +02:00
Yuri Kuznetsov e2cde84447 ref 2023-03-03 10:19:32 +02:00
Yuri Kuznetsov 26e7f3dfd2 type ref 2023-03-03 10:07:04 +02:00
Yuri Kuznetsov 6a75b28f8c test fix 2023-03-03 09:56:46 +02:00
Yuri Kuznetsov e8b6db20a2 orm discard aggregation 2023-03-03 09:55:15 +02:00
Yuri Kuznetsov 9e37739197 Merge branch 'fix' 2023-03-02 15:19:03 +02:00
Yuri Kuznetsov aae918886f authApiUserLogDisabled 2023-03-02 14:38:39 +02:00
Yuri Kuznetsov cbf0a82c52 config param system 2023-03-02 14:24:31 +02:00
Yuri Kuznetsov fb7683e35b ref 2023-03-02 14:15:13 +02:00
Yuri Kuznetsov 105bb8a80f cs 2023-03-02 13:13:20 +02:00
Yuri Kuznetsov eab0596c33 orm executor ref 2023-03-02 13:11:28 +02:00
Yuri Kuznetsov 7e84278cef ref 2023-03-02 12:47:13 +02:00
Yuri Kuznetsov f2a27c024f ref 2023-03-02 12:32:53 +02:00
Yuri Kuznetsov 1b76097311 sql log 2023-03-02 10:21:16 +02:00
Yuri Kuznetsov ae23f58bf5 ref 2023-03-02 09:19:30 +02:00
Yuri Kuznetsov f3bf7d93c1 pg action user 2023-03-01 13:58:34 +02:00
Yuri Kuznetsov 656ff76a8d lock alias mysql 2023-03-01 13:04:50 +02:00
Yuri Kuznetsov c72bcc365a pg fix week 2023-03-01 10:58:49 +02:00
Yuri Kuznetsov f58687ad6b pg fix quarter fiscal 2023-03-01 10:24:46 +02:00
Yuri Kuznetsov aefe547ef8 has many bc 2023-03-01 09:55:28 +02:00
Yuri Kuznetsov 3619696b12 fix 2023-02-28 19:06:15 +02:00
Yuri Kuznetsov 40d0ad5d08 remove scheduled job service 2023-02-28 19:01:12 +02:00
Yuri Kuznetsov c9d3a3f967 install ref 2023-02-28 18:00:47 +02:00
Yuri Kuznetsov 0b1f7d1548 tester changes 2023-02-28 16:34:16 +02:00
Yuri Kuznetsov 59cee9a2ee pg integration tests action 2023-02-28 15:57:23 +02:00
Yuri Kuznetsov e7373ab817 mass action encode serialized data 2023-02-28 14:03:18 +02:00
Yuri Kuznetsov 9570a28066 exclude test 2023-02-28 13:37:19 +02:00
Yuri Kuznetsov b3f7242bae integratino test platform env 2023-02-28 12:44:18 +02:00
Yuri Kuznetsov 823d371e9d integration-pg test suite 2023-02-28 12:28:17 +02:00
Yuri Kuznetsov 2399d21829 PostgreSQL (#2599)
* dev

* postgres update set

* position in list

* discard charset

* binary => blob

* rollback transaction

* insert on conflict

* fix POSITION_IN_LIST

* TIMESTAMPDIFF

* functions

* functions

* set UTC time zone

* functions and operators

* function

* fulltext

* fix details provider

* full text config usage

* fix param

* full text index rebuild

* full text and round fix

* add uuid db type

* if function

* tests

* delete with joins order limit

* update

* alias max length
2023-02-28 12:12:21 +02:00
Yuri Kuznetsov 01d5091aa3 integration test fix 2023-02-28 12:00:07 +02:00
Yuri Kuznetsov ed867d1b95 array validation max item length rename 2023-02-28 10:37:06 +02:00
Yuri Kuznetsov 3392c843e6 fix case hook 2023-02-28 09:01:05 +02:00
Yuri Kuznetsov 1e29280a1d fix 2023-02-27 18:02:26 +02:00
Yuri Kuznetsov aeecd1f3a6 ref 2023-02-27 16:20:57 +02:00
Yuri Kuznetsov 5ecce30720 fix test 2023-02-27 16:11:32 +02:00
Yuri Kuznetsov c02f87d1c0 integration test quote identifier and ref 2023-02-27 16:07:48 +02:00
Yuri Kuznetsov 929dce6b2e integration tests do not use full-reset 2023-02-27 15:00:20 +02:00
Yuri Kuznetsov c82d35af27 integration test ref 2023-02-27 14:13:01 +02:00
Yuri Kuznetsov a0a0b22d2f bind file manager 2023-02-27 10:46:41 +02:00
Yuri Kuznetsov db3af5749c bind config 2023-02-27 10:20:41 +02:00
Yuri Kuznetsov f1e13f4b95 change inspections 2023-02-27 09:48:41 +02:00
Yuri Kuznetsov 9f43a0ff89 docs 2023-02-26 20:46:56 +02:00
Yuri Kuznetsov 17bc8a6137 ref 2023-02-24 16:22:23 +02:00
Yuri Kuznetsov 66b06b8baa ref 2023-02-24 15:59:45 +02:00
Yuri Kuznetsov 1d6745396d ref 2023-02-24 15:30:26 +02:00
Yuri Kuznetsov 4f53038578 ref 2023-02-24 15:25:59 +02:00
Yuri Kuznetsov 79f5a7a94b expression renamings 2023-02-24 15:17:11 +02:00
Yuri Kuznetsov 77a433445b fix label 2023-02-24 14:03:36 +02:00
Yuri Kuznetsov be54198265 phone email invalid on list view 2023-02-24 13:49:31 +02:00
Yuri Kuznetsov 7da892ba98 fix 2023-02-24 13:33:49 +02:00
Yuri Kuznetsov 7b2526430a email address phone number tools 2023-02-24 13:30:25 +02:00
Yuri Kuznetsov e416dac56f fix validation 2023-02-24 12:14:41 +02:00
Yuri Kuznetsov d11f0f4f1b delete relationships on import revert 2023-02-24 11:40:23 +02:00
Yuri Kuznetsov f72fbed6e1 relation defs getConditions 2023-02-24 11:18:15 +02:00
Yuri Kuznetsov 56fe4e2ef7 cs 2023-02-24 11:00:03 +02:00
Yuri Kuznetsov 684a995e17 fix doc type 2023-02-24 10:54:38 +02:00
Yuri Kuznetsov 2f03572df8 orm alias max length prop 2023-02-24 09:25:10 +02:00
Yuri Kuznetsov 5bb0222abd Merge branch 'fix' 2023-02-24 09:15:00 +02:00
Yuri Kuznetsov 29fd164023 ref cs 2023-02-23 16:17:02 +02:00
Yuri Kuznetsov c52fedcf07 ref 2023-02-23 16:11:11 +02:00
Yuri Kuznetsov 68b12ff848 ref 2023-02-23 14:32:21 +02:00
Yuri Kuznetsov a351af06a1 ref 2023-02-23 14:05:28 +02:00
Yuri Kuznetsov 72c502b492 cs 2023-02-23 13:45:47 +02:00
Yuri Kuznetsov 1047d243a2 ref 2023-02-23 13:44:23 +02:00
Yuri Kuznetsov 5ab129bd9b ref 2023-02-23 13:06:09 +02:00
Yuri Kuznetsov 6e5e940b30 cs 2023-02-23 12:54:51 +02:00
Yuri Kuznetsov fa577a4fa9 ref 2023-02-23 12:40:41 +02:00
Yuri Kuznetsov 64f2c59134 list with categories fallback css fix 2023-02-23 10:18:08 +02:00
Yuri Kuznetsov a82408a06f fix message 2023-02-23 09:38:56 +02:00
Yuri Kuznetsov b80d8830ba create admin user command 2023-02-22 18:01:49 +02:00
Yuri Kuznetsov edcdf3c8be fixes 2023-02-22 18:01:41 +02:00
Yuri Kuznetsov ae27f360ca command io readSecretLine 2023-02-22 17:40:41 +02:00
Yuri Kuznetsov 474759ab6e cs 2023-02-22 17:28:45 +02:00
Yuri Kuznetsov 02ea7cc041 logger ignore empty context 2023-02-22 17:09:02 +02:00
Yuri Kuznetsov 0a67950913 ref 2023-02-22 17:02:07 +02:00
Yuri Kuznetsov 4ef66b1601 fix link conflict 2023-02-22 14:34:50 +02:00
Yuri Kuznetsov 33220d607a phpdocs 2023-02-22 13:25:30 +02:00
Yuri Kuznetsov ac9f80312d container get by class 2023-02-22 12:37:30 +02:00
Yuri Kuznetsov 0db2ee0a8d ref cs 2023-02-22 12:13:47 +02:00
Yuri Kuznetsov 7d4ab54505 greatest/least expression 2023-02-22 11:51:10 +02:00
Yuri Kuznetsov a06bfde766 fix link quick search 2023-02-22 10:57:53 +02:00
Yuri Kuznetsov 46f333fced entityDefs modifier 2023-02-22 10:51:20 +02:00
Yuri Kuznetsov f9294c652d settings skip rebuild 2023-02-21 19:05:38 +02:00
Yuri Kuznetsov a22dc4b2fb languageAclDisabled moved to scopes 2023-02-21 19:01:05 +02:00
Yuri Kuznetsov b293086482 cleanup 2023-02-21 18:57:43 +02:00
Yuri Kuznetsov 93139cb3ef preferences id fix 2023-02-21 18:56:22 +02:00
Yuri Kuznetsov 1db70eeaa8 entity acl changes 2023-02-21 18:56:07 +02:00
Yuri Kuznetsov 8ccda3fd2d currency db type string 2023-02-21 17:18:24 +02:00
Yuri Kuznetsov 60eb9008f7 mail merge endpoint 2023-02-21 14:14:14 +02:00
Yuri Kuznetsov 635fa8b893 activities endpoints 2023-02-21 14:00:09 +02:00
Yuri Kuznetsov 183603e09a forbid layout link name 2023-02-21 13:14:55 +02:00
Yuri Kuznetsov 3399b2cc01 email template api endpoint 2023-02-21 13:12:45 +02:00
Yuri Kuznetsov 9d4d441ec2 import api entrypoints 2023-02-21 12:56:36 +02:00
Yuri Kuznetsov 51bda2f5d2 cleanup 2023-02-21 11:32:45 +02:00
Yuri Kuznetsov 92e62c7760 orm greatest least functions 2023-02-21 11:07:58 +02:00
Yuri Kuznetsov 7bb204a432 authAnotherUserDisabled admin readOnly 2023-02-21 08:38:03 +02:00
Yuri Kuznetsov 39939191e5 fix portal starter 2023-02-20 18:17:12 +02:00
Yuri Kuznetsov aa43f29615 add line 2023-02-20 18:12:09 +02:00
Yuri Kuznetsov f109131d38 route cache check use cache 2023-02-20 18:11:15 +02:00
Yuri Kuznetsov a6fb0bbded slim route cache 2023-02-20 17:45:07 +02:00
Yuri Kuznetsov 9b808908b6 user api entry points routes 2023-02-20 17:19:10 +02:00
Eymen Elkum 0691a3cdda trTag helper put attributes part (#2615) 2023-02-20 14:31:50 +02:00
Yuri Kuznetsov 7208a4f88c cs fix 2023-02-20 14:27:31 +02:00
Yuri Kuznetsov 7d37006450 email address api action, global search endpoint fix 2023-02-20 14:22:53 +02:00
Yuri Kuznetsov 73f1e425ca fix tests 2023-02-20 13:44:36 +02:00
Yuri Kuznetsov 2336ae74da entity manager tool conflict with routes check 2023-02-20 12:56:10 +02:00
Yuri Kuznetsov 4419d0827a global search api endpoint change 2023-02-20 12:33:31 +02:00
Yuri Kuznetsov 0d1bc90848 api endpoints 2023-02-20 12:01:46 +02:00
Yuri Kuznetsov 7665010bff ref 2023-02-20 09:30:31 +02:00
Yuri Kuznetsov 5ba16b4c27 record id type 2023-02-19 16:47:09 +02:00
Yuri Kuznetsov cbf6b0cc6b ref 2023-02-19 13:30:28 +02:00
Yuri Kuznetsov c51a51e110 api apply response headers added by auth 2023-02-19 13:30:21 +02:00
Yuri Kuznetsov b59ff42ac2 ref 2023-02-19 12:58:37 +02:00
Yuri Kuznetsov b635018949 ref 2023-02-19 12:26:21 +02:00
Yuri Kuznetsov fcb2a0b7a5 ref 2023-02-19 11:49:22 +02:00
Yuri Kuznetsov 2048f3fa10 ref 2023-02-19 08:46:15 +02:00
Yuri Kuznetsov 0fe7fb032f phpdoc fix 2023-02-19 08:44:06 +02:00
Yuri Kuznetsov bb24b576e0 cs 2023-02-19 08:42:53 +02:00
Yuri Kuznetsov 2a7a12f58c cs 2023-02-18 22:49:43 +02:00
Yuri Kuznetsov 5ccb081af5 cs and fix class hint 2023-02-18 22:29:34 +02:00
Yuri Kuznetsov ddcb5ccbf9 ref 2023-02-18 14:54:24 +02:00
Yuri Kuznetsov 5b8dc4e629 fix doc 2023-02-18 12:06:47 +02:00
Yuri Kuznetsov 1032b73b85 api response preparing 2023-02-18 11:42:04 +02:00
Yuri Kuznetsov e7b914ff4b ref 2023-02-18 11:37:41 +02:00
Yuri Kuznetsov 30e94f25e6 action middlewares 2023-02-18 09:22:30 +02:00
Yuri Kuznetsov 7fa7fe63f8 api action return response 2023-02-17 21:42:32 +02:00
Yuri Kuznetsov 716a5b86ff rensponse methods 2023-02-17 21:21:27 +02:00
Yuri Kuznetsov a29ce1a873 cleanup 2023-02-17 21:06:16 +02:00
Yuri Kuznetsov ace2dc802a api action 2023-02-17 20:34:29 +02:00
Yuri Kuznetsov 1018bfd4d4 cs 2023-02-17 18:16:57 +02:00
Yuri Kuznetsov cc828661da cs 2023-02-17 17:44:29 +02:00
Yuri Kuznetsov 4c265dbae1 cs 2023-02-17 17:32:17 +02:00
Yuri Kuznetsov 2615869691 cs 2023-02-17 17:13:42 +02:00
Yuri Kuznetsov fbbb7c99c0 cs 2023-02-17 16:38:13 +02:00
Yuri Kuznetsov da7fc9d6a0 cs 2023-02-17 16:28:31 +02:00
Yuri Kuznetsov 9b63470e9a email from autocomplete fix 2023-02-17 16:12:00 +02:00
Yuri Kuznetsov 5879a57cd1 cleanup 2023-02-17 15:47:41 +02:00
Yuri Kuznetsov 702b3f4e2b cs ref 2023-02-17 15:46:44 +02:00
Yuri Kuznetsov 75749efacb formula func interface 2023-02-17 14:01:06 +02:00
Yuri Kuznetsov 854f6c6390 fix 2023-02-17 13:00:22 +02:00
Yuri Kuznetsov 717c21e91b cs 2023-02-17 11:50:53 +02:00
Yuri Kuznetsov 07f1100ccc fix install alert css 2023-02-17 11:40:20 +02:00
Yuri Kuznetsov 3157bb4fcf set read only config params 2023-02-17 10:43:18 +02:00
Yuri Kuznetsov a3a35be818 cleanup 2023-02-17 10:41:32 +02:00
Yuri Kuznetsov 1c1042cd75 stream note unrelate fix 2023-02-17 10:31:41 +02:00
Yuri Kuznetsov 8f8eb4807d cs fix 2023-02-17 10:18:30 +02:00
Yuri Kuznetsov f97bb82d9e unrelate stream note 2023-02-17 10:05:31 +02:00
Yuri Kuznetsov 62b325c24b cs 2023-02-17 09:38:29 +02:00
Yuri Kuznetsov 4051b83b30 docs 2023-02-17 09:08:34 +02:00
Yuri Kuznetsov 75444b3e0b uuid db type 2023-02-17 08:55:32 +02:00
Yuri Kuznetsov 961a7bd0bf ref 2023-02-16 17:17:02 +02:00
Eymen Elkum f4b09c0135 fix foreign enum & array with optionPath & translations (#2610)
* fix foreign enum & array with optionPath & translations

* foreign enum & array clone options

---------

Co-authored-by: Eymen Elkum <eymen@eblasoft.com.tr>
2023-02-16 16:49:19 +02:00
Yuri Kuznetsov d09f83e267 system user id in config 2023-02-16 14:23:01 +02:00
Yuri Kuznetsov b874cc283f read only config param 2023-02-16 14:19:15 +02:00
Yuri Kuznetsov b5873c9c1d orm md5 function 2023-02-16 12:51:55 +02:00
Yuri Kuznetsov 89bde00e3b cs 2023-02-16 12:11:57 +02:00
Yuri Kuznetsov 242de1824f exception fix 2023-02-16 12:10:20 +02:00
Yuri Kuznetsov 02d277d045 drop mariadb 10.1 2023-02-16 10:30:07 +02:00
Yuri Kuznetsov 46bd520b52 cs 2023-02-15 17:09:36 +02:00
Yuri Kuznetsov d9c0f7d055 uuid generator 2023-02-15 17:03:14 +02:00
Yuri Kuznetsov 4a9bd1b54c fix getSet 2023-02-15 16:05:30 +02:00
Yuri Kuznetsov 1626e4b6bf update getSet 2023-02-15 15:27:17 +02:00
Yuri Kuznetsov c1cfa0483a orm update in not null 2023-02-15 15:15:13 +02:00
Yuri Kuznetsov c7d5bc8169 fix orm get join 2023-02-15 14:40:15 +02:00
Yuri Kuznetsov a7c33afd93 cs 2023-02-14 20:37:54 +02:00
Yuri Kuznetsov f7678abad1 fix 2023-02-14 18:34:54 +02:00
Yuri Kuznetsov b181624064 ref 2023-02-14 18:34:21 +02:00
Yuri Kuznetsov 8a6c63e12c modified by id fix 2023-02-14 13:28:00 +02:00
Yuri Kuznetsov aba1881a5c fix docs and cleanup 2023-02-14 13:00:52 +02:00
Yuri Kuznetsov 3350ffc0b7 system user id ref 2023-02-14 12:48:42 +02:00
Yuri Kuznetsov c498d463e7 merge fix 2023-02-14 10:58:50 +02:00
Yuri Kuznetsov 54bf67e8bd cleanup 2023-02-13 18:16:35 +02:00
Yuri Kuznetsov 59e852a285 install ref 2023-02-13 18:11:13 +02:00
Yuri Kuznetsov dcbe24746e deprecate SYSTEM_USER_ID const 2023-02-13 17:10:34 +02:00
Yuri Kuznetsov deb3ee0653 system user id ref 2023-02-13 17:00:06 +02:00
Yuri Kuznetsov 698db9ebd6 ref 2023-02-13 14:52:00 +02:00
Rabii Brahimi 214cf538ad Update select-records.js (#2603)
Add ability to show pagination one select records modal (if pagination enabled).
2023-02-13 14:16:46 +02:00
Yuri Kuznetsov f578fd4af9 external account, integration id db type string 2023-02-13 13:46:49 +02:00
Yuri Kuznetsov 57c011ee94 fix test 2023-02-13 13:43:07 +02:00
Yuri Kuznetsov e4285d1d2a ref 2023-02-13 13:28:06 +02:00
Yuri Kuznetsov 5f4643d725 docs 2023-02-13 12:03:02 +02:00
Yuri Kuznetsov fcc13ef10c skip text search for email address if whitespaces 2023-02-13 10:14:43 +02:00
Yuri Kuznetsov 826c1734a2 fix test 2023-02-13 10:01:07 +02:00
Yuri Kuznetsov 849ddadb8c cs fix 2023-02-13 09:59:21 +02:00
Yuri Kuznetsov 3e4bd12a76 fulltext boolean expression 2023-02-13 09:48:47 +02:00
Yuri Kuznetsov e6aae5f4ed schema manager creation change 2023-02-12 19:00:19 +02:00
Yuri Kuznetsov d8bd2f451f orm refactor match expr 2023-02-12 15:13:43 +02:00
Yuri Kuznetsov 9c47341fc1 test fix 2023-02-12 11:40:52 +02:00
Yuri Kuznetsov bbea0c0215 filters improvements 2023-02-12 11:34:55 +02:00
Yuri Kuznetsov 4e4e29e0f1 fix 2023-02-11 23:23:18 +02:00
Yuri Kuznetsov 4658eb800e join only middle, filters change 2023-02-11 23:18:06 +02:00
Yuri Kuznetsov 16a313f659 orm exists operator 2023-02-11 20:04:54 +02:00
Yuri Kuznetsov c2fcd8d86c ref 2023-02-11 16:11:04 +02:00
Yuri Kuznetsov 8666a3977a orm operator key fix 2023-02-11 12:05:01 +02:00
Yuri Kuznetsov fa06a437e5 fix test 2023-02-10 14:40:51 +02:00
Yuri Kuznetsov 0dcea2ad5b fix additionalSelect alias not string 2023-02-10 14:39:51 +02:00
Yuri Kuznetsov 5a3142b252 fix foreign select join value bool 2023-02-10 14:32:57 +02:00
Yuri Kuznetsov 4598b58fbb schema diff modifier handle dropped sequences 2023-02-10 13:23:53 +02:00
Yuri Kuznetsov 7ff9d85a11 id length 17 2023-02-09 18:38:00 +02:00
Yuri Kuznetsov 89b912ee73 fix some foreign id fields and max-length 2023-02-09 16:24:02 +02:00
Yuri Kuznetsov a4f75f1423 person name name not storable 2023-02-09 13:13:52 +02:00
Yuri Kuznetsov 03a69cb364 ORM use COALESCE 2023-02-09 13:01:01 +02:00
Yuri Kuznetsov e7fa98dc09 lock table ref 2023-02-09 12:55:47 +02:00
Yuri Kuznetsov 5eb49d6c3d fix test 2023-02-09 12:51:28 +02:00
Yuri Kuznetsov 1d84aad483 fix bool 0 where clause 2023-02-09 12:47:25 +02:00
Yuri Kuznetsov af847a8fe7 fix test 2023-02-08 13:40:36 +02:00
dependabot[bot] 08a6a2c66b Bump dompdf/dompdf from 2.0.2 to 2.0.3 (#2597)
Bumps [dompdf/dompdf](https://github.com/dompdf/dompdf) from 2.0.2 to 2.0.3.
- [Release notes](https://github.com/dompdf/dompdf/releases)
- [Commits](https://github.com/dompdf/dompdf/compare/v2.0.2...v2.0.3)

---
updated-dependencies:
- dependency-name: dompdf/dompdf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-07 20:20:42 +02:00
Yuri Kuznetsov 0a4a3938fb foreign id usage 2023-02-07 18:39:07 +02:00
Yuri Kuznetsov 0bc05b4a6d subscription bigint 2023-02-07 17:37:42 +02:00
Yuri Kuznetsov b7ad2dd760 docs and ref 2023-02-07 16:17:04 +02:00
Yuri Kuznetsov 363842aff2 integration entity id max length 2023-02-07 15:29:53 +02:00
Yuri Kuznetsov ac889dea91 currency column length 3 2023-02-07 12:19:04 +02:00
Yuri Kuznetsov 89e31795cc mass update link test 2023-02-07 11:41:04 +02:00
Yuri Kuznetsov 9953ea8419 link check integration test 2023-02-07 11:20:54 +02:00
Yuri Kuznetsov 1ae1f07de2 merge fix 2023-02-07 10:23:21 +02:00
Yuri Kuznetsov 6fcd0e155d record link check 2023-02-06 22:58:02 +02:00
Yuri Kuznetsov 3e945b7fd8 ref 2023-02-06 21:32:43 +02:00
Yuri Kuznetsov 99464c5210 comment 2023-02-06 16:54:41 +02:00
Yuri Kuznetsov 64c9fbf4f8 fix tests 2023-02-06 16:15:00 +02:00
Yuri Kuznetsov 029d09e689 access link check 2023-02-06 15:24:25 +02:00
Yuri Kuznetsov be27fc45ee link multiple access check 2023-02-06 10:44:04 +02:00
Yuri Kuznetsov 0eefc5d75d ref 2023-02-06 09:50:25 +02:00
Yuri Kuznetsov 876817ffe9 acl link checker 2023-02-06 09:43:59 +02:00
Yuri Kuznetsov f6b3e33e7e ref 2023-02-05 19:46:01 +02:00
Yuri Kuznetsov 24301b22a9 note select optimization 2023-02-05 18:23:59 +02:00
Yuri Kuznetsov 31be2f81ff fix tests 2023-02-05 17:45:25 +02:00
Yuri Kuznetsov bc435b0729 select optimizations 2023-02-05 17:38:08 +02:00
Yuri Kuznetsov 4cd0961f80 assigned users avatar 2023-02-04 17:29:00 +02:00
Yuri Kuznetsov 70edfbb88b ref 2023-02-04 16:38:01 +02:00
Yuri Kuznetsov 9066a2bf97 rel table id params 2023-02-04 16:19:10 +02:00
Yuri Kuznetsov b39bffa1f2 record id dbType 2023-02-04 15:51:30 +02:00
Yuri Kuznetsov c49db089f5 docs 2023-02-04 14:57:19 +02:00
Yuri Kuznetsov 2a110851df column fixed param 2023-02-04 14:56:26 +02:00
Yuri Kuznetsov 3e60103516 fix tests 2023-02-04 14:37:27 +02:00
Yuri Kuznetsov 481b870565 rename 2023-02-04 11:44:01 +02:00
Yuri Kuznetsov 9564d0807a rename 2023-02-04 11:37:00 +02:00
Yuri Kuznetsov 785746c801 ref 2023-02-04 11:34:30 +02:00
Yuri Kuznetsov a89ac23625 rebuild database hard 2023-02-04 10:38:45 +02:00
Yuri Kuznetsov adc2cb5a66 field manager reset rebuild 2023-02-04 10:08:19 +02:00
Yuri Kuznetsov 4b5787c0d0 metadata record id length 2023-02-04 09:36:11 +02:00
Yuri Kuznetsov dcbd2bfa42 fix metadata 2023-02-03 18:31:35 +02:00
Yuri Kuznetsov 147fcb02b6 rename metadata app database 2023-02-03 18:25:07 +02:00
Yuri Kuznetsov 1762096532 orm converter refactoring 2023-02-03 16:00:55 +02:00
Yuri Kuznetsov d21857075e fix typo 2023-02-02 16:16:36 +02:00
Yuri Kuznetsov a81759b0f1 dbal ref 2023-02-02 16:12:14 +02:00
Yuri Kuznetsov 99fb897b63 cleanup 2023-02-02 16:12:13 +02:00
dependabot[bot] f6e7da57f7 Bump dompdf/dompdf from 2.0.1 to 2.0.2 (#2587)
Bumps [dompdf/dompdf](https://github.com/dompdf/dompdf) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/dompdf/dompdf/releases)
- [Commits](https://github.com/dompdf/dompdf/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: dompdf/dompdf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-01 08:39:35 +02:00
David aa001b4a7f mime type already contains "image/" (#2585)
Co-authored-by: David Moškoř <david.moskor@apertia.cz>
2023-01-30 21:37:04 +02:00
Yuri Kuznetsov 37c3cfc539 cs 2023-01-30 20:39:35 +02:00
Yuri Kuznetsov da45d958b7 orm field converter 2023-01-30 14:06:45 +02:00
Yuri Kuznetsov aaf095e32b bool fix 0/1 usagge 2023-01-29 10:04:35 +02:00
Yuri Kuznetsov 282f701b77 fix schema manager proxy 2023-01-28 21:13:37 +02:00
Yuri Kuznetsov ea8ba18d2e ref 2023-01-28 20:47:11 +02:00
Yuri Kuznetsov d08e969915 dbal types refactor 2023-01-28 20:25:27 +02:00
Yuri Kuznetsov 5567cc6938 ref 2023-01-28 14:00:03 +02:00
Yuri Kuznetsov 720188822d currency service refactor 2023-01-28 13:46:56 +02:00
Yuri Kuznetsov 48e0e53ab1 ref 2023-01-28 12:49:19 +02:00
Yuri Kuznetsov 03c123d63e grand refactoring of schema and other database related things 2023-01-28 12:45:44 +02:00
Yuri Kuznetsov 7d4ded2480 Merge branch 'fix' 2023-01-28 12:43:11 +02:00
Yuri Kuznetsov 1ce3825338 ref 2023-01-24 12:11:40 +02:00
Yuri Kuznetsov 1fba05dd04 ref 2023-01-24 11:59:55 +02:00
Yuri Kuznetsov 2c9b16a2d5 ref 2023-01-24 11:54:34 +02:00
Yuri Kuznetsov 314b5bcd87 refactor 2023-01-24 11:18:05 +02:00
Yuri Kuznetsov 433af312cf converter ref 2023-01-23 22:41:48 +02:00
Yuri Kuznetsov 30b18cf945 index defs get flags 2023-01-23 21:43:38 +02:00
Yuri Kuznetsov 81b9991e6d fix login as user logout error 2023-01-23 16:07:56 +02:00
Yuri Kuznetsov 84f3830eca login style fix 2023-01-23 15:57:00 +02:00
Yuri Kuznetsov cb878c70b5 fix 2023-01-23 15:36:54 +02:00
Yuri Kuznetsov 8224eec990 logout wait 2023-01-23 15:31:38 +02:00
Yuri Kuznetsov d655ee92a2 discard user repo 2023-01-23 15:02:12 +02:00
Yuri Kuznetsov a8d868e812 oidc portal fix 2023-01-22 20:26:03 +02:00
Yuri Kuznetsov e5c400214a oidc backchannel logout portal 2023-01-22 20:09:30 +02:00
Yuri Kuznetsov 5be5275eda ref 2023-01-22 20:08:00 +02:00
Yuri Kuznetsov e61535ee92 cleanup 2023-01-22 19:59:48 +02:00
Yuri Kuznetsov e0abe23260 oidc ref 2023-01-22 19:58:31 +02:00
Yuri Kuznetsov 8d65a3256b query composer indexHints prop 2023-01-22 17:56:59 +02:00
Yuri Kuznetsov d82c297b79 query composer order LIST to use POSITION_IN_LIST 2023-01-22 16:12:50 +02:00
Yuri Kuznetsov f7ed15c507 query composer use quote for true false 2023-01-22 15:50:42 +02:00
Yuri Kuznetsov 37b583d431 query composer add quote column 2023-01-22 15:44:49 +02:00
Yuri Kuznetsov c3286c7c4a cleanup 2023-01-22 14:18:26 +02:00
Yuri Kuznetsov 740c751d1b add methods to entity manager proxy 2023-01-22 14:15:13 +02:00
Yuri Kuznetsov 7e66f14e16 password change use method provider 2023-01-22 14:08:04 +02:00
Yuri Kuznetsov c3d0559260 ref 2023-01-22 13:53:00 +02:00
Yuri Kuznetsov 27762e61a6 ref 2023-01-22 13:51:40 +02:00
Yuri Kuznetsov cdbeab8e47 app service ref and recover password check change 2023-01-22 13:42:11 +02:00
Yuri Kuznetsov 15f337a319 oidc portal 2023-01-22 12:55:39 +02:00
Yuri Kuznetsov dd0deeb967 Merge branch 'fix' 2023-01-22 12:50:37 +02:00
Yuri Kuznetsov 7d36e685b8 exportCOllection support params 2023-01-18 11:24:38 +02:00
Yuri Kuznetsov 48fa62105f change default font 2023-01-16 17:23:19 +02:00
Yuri Kuznetsov 7e354d560f cleanup 2023-01-16 17:19:39 +02:00
Yuri Kuznetsov bd1fdaf9d9 change template defaults 2023-01-16 17:18:45 +02:00
Yuri Kuznetsov ab87cff5bc fix 2023-01-15 17:01:02 +02:00
Yuri Kuznetsov a5280ec0ac cleanup 2023-01-15 16:59:00 +02:00
Yuri Kuznetsov 6339690e58 dompdf engine 2023-01-15 16:57:45 +02:00
Yuri Kuznetsov c5ae2800b5 html-container overflow hidden 2023-01-15 16:25:49 +02:00
Yuri Kuznetsov 62501dc20d Merge branch 'fix' 2023-01-15 16:18:00 +02:00
Yuri Kuznetsov c7b7848e69 gruntfile chang 2023-01-14 10:07:03 +02:00
Yuri Kuznetsov a3be63c6f1 fix test 2023-01-12 13:03:16 +02:00
Yuri Kuznetsov 52fe2ca1c9 labels 2023-01-12 13:03:16 +02:00
Yuri Kuznetsov 6513513108 fix hide/show remove 2023-01-12 13:03:16 +02:00
Yuri Kuznetsov 442d2c030b fix grouped notifications 2023-01-12 13:03:16 +02:00
Yuri Kuznetsov 391500a1c1 cleanup 2023-01-12 13:03:16 +02:00
Yuri Kuznetsov f239d2c478 notifications fix 2023-01-12 13:03:16 +02:00
dependabot[bot] 101f8a29a3 Bump json5 from 2.2.0 to 2.2.3 (#2554)
Bumps [json5](https://github.com/json5/json5) from 2.2.0 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.0...v2.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-12 11:27:17 +02:00
Yuri Kuznetsov dfb846fb7e lower exceptions log levels 2023-01-11 13:37:21 +02:00
Yuri Kuznetsov ecd6d5a558 Merge branch 'fix' 2023-01-11 13:22:06 +02:00
Yuri Kuznetsov 048d54a7af revert constant usage 2023-01-09 13:07:12 +02:00
Yuri Kuznetsov 5645066d68 Merge branch 'version/7.4' 2023-01-09 12:56:18 +02:00
Yuri Kuznetsov 923ecc883a formula remove while 2023-01-06 12:07:57 +02:00
Yuri Kuznetsov 59a95fa3bc email list view show attachments 2023-01-06 12:03:29 +02:00
Yuri Kuznetsov 4789c3c2ad attachments modal 2023-01-06 11:54:00 +02:00
Yuri Kuznetsov c149aa8560 import validation explanations 2023-01-06 11:01:34 +02:00
Yuri Kuznetsov e9f5d99e9b markdown to string 2023-01-06 10:39:45 +02:00
Yuri Kuznetsov 4309dfb57a massRemoveDisabled param 2023-01-05 11:44:08 +02:00
Yuri Kuznetsov 4ad0a22a6a fix 2023-01-05 11:29:15 +02:00
Yuri Kuznetsov 2daff0eabb list view ref 2023-01-05 11:23:50 +02:00
Yuri Kuznetsov cde59bce55 action handler support not in data 2023-01-05 10:08:12 +02:00
Yuri Kuznetsov 80b9d0dc7b ref 2023-01-05 09:43:28 +02:00
Yuri Kuznetsov bfdaa4721d mass action list fixes 2023-01-04 13:55:34 +02:00
David 60db5cdc6c global mass actions defs fix (#2549)
Co-authored-by: David Moškoř <david.moskor@apertia.cz>
2023-01-04 13:37:20 +02:00
Yuri Kuznetsov 1a5a9060a6 fix 2023-01-04 12:01:49 +02:00
Yuri Kuznetsov ddf9dfba47 decimal mark thousand separator options 2023-01-04 11:56:50 +02:00
Yuri Kuznetsov f93060a5ca autonumeric 2023-01-04 11:38:30 +02:00
Yuri Kuznetsov 7e5b491313 formula: break & continue 2023-01-03 18:44:09 +02:00
Yuri Kuznetsov b083a11099 use true/false 2023-01-03 17:59:17 +02:00
Taras Machyshyn 63f67e0f66 2023 year 2023-01-03 16:26:52 +02:00
Taras Machyshyn 141c6430c4 Merge master 2023-01-03 16:22:44 +02:00
Yuri Kuznetsov b872e6960a fix db helper 2023-01-03 15:37:57 +02:00
Yuri Kuznetsov 8ba47b11ec orm quote column 2023-01-03 15:29:56 +02:00
Yuri Kuznetsov 883351eaf3 ref 2023-01-03 10:17:51 +02:00
Yuri Kuznetsov 7976e6d5b5 cs ref 2023-01-03 09:56:04 +02:00
Yuri Kuznetsov 8fcda8c621 ref 2023-01-03 09:43:50 +02:00
Yuri Kuznetsov c1b2870c91 dbal connection factory 2023-01-03 09:18:20 +02:00
Yuri Kuznetsov 2b4f6239f7 revert 2023-01-02 17:31:06 +02:00
Yuri Kuznetsov bae0144393 db helper ref 2023-01-02 17:24:08 +02:00
Yuri Kuznetsov af21cc4400 Merge branch 'master' into version/7.4 2023-01-02 14:51:42 +02:00
Andrew Fontana f50d7717a9 isSubscribedToWebSocket Typo (#2550)
* Fixed WebSocked typo to WebSocket

* Fixed WebSocked typo to WebSocket
2023-01-02 13:42:36 +02:00
Yuri Kuznetsov 03a79ddefe fix 2023-01-01 14:03:50 +02:00
Yuri Kuznetsov 353246281c currency decimal 2023-01-01 13:50:33 +02:00
Yuri Kuznetsov 747d8220e1 currency changes 2023-01-01 12:20:06 +02:00
Yuri Kuznetsov b6a1648486 cleanup 2022-12-31 11:03:44 +02:00
Yuri Kuznetsov e0e168945c cleanup 2022-12-31 10:42:20 +02:00
Yuri Kuznetsov 385d7af1b6 ref 2022-12-31 10:41:36 +02:00
Yuri Kuznetsov 14811a7c1a Merge branch 'master' into version/7.4 2022-12-31 10:10:08 +02:00
Yuri Kuznetsov c828242359 Merge branch 'master' into version/7.4 2022-12-29 13:04:00 +02:00
Yuri Kuznetsov c4610c4e24 jsdoc 2022-12-29 12:43:29 +02:00
Yuri Kuznetsov 583ba47e78 fix doc 2022-12-29 12:06:17 +02:00
Yuri Kuznetsov a9e8d5b2b2 auth error translatable exception message 2022-12-27 14:37:34 +02:00
Yuri Kuznetsov 3c4e33f0c5 fix tests 2022-12-27 14:07:27 +02:00
Yuri Kuznetsov a0d39be19c middlewares after auth 2022-12-27 13:58:04 +02:00
Yuri Kuznetsov 83ef7b32b5 hook interfaces 2022-12-26 14:18:51 +02:00
Yuri Kuznetsov f365a38858 bind pdo provider 2022-12-25 20:28:09 +02:00
Yuri Kuznetsov e6f081c7d1 injectable factory create resolved 2022-12-25 20:22:53 +02:00
Yuri Kuznetsov f463421a33 psr container 2022-12-25 16:50:49 +02:00
Yuri Kuznetsov 49bb6771f6 pdo factory and dbal ref 2022-12-25 14:07:06 +02:00
Yuri Kuznetsov 28d8fcd31e middleware api 2022-12-25 10:28:51 +02:00
Yuri Kuznetsov 008935f054 fix 2022-12-24 20:44:37 +02:00
Yuri Kuznetsov 7ed90d7bbf Merge branch 'master' into version/7.4 2022-12-24 12:13:18 +02:00
Yuri Kuznetsov 7fdcb41547 hook suppress 2022-12-24 11:39:32 +02:00
Yuri Kuznetsov 7eca082d9f Merge branch 'master' into version/7.4 2022-12-24 11:27:47 +02:00
Yuri Kuznetsov ab81d0fae6 fix typo 2022-12-21 18:29:00 +02:00
Yuri Kuznetsov 3f84551b50 entity defs fields validatorClassNameMap 2022-12-21 15:27:43 +02:00
Yuri Kuznetsov 0e56c727ff formula new statments and refactoring 2022-12-20 17:24:53 +02:00
Yuri Kuznetsov c911c0f6e5 Merge branch 'master' into version/7.4 2022-12-20 14:49:50 +02:00
Yuri Kuznetsov 9e424f16d7 Merge branch 'master' into version/7.4 2022-12-18 21:34:06 +02:00
Yuri Kuznetsov dcf1698dad ref 2022-12-15 19:09:22 +02:00
Yuri Kuznetsov 8b92df3f6a rename 2022-12-15 15:40:17 +02:00
Yuri Kuznetsov c343a4600f xlsx changes 2022-12-15 15:36:12 +02:00
Yuri Kuznetsov d196a574fd export with openspout 2022-12-15 15:04:08 +02:00
Yuri Kuznetsov 72b2a456b9 export filter field list 2022-12-15 11:47:21 +02:00
Yuri Kuznetsov d9d42fd664 export params 2022-12-15 11:37:13 +02:00
Yuri Kuznetsov a131308e1a export refactor 2022-12-14 12:49:15 +02:00
Yuri Kuznetsov aff948f0a6 ref 2022-12-14 09:45:28 +02:00
Yuri Kuznetsov 0657d78eb5 move 2022-12-13 15:48:11 +02:00
Yuri Kuznetsov 4807beca0b move 2022-12-13 15:32:07 +02:00
Yuri Kuznetsov 8a26478019 export ref 2022-12-13 15:26:48 +02:00
Yuri Kuznetsov 3a9851c89b refactoring 2022-12-13 14:19:31 +02:00
Yuri Kuznetsov 2824d8cd84 ref 2022-12-12 18:23:56 +02:00
Yuri Kuznetsov e041238d03 cleanup 2022-12-12 18:11:14 +02:00
Yuri Kuznetsov df36d33b98 fix 2022-12-12 18:04:38 +02:00
Yuri Kuznetsov f34ca2fe6b xlsx ref 2022-12-12 17:53:17 +02:00
1187 changed files with 41387 additions and 24411 deletions
+15 -7
View File
@@ -1,17 +1,25 @@
## Pull Requests
Before we can merge your pull request you need to accept our CLA [here](https://github.com/espocrm/cla). It's very simple to do.
Before we can merge your pull request, you need to accept our CLA [here](https://github.com/espocrm/cla).
See [Code Style Guidelines](https://github.com/espocrm/espocrm/wiki/Code-Style-Guidelines).
It's desirable that one PR solves one specific problem. Do not include code style changes to PRs
(unless the main purpose of the PR is a code style fix).
If you would like to contribute something that is not a small fix, it's reasonable to create an issue first
(a bug report or feature request).
Branches:
* *hotfix/** upcoming maintenance release; fixes should be pushed to this branch;
* *master* develop branch; new features should be pushed to this branch;
* *stable* last stable release.
* *master* the develop branch; new features should be pushed to here;
* *fix* the upcoming maintenance release; small fixes should be pushed to here.
## Issues
When reporting a possible bug please provide detail steps so that we will be able to reproduce the issue. Please try not to use phrases like "very big bug", "huge issue", etc. No need to use exclamation marks as well.
We'd appreciate if you prefer posting issues on weekdays rather than weekends.
Note that we don't provide developer help or any kind of support on Github. Please use our [forum](https://forum.espocrm.com) for this.
When reporting a possible bug, please provide detail steps so that we will be able
to reproduce the issue. Please try not to use phrases like "very big bug",
"huge issue", etc. No need to use exclamation marks as well.
Note that we don't provide developer help or any kind of support on GitHub.
For this, please use our [forum](https://forum.espocrm.com).
+1 -1
View File
@@ -2,7 +2,7 @@
## Reporting a vulnerability
If you believe you have discovered a vulnerability in EspoCRM please contacts us via [this](https://www.espocrm.com/contacts/) or [this](https://www.espocrm.com/support/) forms.
If you believe you have discovered a vulnerability in EspoCRM, please contacts us via [this](https://www.espocrm.com/contacts/) or [this](https://www.espocrm.com/support/) forms. Or create a private vulnerability report on GitHub.
## Supported versions
+63
View File
@@ -0,0 +1,63 @@
name: Test Integration on PostgreSQL
on:
schedule:
- cron: '0 11 * * *'
jobs:
test:
name: Test on PHP ${{ matrix.php-versions }}
runs-on: ubuntu-20.04
env:
TEST_DATABASE_HOST: '127.0.0.1'
TEST_DATABASE_PLATFORM: 'Postgresql'
TEST_DATABASE_CHARSET: 'utf8'
TEST_DATABASE_PORT: '8888'
TEST_DATABASE_NAME: integration_test
TEST_DATABASE_USER: postgres
TEST_DATABASE_PASSWORD: password
services:
postgres:
image: postgres:15.2
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: integration_test
ports:
- '8888:5432'
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
fail-fast: false
matrix:
php-versions: ['8.2']
branches: ['master']
steps:
- uses: actions/checkout@v2
with:
ref: ${{ matrix.branches }}
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Setup PHP with Composer
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: composer:v2
ini-values: memory_limit=1024M
- name: NPM install
run: npm install
- name: Build
run: grunt test
- name: Integration testing
run: vendor/bin/phpunit --testsuite integration-pg
+5
View File
@@ -6,6 +6,11 @@
<option name="KEEP_RPAREN_AND_LBRACE_ON_ONE_LINE" value="true" />
<option name="FORCE_EMPTY_METHODS_IN_ONE_LINE" value="true" />
</PHPCodeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="PHP">
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
+2
View File
@@ -14,6 +14,8 @@
</inspection_tool>
<inspection_tool class="PhpSwitchStatementWitSingleBranchInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PsalmAdvanceCallableParamsInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SqlDialectInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="TrivialIfJS" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>
+4 -3
View File
@@ -21,10 +21,10 @@
/**
* * `grunt` - full build;
* * `grunt dev` - build only items needed for development (takes less time);
* * `grunt dev` - build for development;
* * `grunt offline` - build but skip *composer install*;
* * `grunt internal` - build only libs and css;
* * `grunt release` - full build plus upgrade packages`;
* * `grunt release` - full build zipped with upgrade packages`;
* * `grunt test` - build for tests running;
* * `grunt run-tests` - build and run unit and integration tests.
*/
@@ -503,7 +503,8 @@ module.exports = grunt => {
grunt.registerTask('dev', [
'composer-install-dev',
'less',
'npm-install',
'internal',
]);
grunt.registerTask('test', [
+22 -8
View File
@@ -2,16 +2,26 @@
[![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.
[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 REST API backend written in PHP.
It's a web application with a frontend designed as a single page application and REST API
backend written in PHP.
[Download](https://www.espocrm.com/download/) the latest release from our website.
[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.
![Screenshot](https://user-images.githubusercontent.com/1006792/226094559-995dfd2a-a18f-4619-a21b-79a4e671990a.png)
### Demo
You can try the CRM on the online [demo](https://www.espocrm.com/demo/).
### Requirements
* PHP 8.0 and later;
* MySQL 5.7 (and later), or MariaDB 10.1 (and later).
* MySQL 5.7 (and later), or MariaDB 10.2 (and later).
For more information about server configuration see [this article](https://docs.espocrm.com/administration/server-configuration/).
@@ -21,11 +31,12 @@ The documentation for administrators, users and developers is available [here](h
### Bug reporting
Create an issue [here](https://github.com/espocrm/espocrm/issues) or post on our [forum](http://forum.espocrm.com/forum/bug-reports).
Create an issue [here](https://github.com/espocrm/espocrm/issues) or post on our [forum](https://forum.espocrm.com/forum/bug-reports).
We'd appreciate if you prefer posting issues on weekdays rather than weekends.
### Installing the stable version
See the [instructions](https://docs.espocrm.com/administration/installation/) about installation.
See the [instructions](https://docs.espocrm.com/administration/installation/) on installation.
### Development
@@ -35,11 +46,14 @@ See the [instructions](https://docs.espocrm.com/administration/installation/) ab
### Contributing
Before we can merge your pull request you need to accept our CLA [here](https://github.com/espocrm/cla). It's very simple to do.
Before we can merge your pull request, you need to accept our CLA [here](https://github.com/espocrm/cla). It's very simple to do.
Contribute translations to [POEditor](https://poeditor.com/join/project/gLDKZtUF4i). Changes
are usually merged to the GitHub repository before minor releases.
Branches:
* *fix* upcoming maintenance release; fixes should be pushed to this branch;
* *fix* upcoming maintenance release; minor fixes should be pushed to this branch;
* *master* develop branch; new features should be pushed to this branch;
* *stable* last stable release.
+28 -4
View File
@@ -44,6 +44,7 @@ class Binding implements BindingProcessor
public function process(Binder $binder): void
{
$this->bindServices($binder);
$this->bindCore($binder);
$this->bindMisc($binder);
$this->bindAcl($binder);
$this->bindWebSocket($binder);
@@ -63,7 +64,12 @@ class Binding implements BindingProcessor
);
$binder->bindService(
'Espo\\Core\\Container\\Container',
'Espo\\Core\\Container',
'container'
);
$binder->bindService(
'Psr\\Container\\ContainerInterface',
'container'
);
@@ -72,6 +78,16 @@ class Binding implements BindingProcessor
'module'
);
$binder->bindService(
'Espo\\Core\\Utils\\Config',
'config'
);
$binder->bindService(
'Espo\\Core\\Utils\\File\\Manager',
'fileManager'
);
$binder->bindService(
'Espo\\ORM\\EntityManager',
'entityManager'
@@ -208,8 +224,8 @@ class Binding implements BindingProcessor
);
$binder->bindService(
'Espo\\Core\\Acl',
'acl'
'Espo\\Core\\Utils\\ClientManager',
'clientManager'
);
$binder->bindService(
@@ -218,6 +234,14 @@ class Binding implements BindingProcessor
);
}
private function bindCore(Binder $binder): void
{
$binder->bindImplementation(
'Espo\\ORM\\PDO\\PDOProvider',
'Espo\\ORM\\PDO\\DefaultPDOProvider'
);
}
private function bindMisc(Binder $binder): void
{
$binder->bindImplementation(
@@ -246,7 +270,7 @@ class Binding implements BindingProcessor
->for('Espo\\Core\\Authentication\\Oidc\\Login')
->bindImplementation(
'Espo\\Core\\Authentication\\Oidc\\UserProvider',
'Espo\\Core\\Authentication\\Oidc\\DefaultUserProvider'
'Espo\\Core\\Authentication\\Oidc\\UserProvider\\DefaultUserProvider'
);
$binder->bindImplementation(
@@ -0,0 +1,102 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\ConsoleCommands;
use Espo\Core\Console\Command;
use Espo\Core\Console\Command\Params;
use Espo\Core\Console\IO;
use Espo\Core\Utils\Config;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use RuntimeException;
class CreateAdminUser implements Command
{
public function __construct(
private EntityManager $entityManager,
private Config $config
) {}
public function run(Params $params, IO $io): void
{
$userName = $params->getArgument(0);
if (!$userName) {
$io->writeLine("A username must be specified as the first argument.");
$io->setExitStatus(1);
return;
}
/** @var ?string $regExp */
$regExp = $this->config->get('userNameRegularExpression');
if (!$regExp) {
throw new RuntimeException("No `userNameRegularExpression` in config.");
}
if (
str_contains($userName, ' ') ||
preg_replace("/{$regExp}/", '_', $userName) !== $userName
) {
$io->writeLine("Not allowed username.");
$io->setExitStatus(1);
return;
}
$repository = $this->entityManager->getRDBRepositoryByClass(User::class);
$existingUser = $repository
->where(['userName' => $userName])
->findOne();
if ($existingUser) {
$io->writeLine("A user with the same username already exists.");
$io->setExitStatus(1);
return;
}
$user = $repository->getNew();
$user->set('userName', $userName);
$user->set('type', User::TYPE_ADMIN);
$user->set('name', $userName);
$repository->save($user);
$message = "The user '{$userName}' has been created. " .
"Set password with the command: `bin/command set-password {$userName}`.";
$io->writeLine($message);
}
}
@@ -32,26 +32,16 @@ namespace Espo\Classes\ConsoleCommands;
use Espo\Tools\Import\Service;
use Espo\Core\Utils\File\Manager as FileManager;
use Espo\Core\{
Console\Command,
Console\Command\Params,
Console\IO,
};
use Espo\Core\Console\Command;
use Espo\Core\Console\Command\Params;
use Espo\Core\Console\IO;
use Throwable;
class Import implements Command
{
private Service $service;
private FileManager $fileManager;
public function __construct(Service $service, FileManager $fileManager)
{
$this->service = $service;
$this->fileManager = $fileManager;
}
public function __construct(private Service $service, private FileManager $fileManager)
{}
public function run(Params $params, IO $io) : void
{
@@ -63,7 +63,7 @@ class PopulateNumbers implements Command
$field = $params->getArgument(1);
$orderBy = $params->getOption('orderBy') ?? 'createdAt';
$order = $params->getOption('order') ?? Order::ASC;
$order = strtoupper($params->getOption('order') ?? Order::ASC);
if (!$entityType) {
throw new ArgumentNotSpecified("No entity type argument.");
@@ -73,6 +73,10 @@ class PopulateNumbers implements Command
throw new ArgumentNotSpecified("No field argument.");
}
if ($order !== Order::ASC && $order !== Order::DESC) {
throw new InvalidArgument("Bad order option.");
}
$fieldType = $this->entityManager
->getDefs()
->getEntity($entityType)
@@ -33,22 +33,21 @@ use Espo\Core\Duplicate\WhereBuilder;
use Espo\Core\Field\EmailAddressGroup;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\ORM\{
Query\Part\Condition as Cond,
Query\Part\WhereItem,
Query\Part\Where\OrGroup,
Entity,
};
use Espo\ORM\Entity;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\ORM\Query\Part\Where\OrGroup;
use Espo\ORM\Query\Part\WhereItem;
/**
* @implements WhereBuilder<CoreEntity>
*/
class Company implements WhereBuilder
{
/**
* @param CoreEntity $entity
*/
public function build(Entity $entity): ?WhereItem
{
assert($entity instanceof CoreEntity);
$orBuilder = OrGroup::createBuilder();
$toCheck = false;
@@ -27,35 +27,28 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Export\Processor;
namespace Espo\Classes\DuplicateWhereBuilders;
class Data
use Espo\Core\Duplicate\WhereBuilder;
use Espo\ORM\Entity;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\ORM\Query\Part\WhereItem;
/**
* @implements WhereBuilder<Entity>
*/
class Name implements WhereBuilder
{
/**
* @var resource
*/
private $resource;
/**
* @param resource $resource
*/
public function __construct($resource)
public function build(Entity $entity): ?WhereItem
{
$this->resource = $resource;
}
/**
*
* @return ?array<string, mixed>
*/
public function readRow(): ?array
{
$line = fgets($this->resource);
if ($line === false) {
return null;
if ($entity->get('name')) {
return Cond::equal(
Cond::column('name'),
$entity->get('name')
);
}
return unserialize(base64_decode($line));
return null;
}
}
@@ -31,27 +31,24 @@ namespace Espo\Classes\DuplicateWhereBuilders;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\{
Duplicate\WhereBuilder,
Field\EmailAddressGroup,
};
use Espo\Core\Duplicate\WhereBuilder;
use Espo\Core\Field\EmailAddressGroup;
use Espo\ORM\{
Query\Part\Condition as Cond,
Query\Part\WhereItem,
Query\Part\Where\OrGroup,
Entity,
};
use Espo\ORM\Entity;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\ORM\Query\Part\Where\OrGroup;
use Espo\ORM\Query\Part\WhereItem;
/**
* @implements WhereBuilder<CoreEntity>
*/
class Person implements WhereBuilder
{
/**
* @param CoreEntity $entity
*/
public function build(Entity $entity): ?WhereItem
{
assert($entity instanceof CoreEntity);
$orBuilder = OrGroup::createBuilder();
$toCheck = false;
@@ -38,17 +38,10 @@ use stdClass;
class ArrayType
{
private Metadata $metadata;
private const DEFAULT_MAX_ITEM_LENGTH = 100;
private Defs $defs;
private const DEFAULT_MAX_LENGTH = 100;
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
{
@@ -181,9 +174,9 @@ class ArrayType
return false;
}
public function checkMaxLength(Entity $entity, string $field, ?int $validationValue): bool
public function checkMaxItemLength(Entity $entity, string $field, ?int $validationValue): bool
{
$maxLength = $validationValue ?? self::DEFAULT_MAX_LENGTH;
$maxLength = $validationValue ?? self::DEFAULT_MAX_ITEM_LENGTH;
/** @var string[] $value */
$value = $entity->get($field) ?? [];
@@ -0,0 +1,62 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\FieldValidators\AuthenticationProvider;
use Espo\Core\FieldValidation\Validator;
use Espo\Core\FieldValidation\Validator\Data;
use Espo\Core\FieldValidation\Validator\Failure;
use Espo\Core\Utils\Metadata;
use Espo\Entities\AuthenticationProvider;
use Espo\ORM\Entity;
/**
* @implements Validator<AuthenticationProvider>
*/
class MethodValid implements Validator
{
public function __construct(private Metadata $metadata) {}
public function validate(Entity $entity, string $field, Data $data): ?Failure
{
$value = $entity->get($field);
if (!$value) {
return Failure::create();
}
$isAvailable = $this->metadata->get(['authenticationMethods', $value, 'provider', 'isAvailable']);
if (!$isAvailable) {
return Failure::create();
}
return null;
}
}
@@ -29,11 +29,15 @@
namespace Espo\Classes\FieldValidators;
use Espo\Core\Field\Currency;
use Espo\Core\Utils\Config;
use Espo\ORM\BaseEntity;
use Espo\ORM\Entity;
class CurrencyType extends FloatType
{
private const DEFAULT_PRECISION = 13;
public function __construct(private Config $config) {}
protected function isNotEmpty(Entity $entity, string $field): bool
@@ -44,6 +48,62 @@ class CurrencyType extends FloatType
$entity->get($field . 'Currency') !== '';
}
public function checkValid(Entity $entity, string $field): bool
{
if (!$this->isNotEmpty($entity, $field)) {
return true;
}
if ($entity->getAttributeType($field) !== Entity::VARCHAR) {
return true;
}
/** @var string $value */
$value = $entity->get($field);
if (preg_match('/-?[0-9]+\\.?[0-9]*/', $value)) {
return true;
}
return false;
}
public function checkInPermittedRange(Entity $entity, string $field): bool
{
if (!$this->isNotEmpty($entity, $field)) {
return true;
}
if ($entity->getAttributeType($field) !== Entity::VARCHAR) {
return true;
}
if (!$entity instanceof BaseEntity) {
return true;
}
/** @var int $precision */
$precision = $entity->getAttributeParam($field, 'precision') ?? self::DEFAULT_PRECISION;
$value = $entity->get($field);
$currency = Currency::create($value, 'USD');
if ($currency->isNegative()) {
$currency = $currency->multiply(-1);
}
$pad = str_pad('', $precision, '9');
$limit = Currency::create($pad, 'USD');
if ($currency->compare($limit) === 1) {
return false;
}
return true;
}
public function checkValidCurrency(Entity $entity, string $field): bool
{
$attribute = $field . 'Currency';
+2 -2
View File
@@ -420,7 +420,7 @@ class Cleanup implements JobDataLess
->from($scope)
->withDeleted()
->where([
'deleted' => 1,
'deleted' => true,
'modifiedAt<' => $datetime->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
'modifiedAt>' => $datetimeFrom->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
])
@@ -760,7 +760,7 @@ class Cleanup implements JobDataLess
$service = $this->recordServiceContainer->get($scope);
$whereClause = [
'deleted' => 1,
'deleted' => true,
];
if ($this->metadata->get(['entityDefs', $scope, 'fields', 'modifiedAt'])) {
@@ -41,29 +41,20 @@ use Espo\Entities\EmailFolder;
use Espo\Entities\GroupEmailFolder;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use Espo\Tools\Email\Folder;
use Espo\Tools\Email\InboxService as EmailService;
use Exception;
class MoveToFolder implements MassAction
{
private const FOLDER_INBOX = 'inbox';
private QueryBuilder $queryBuilder;
private EntityManager $entityManager;
private EmailService $service;
private User $user;
private const FOLDER_INBOX = Folder::INBOX;
public function __construct(
QueryBuilder $queryBuilder,
EntityManager $entityManager,
EmailService $service,
User $user
) {
$this->queryBuilder = $queryBuilder;
$this->entityManager = $entityManager;
$this->service = $service;
$this->user = $user;
}
private QueryBuilder $queryBuilder,
private EntityManager $entityManager,
private EmailService $service,
private User $user
) {}
/**
* @throws BadRequest
@@ -77,7 +68,7 @@ class MoveToFolder implements MassAction
throw new BadRequest("No folder ID.");
}
if ($folderId !== self::FOLDER_INBOX && strpos($folderId, 'group:') !== 0) {
if ($folderId !== self::FOLDER_INBOX && !str_starts_with($folderId, 'group:')) {
$folder = $this->entityManager
->getRDBRepositoryByClass(EmailFolder::class)
->where([
@@ -91,7 +82,7 @@ class MoveToFolder implements MassAction
}
}
if ($folderId && strpos($folderId, 'group:') === 0) {
if ($folderId && str_starts_with($folderId, 'group:')) {
$folder = $this->entityManager
->getRDBRepositoryByClass(GroupEmailFolder::class)
->where(['id' => substr($folderId, 6)])
@@ -117,7 +108,7 @@ class MoveToFolder implements MassAction
try {
$this->service->moveToFolder($email->getId(), $folderId, $this->user->getId());
}
catch (Exception $e) {
catch (Exception) {
continue;
}
@@ -29,48 +29,36 @@
namespace Espo\Classes\MassAction\User;
use Espo\Core\{
ApplicationUser,
MassAction\Actions\MassDelete as MassDeleteOriginal,
MassAction\QueryBuilder,
MassAction\Params,
MassAction\Result,
MassAction\Data,
MassAction\MassAction,
Acl,
ORM\EntityManager,
Exceptions\Forbidden,
};
use Espo\Core\Acl;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\MassAction\Actions\MassDelete as MassDeleteOriginal;
use Espo\Core\MassAction\Data;
use Espo\Core\MassAction\MassAction;
use Espo\Core\MassAction\Params;
use Espo\Core\MassAction\QueryBuilder;
use Espo\Core\MassAction\Result;
use Espo\Core\ORM\EntityManager;
use Espo\{
Entities\User,
ORM\Entity,
};
use Espo\Core\Utils\SystemUser;
use Espo\Entities\User;
/**
* Extended to forbid removal of own and system users.
*/
class MassDelete implements MassAction
{
private MassDeleteOriginal $massDeleteOriginal;
private QueryBuilder $queryBuilder;
private EntityManager $entityManager;
private Acl $acl;
private User $user;
public function __construct(
MassDeleteOriginal $massDeleteOriginal,
QueryBuilder $queryBuilder,
EntityManager $entityManager,
Acl $acl,
User $user
) {
$this->massDeleteOriginal = $massDeleteOriginal;
$this->queryBuilder = $queryBuilder;
$this->entityManager = $entityManager;
$this->acl = $acl;
$this->user = $user;
}
private MassDeleteOriginal $massDeleteOriginal,
private QueryBuilder $queryBuilder,
private EntityManager $entityManager,
private Acl $acl,
private User $user
) {}
/**
* @throws Forbidden
* @throws BadRequest
*/
public function process(Params $params, Data $data): Result
{
@@ -93,7 +81,7 @@ class MassDelete implements MassAction
->getRDBRepository(User::ENTITY_TYPE)
->clone($query)
->sth()
->select(['id'])
->select(['id', 'userName'])
->find();
foreach ($collection as $entity) {
@@ -106,9 +94,9 @@ class MassDelete implements MassAction
/**
* @throws Forbidden
*/
protected function checkEntity(Entity $entity): void
private function checkEntity(User $entity): void
{
if ($entity->getId() === ApplicationUser::SYSTEM_USER_ID) {
if ($entity->getUserName() === SystemUser::NAME) {
throw new Forbidden("Can't delete 'system' user.");
}
@@ -29,6 +29,7 @@
namespace Espo\Classes\MassAction\User;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\MassAction\Actions\MassUpdate as MassUpdateOriginal;
use Espo\Core\MassAction\QueryBuilder;
use Espo\Core\MassAction\Params;
@@ -42,24 +43,15 @@ use Espo\Core\Acl\Table;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Utils\SystemUser;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Tools\MassUpdate\Data as MassUpdateData;
class MassUpdate implements MassAction
{
private MassUpdateOriginal $massUpdateOriginal;
private QueryBuilder $queryBuilder;
private EntityManager $entityManager;
private Acl $acl;
private User $user;
private FileManager $fileManager;
private DataManager $dataManager;
private const PERMISSION = 'massUpdatePermission';
private const SYSTEM_USER_ID = 'system';
/** @var string[] */
private array $notAllowedAttributeList = [
@@ -72,25 +64,18 @@ class MassUpdate implements MassAction
];
public function __construct(
MassUpdateOriginal $massUpdateOriginal,
QueryBuilder $queryBuilder,
EntityManager $entityManager,
Acl $acl,
User $user,
FileManager $fileManager,
DataManager $dataManager
) {
$this->massUpdateOriginal = $massUpdateOriginal;
$this->queryBuilder = $queryBuilder;
$this->entityManager = $entityManager;
$this->acl = $acl;
$this->user = $user;
$this->fileManager = $fileManager;
$this->dataManager = $dataManager;
}
private MassUpdateOriginal $massUpdateOriginal,
private QueryBuilder $queryBuilder,
private EntityManager $entityManager,
private Acl $acl,
private User $user,
private FileManager $fileManager,
private DataManager $dataManager
) {}
/**
* @throws Forbidden
* @throws BadRequest
*/
public function process(Params $params, Data $data): Result
{
@@ -118,7 +103,7 @@ class MassUpdate implements MassAction
->getRDBRepository(User::ENTITY_TYPE)
->clone($query)
->sth()
->select(['id'])
->select(['id', 'userName'])
->find();
foreach ($collection as $entity) {
@@ -147,9 +132,9 @@ class MassUpdate implements MassAction
/**
* @throws Forbidden
*/
private function checkEntity(Entity $entity, MassUpdateData $data): void
private function checkEntity(User $entity, MassUpdateData $data): void
{
if ($entity->getId() === self::SYSTEM_USER_ID) {
if ($entity->getUserName() === SystemUser::NAME) {
throw new Forbidden("Can't update 'system' user.");
}
@@ -29,34 +29,23 @@
namespace Espo\Classes\Select\Email\AccessControlFilters;
use Espo\Core\{
Select\AccessControl\Filter,
};
use Espo\Core\Select\AccessControl\Filter;
use Espo\{
ORM\Query\SelectBuilder as QueryBuilder,
Classes\Select\Email\Helpers\JoinHelper,
Entities\User,
};
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\User;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
class OnlyOwn implements Filter
{
private $user;
private $joinHelper;
public function __construct(User $user, JoinHelper $joinHelper)
{
$this->user = $user;
$this->joinHelper = $joinHelper;
}
public function __construct(
private User $user,
private JoinHelper $joinHelper
) {}
public function apply(QueryBuilder $queryBuilder): void
{
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
$queryBuilder->where([
'emailUser.userId' => $this->user->getId(),
]);
$queryBuilder->where(['emailUser.userId' => $this->user->getId()]);
}
}
@@ -29,39 +29,47 @@
namespace Espo\Classes\Select\Email\AccessControlFilters;
use Espo\Core\{
Select\AccessControl\Filter,
};
use Espo\Core\Select\AccessControl\Filter;
use Espo\{
ORM\Query\SelectBuilder as QueryBuilder,
Classes\Select\Email\Helpers\JoinHelper,
Entities\User,
};
use Espo\Entities\Email;
use Espo\Entities\Team;
use Espo\Entities\User;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
class OnlyTeam implements Filter
{
private $user;
private $joinHelper;
public function __construct(User $user, JoinHelper $joinHelper)
{
$this->user = $user;
$this->joinHelper = $joinHelper;
}
public function __construct(private User $user)
{}
public function apply(QueryBuilder $queryBuilder): void
{
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
$queryBuilder->distinct();
$queryBuilder->leftJoin('teams', 'teamsAccess');
$queryBuilder->where([
'OR' => [
'teamsAccessMiddle.teamId' => $this->user->getLinkMultipleIdList('teams'),
$subQuery = QueryBuilder::create()
->select('id')
->from(Email::ENTITY_TYPE)
->leftJoin(Team::RELATIONSHIP_ENTITY_TEAM, 'entityTeam', [
'entityTeam.entityId:' => 'id',
'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(),
]
]);
])
->where([
'OR' => [
'entityTeam.teamId' => $this->user->getTeamIdList(),
'emailUser.userId' => $this->user->getId(),
]
])
->build();
$queryBuilder->where(
Cond::in(
Cond::column('id'),
$subQuery
)
);
}
}
@@ -30,45 +30,53 @@
namespace Espo\Classes\Select\Email\AdditionalAppliers;
use Espo\Core\Select\Applier\AdditionalApplier;
use Espo\ORM\Query\SelectBuilder;
use Espo\Core\Select\SearchParams;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\Query\SelectBuilder;
use Espo\Tools\Email\Folder;
class Main implements AdditionalApplier
{
private $user;
private $joinHelper;
public function __construct(User $user, JoinHelper $joinHelper)
{
$this->user = $user;
$this->joinHelper = $joinHelper;
}
public function __construct(
private User $user,
private JoinHelper $joinHelper
) {}
public function apply(SelectBuilder $queryBuilder, SearchParams $searchParams): void
{
$folder = $this->retrieveFolder($searchParams);
if ($folder === 'drafts') {
$queryBuilder->useIndex('createdById');
}
else if ($folder === 'important') {
// skip
}
else if ($this->checkApplyDateSentIndex($queryBuilder, $searchParams)) {
$queryBuilder->useIndex('dateSent');
}
$this->applyIndexes($folder, $queryBuilder, $searchParams);
if ($folder !== 'drafts') {
if ($folder !== Folder::DRAFTS) {
$this->joinEmailUser($queryBuilder);
}
}
protected function joinEmailUser(SelectBuilder $queryBuilder): void
private function applyIndexes(?string $folder, SelectBuilder $queryBuilder, SearchParams $searchParams): void
{
if ($searchParams->getTextFilter()) {
return;
}
if ($folder === Folder::IMPORTANT) {
return;
}
if ($folder === Folder::DRAFTS) {
$queryBuilder->useIndex('createdById');
return;
}
if ($this->checkApplyDateSentIndex($queryBuilder, $searchParams)) {
$queryBuilder->useIndex('dateSent');
}
}
private function joinEmailUser(SelectBuilder $queryBuilder): void
{
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
@@ -77,10 +85,10 @@ class Main implements AdditionalApplier
}
$itemList = [
'isRead',
'isImportant',
'inTrash',
'folderId',
Email::USERS_COLUMN_IS_READ,
Email::USERS_COLUMN_IS_IMPORTANT,
Email::USERS_COLUMN_IN_TRASH,
Email::USERS_COLUMN_FOLDER_ID,
];
foreach ($itemList as $item) {
@@ -88,7 +96,7 @@ class Main implements AdditionalApplier
}
}
protected function retrieveFolder(SearchParams $searchParams): ?string
private function retrieveFolder(SearchParams $searchParams): ?string
{
if (!$searchParams->getWhere()) {
return null;
@@ -103,7 +111,7 @@ class Main implements AdditionalApplier
return null;
}
protected function checkApplyDateSentIndex(SelectBuilder $queryBuilder, SearchParams $searchParams): bool
private function checkApplyDateSentIndex(SelectBuilder $queryBuilder, SearchParams $searchParams): bool
{
if ($searchParams->getTextFilter()) {
return false;
@@ -29,9 +29,8 @@
namespace Espo\Classes\Select\Email\Helpers;
use Espo\{
ORM\Query\SelectBuilder as QueryBuilder,
};
use Espo\Entities\Email;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
class JoinHelper
{
@@ -41,14 +40,10 @@ class JoinHelper
return;
}
$queryBuilder->leftJoin(
'EmailUser',
'emailUser',
[
'emailUser.emailId:' => 'id',
'emailUser.deleted' => false,
'emailUser.userId' => $userId,
]
);
$queryBuilder->leftJoin(Email::RELATIONSHIP_EMAIL_USER, 'emailUser', [
'emailUser.emailId:' => 'id',
'emailUser.deleted' => false,
'emailUser.userId' => $userId,
]);
}
}
@@ -29,6 +29,7 @@
namespace Espo\Classes\Select\Email;
use Espo\Core\Exceptions\Error;
use Espo\Core\Select\Text\Filter;
use Espo\Core\Select\Text\Filter\Data;
use Espo\Core\Select\Text\DefaultFilter;
@@ -44,22 +45,15 @@ use Espo\Entities\EmailAddress;
class TextFilter implements Filter
{
private $defaultFilter;
private $config;
private $entityManager;
public function __construct(
DefaultFilter $defaultFilter,
ConfigProvider $config,
EntityManager $entityManager
) {
$this->defaultFilter = $defaultFilter;
$this->config = $config;
$this->entityManager = $entityManager;
}
private DefaultFilter $defaultFilter,
private ConfigProvider $config,
private EntityManager $entityManager
) {}
/**
* @throws Error
*/
public function apply(QueryBuilder $queryBuilder, Data $data): void
{
$filter = $data->getFilter();
@@ -67,7 +61,7 @@ class TextFilter implements Filter
if (
mb_strlen($filter) < $this->config->getMinLengthForContentSearch() ||
strpos($filter, '@') === false ||
!str_contains($filter, '@') ||
$data->forceFullTextSearch()
) {
$this->defaultFilter->apply($queryBuilder, $data);
@@ -39,46 +39,29 @@ use Espo\ORM\Query\Part\WhereClause;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Classes\Select\Email\Helpers\JoinHelper;
use Espo\Tools\Email\Folder;
class InFolder implements ItemConverter
{
private User $user;
private EntityManager $entityManager;
private JoinHelper $joinHelper;
public function __construct(User $user, EntityManager $entityManager, JoinHelper $joinHelper)
{
$this->user = $user;
$this->entityManager = $entityManager;
$this->joinHelper = $joinHelper;
}
public function __construct(
private User $user,
private EntityManager $entityManager,
private JoinHelper $joinHelper
) {}
public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem
{
$folderId = $item->getValue();
switch ($folderId) {
case 'all':
return WhereClause::fromRaw([]);
case 'inbox':
return $this->convertInbox($queryBuilder);
case 'important':
return $this->convertImportant($queryBuilder);
case 'sent':
return $this->convertSent($queryBuilder);
case 'trash':
return $this->convertTrash($queryBuilder);
case 'drafts':
return $this->convertDraft($queryBuilder);
default:
return $this->convertFolderId($queryBuilder, $folderId);
}
return match ($folderId) {
Folder::ALL => WhereClause::fromRaw([]),
Folder::INBOX => $this->convertInbox($queryBuilder),
Folder::IMPORTANT => $this->convertImportant($queryBuilder),
Folder::SENT => $this->convertSent($queryBuilder),
Folder::TRASH => $this->convertTrash($queryBuilder),
Folder::DRAFTS => $this->convertDraft($queryBuilder),
default => $this->convertFolderId($queryBuilder, $folderId),
};
}
protected function convertInbox(QueryBuilder $queryBuilder): WhereClauseItem
@@ -171,7 +154,7 @@ class InFolder implements ItemConverter
{
$this->joinEmailUser($queryBuilder);
if (substr($folderId, 0, 6) === 'group:') {
if (str_starts_with($folderId, 'group:')) {
$groupFolderId = substr($folderId, 6);
if ($groupFolderId === '') {
@@ -30,22 +30,14 @@
namespace Espo\Classes\Select\Team\AccessControlFilters;
use Espo\Entities\User;
use Espo\Core\Select\AccessControl\Filter;
use Espo\ORM\Query\{
SelectBuilder,
Part\Condition as Cond,
};
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\ORM\Query\SelectBuilder;
class OnlyTeam implements Filter
{
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function __construct(private User $user)
{}
public function apply(SelectBuilder $queryBuilder): void
{
@@ -30,40 +30,31 @@
namespace Espo\Classes\Select\User\AccessControlFilters;
use Espo\ORM\Query\SelectBuilder;
use Espo\Core\{
Select\AccessControl\Filter,
AclManager,
Acl\Table,
};
use Espo\Core\Acl\Table;
use Espo\Core\AclManager;
use Espo\Core\Select\AccessControl\Filter;
use Espo\Entities\User;
class Mandatory implements Filter
{
private $user;
private $aclManager;
public function __construct(User $user, AclManager $aclManager)
{
$this->user = $user;
$this->aclManager = $aclManager;
}
public function __construct(
private User $user,
private AclManager $aclManager
) {}
public function apply(SelectBuilder $queryBuilder): void
{
if (!$this->user->isAdmin()) {
$queryBuilder->where([
'isActive' => true,
'type!=' => 'api',
'type!=' => User::TYPE_API,
]);
}
if ($this->aclManager->getPermissionLevel($this->user, 'portalPermission') !== Table::LEVEL_YES) {
$queryBuilder->where([
'OR' => [
'type!=' => 'portal',
'type!=' => User::TYPE_PORTAL,
'id' => $this->user->getId(),
]
]);
@@ -71,12 +62,12 @@ class Mandatory implements Filter
if (!$this->user->isSuperAdmin()) {
$queryBuilder->where([
'type!=' => 'super-admin'
'type!=' => User::TYPE_SUPER_ADMIN,
]);
}
$queryBuilder->where([
'type!=' => 'system'
'type!=' => User::TYPE_SYSTEM,
]);
}
}
@@ -31,25 +31,18 @@ namespace Espo\Classes\Select\User\AccessControlFilters;
use Espo\ORM\Query\SelectBuilder;
use Espo\Core\{
Select\AccessControl\Filter,
AclManager,
Acl\Table,
};
use Espo\Core\Acl\Table;
use Espo\Core\AclManager;
use Espo\Core\Select\AccessControl\Filter;
use Espo\Entities\User;
class OnlyTeam implements Filter
{
private $user;
private $aclManager;
public function __construct(User $user, AclManager $aclManager)
{
$this->user = $user;
$this->aclManager = $aclManager;
}
public function __construct(
private User $user,
private AclManager $aclManager
) {}
public function apply(SelectBuilder $queryBuilder): void
{
@@ -59,7 +52,7 @@ class OnlyTeam implements Filter
];
if ($this->aclManager->getPermissionLevel($this->user, 'portalPermission') === Table::LEVEL_YES) {
$orGroup['type'] = 'portal';
$orGroup['type'] = User::TYPE_PORTAL;
}
$queryBuilder
@@ -30,6 +30,7 @@
namespace Espo\Classes\Select\User\PrimaryFilters;
use Espo\Core\Select\Primary\Filter;
use Espo\Entities\User;
use Espo\ORM\Query\SelectBuilder;
class Internal implements Filter
@@ -37,7 +38,11 @@ class Internal implements Filter
public function apply(SelectBuilder $queryBuilder): void
{
$queryBuilder->where([
'type!=' => ['portal', 'api', 'system'],
'type!=' => [
User::TYPE_PORTAL,
User::TYPE_API,
User::TYPE_SYSTEM,
],
]);
}
}
@@ -29,16 +29,12 @@
namespace Espo\Classes\Select\User\Where\ItemConverters;
use Espo\Core\{
Select\Where\ItemConverter,
Select\Where\Item,
};
use Espo\{
ORM\Query\SelectBuilder as QueryBuilder,
ORM\Query\Part\WhereItem as WhereClauseItem,
ORM\Query\Part\WhereClause,
};
use Espo\Core\Select\Where\Item;
use Espo\Core\Select\Where\ItemConverter;
use Espo\Entities\User;
use Espo\ORM\Query\Part\WhereClause;
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
class IsOfType implements ItemConverter
{
@@ -46,23 +42,21 @@ class IsOfType implements ItemConverter
{
$type = $item->getValue();
switch ($type) {
case 'internal':
return WhereClause::fromRaw([
'type!=' => ['portal', 'api', 'system'],
]);
case 'api':
return WhereClause::fromRaw([
'type' => 'api',
]);
case 'portal':
return WhereClause::fromRaw([
'type' => 'portal',
]);
}
return WhereClause::fromRaw(['id' => null]);
return match ($type) {
'internal' => WhereClause::fromRaw([
'type!=' => [
User::TYPE_PORTAL,
User::TYPE_API,
User::TYPE_SYSTEM,
],
]),
User::TYPE_PORTAL => WhereClause::fromRaw([
'type' => User::TYPE_PORTAL,
]),
User::TYPE_API => WhereClause::fromRaw([
'type' => User::TYPE_API,
]),
default => WhereClause::fromRaw(['id' => null]),
};
}
}
@@ -51,8 +51,10 @@ class TableTag implements Helper
$content = $function !== null ? $function() : '';
$style = "border: {$border}; border-spacing: 0; border-collapse: collapse;";
return Result::createSafeString(
"<table border=\"{$border}\" cellpadding=\"{$cellpadding}\" {$attributesPart}>" .
"<table style=\"{$style}\" border=\"{$border}\" cellpadding=\"{$cellpadding}\" {$attributesPart}>" .
$content .
"</table>"
);
@@ -56,7 +56,7 @@ class TdTag implements Helper
$content = $function !== null ? $function() : '';
return Result::createSafeString(
"<td>" . $content . "</td>"
"<td {$attributesPart}>{$content}</td>"
);
}
}
+7 -22
View File
@@ -44,30 +44,15 @@ use Espo\Entities\User;
class Admin
{
private Container $container;
private Config $config;
private User $user;
private AdminNotificationManager $adminNotificationManager;
private SystemRequirements $systemRequirements;
private ScheduledJob $scheduledJob;
private DataManager $dataManager;
public function __construct(
Container $container,
Config $config,
User $user,
AdminNotificationManager $adminNotificationManager,
SystemRequirements $systemRequirements,
ScheduledJob $scheduledJob,
DataManager $dataManager
private Container $container,
private Config $config,
private User $user,
private AdminNotificationManager $adminNotificationManager,
private SystemRequirements $systemRequirements,
private ScheduledJob $scheduledJob,
private DataManager $dataManager
) {
$this->container = $container;
$this->config = $config;
$this->user = $user;
$this->adminNotificationManager = $adminNotificationManager;
$this->systemRequirements = $systemRequirements;
$this->scheduledJob = $scheduledJob;
$this->dataManager = $dataManager;
if (!$this->user->isAdmin()) {
throw new Forbidden();
+1 -128
View File
@@ -29,17 +29,11 @@
namespace Espo\Controllers;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Controllers\RecordBase;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Tools\Attachment\FieldData;
use Espo\Tools\Attachment\Service;
use Espo\Tools\Attachment\UploadUrlService;
use Espo\Tools\Attachment\UploadService;
use stdClass;
class Attachment extends RecordBase
@@ -52,125 +46,4 @@ class Attachment extends RecordBase
return parent::getActionList($request, $response);
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws Error
*/
public function postActionGetAttachmentFromImageUrl(Request $request): stdClass
{
$data = $request->getParsedBody();
$url = $data->url ?? null;
$field = $data->field ?? null;
$parentType = $data->parentType ?? null;
$relatedType = $data->relatedType ?? null;
if (!$url || !$field) {
throw new BadRequest("No `url` or `field`.");
}
try {
$fieldData = new FieldData(
$field,
$parentType,
$relatedType
);
}
catch (Error $e) {
throw new BadRequest($e->getMessage());
}
return $this->injectableFactory
->create(UploadUrlService::class)
->uploadImage($url, $fieldData)
->getValueMap();
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
*/
public function postActionGetCopiedAttachment(Request $request): stdClass
{
$data = $request->getParsedBody();
$id = $data->id ?? null;
$field = $data->field ?? null;
$parentType = $data->parentType ?? null;
$relatedType = $data->relatedType ?? null;
if (!$id || !$field) {
throw new BadRequest("No `id` or `field`.");
}
try {
$fieldData = new FieldData(
$field,
$parentType,
$relatedType
);
}
catch (Error $e) {
throw new BadRequest($e->getMessage());
}
return $this->getAttachmentService()
->copy($id, $fieldData)
->getValueMap();
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
*/
public function getActionFile(Request $request, Response $response): void
{
$id = $request->getRouteParam('id');
if (!$id) {
throw new BadRequest();
}
$fileData = $this->getAttachmentService()->getFileData($id);
if ($fileData->getType()) {
$response->setHeader('Content-Type', $fileData->getType());
}
$response
->setHeader('Content-Disposition', 'attachment; filename="' . $fileData->getName() . '"')
->setHeader('Content-Length', (string) $fileData->getSize())
->setBody($fileData->getStream());
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws Error
* @throws NotFound
*/
public function postActionChunk(Request $request, Response $response): void
{
$id = $request->getRouteParam('id');
$body = $request->getBodyContents();
if (!$id || !$body) {
throw new BadRequest();
}
$this->injectableFactory
->create(UploadService::class)
->uploadChunk($id, $body);
$response->writeBody('true');
}
private function getAttachmentService(): Service
{
return $this->injectableFactory->create(Service::class);
}
}
@@ -0,0 +1,44 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
use Espo\Core\Controllers\RecordBase;
class AuthenticationProvider extends RecordBase
{
protected function checkAccess(): bool
{
if (!$this->user->isAdmin()) {
return false;
}
return true;
}
}
+5 -14
View File
@@ -32,26 +32,17 @@ namespace Espo\Controllers;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\{
Api\Request,
Api\Response,
Acl,
};
use Espo\Core\Acl;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Tools\DataPrivacy\Erasor;
class DataPrivacy
{
private $erasor;
private $acl;
public function __construct(Erasor $erasor, Acl $acl)
public function __construct(private Erasor $erasor, private Acl $acl)
{
$this->erasor = $erasor;
$this->acl = $acl;
if ($this->acl->get('dataPrivacyPermission') === 'no') {
if ($this->acl->getPermissionLevel('dataPrivacyPermission') === Acl\Table::LEVEL_NO) {
throw new Forbidden();
}
}
+1 -350
View File
@@ -29,356 +29,7 @@
namespace Espo\Controllers;
use Espo\Core\Acl\Table;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Controllers\Record;
use Espo\Core\Api\Request;
use Espo\Core\Mail\SmtpParams;
use Espo\Entities\Email as EmailEntity;
use Espo\Tools\Attachment\FieldData;
use Espo\Tools\Email\SendService;
use Espo\Tools\Email\InboxService as InboxService;
use Espo\Tools\Email\Service;
use Espo\Tools\Email\TestSendData;
use Espo\Tools\EmailTemplate\InsertField\Service as InsertFieldService;
use stdClass;
class Email extends Record
{
/**
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
*/
public function postActionGetCopiedAttachments(Request $request): stdClass
{
$data = $request->getParsedBody();
$id = $data->id ?? null;
$field = $data->field ?? null;
$parentType = $data->parentType ?? null;
$relatedType = $data->relatedType ?? null;
if (!$id || !$field) {
throw new BadRequest("No `id` or `field`.");
}
try {
$fieldData = new FieldData(
$field,
$parentType,
$relatedType
);
}
catch (Error $e) {
throw new BadRequest($e->getMessage());
}
$list = $this->injectableFactory
->create(Service::class)
->copyAttachments($id, $fieldData);
$ids = array_map(
fn ($item) => $item->getId(),
$list
);
$names = (object) [];
foreach ($list as $item) {
$names->{$item->getId()} = $item->getName();
}
return (object) [
'ids' => $ids,
'names' => $names,
];
}
/**
* @throws Forbidden
* @throws NotFound
* @throws Error
* @throws BadRequest
*/
public function postActionSendTestEmail(Request $request): bool
{
if (!$this->acl->checkScope(EmailEntity::ENTITY_TYPE)) {
throw new Forbidden();
}
$data = $request->getParsedBody();
$type = $data->type ?? null;
$id = $data->id ?? null;
$server = $data->server ?? null;
$port = $data->port ?? null;
$username = $data->username ?? null;
$password = $data->password ?? null;
$auth = $data->auth ?? null;
$authMechanism = $data->authMechanism ?? null;
$security = $data->security ?? null;
$userId = $data->userId ?? null;
$fromAddress = $data->fromAddress ?? null;
$fromName = $data->fromName ?? null;
$emailAddress = $data->emailAddress ?? null;
if (!is_string($server)) {
throw new BadRequest("`server`");
}
if (!is_int($port)) {
throw new BadRequest("`port`.");
}
if (!is_string($emailAddress)) {
throw new BadRequest("`emailAddress`.");
}
$smtpParams = SmtpParams
::create($server, $port)
->withSecurity($security)
->withFromName($fromName)
->withFromAddress($fromAddress)
->withAuth($auth);
if ($auth) {
$smtpParams = $smtpParams
->withUsername($username)
->withPassword($password)
->withAuthMechanism($authMechanism);
}
$data = new TestSendData($emailAddress, $type, $id, $userId);
$this->getSendService()->sendTestEmail($smtpParams, $data);
return true;
}
/**
* @throws BadRequest
*/
public function postActionMarkAsRead(Request $request): bool
{
$data = $request->getParsedBody();
if (!empty($data->ids)) {
$idList = $data->ids;
}
else {
if (!empty($data->id)) {
$idList = [$data->id];
}
else {
throw new BadRequest();
}
}
$this->getInboxService()->markAsReadIdList($idList);
return true;
}
/**
* @throws BadRequest
*/
public function postActionMarkAsNotRead(Request $request): bool
{
$data = $request->getParsedBody();
if (!empty($data->ids)) {
$idList = $data->ids;
}
else {
if (!empty($data->id)) {
$idList = [$data->id];
}
else {
throw new BadRequest();
}
}
$this->getInboxService()->markAsNotReadIdList($idList);
return true;
}
public function postActionMarkAllAsRead(): bool
{
$this->getInboxService()->markAllAsRead();
return true;
}
/**
* @throws BadRequest
*/
public function postActionMarkAsImportant(Request $request): bool
{
$data = $request->getParsedBody();
if (!empty($data->ids)) {
$idList = $data->ids;
}
else {
if (!empty($data->id)) {
$idList = [$data->id];
}
else {
throw new BadRequest();
}
}
$this->getInboxService()->markAsImportantIdList($idList);
return true;
}
/**
* @throws BadRequest
*/
public function postActionMarkAsNotImportant(Request $request): bool
{
$data = $request->getParsedBody();
if (!empty($data->ids)) {
$idList = $data->ids;
}
else {
if (!empty($data->id)) {
$idList = [$data->id];
}
else {
throw new BadRequest();
}
}
$this->getInboxService()->markAsNotImportantIdList($idList);
return true;
}
/**
* @throws BadRequest
*/
public function postActionMoveToTrash(Request $request): bool
{
$data = $request->getParsedBody();
if (!empty($data->ids)) {
$idList = $data->ids;
}
else {
if (!empty($data->id)) {
$idList = [$data->id];
}
else {
throw new BadRequest();
}
}
$this->getInboxService()->moveToTrashIdList($idList);
return true;
}
/**
* @throws BadRequest
*/
public function postActionRetrieveFromTrash(Request $request): bool
{
$data = $request->getParsedBody();
if (!empty($data->ids)) {
$idList = $data->ids;
}
else {
if (!empty($data->id)) {
$idList = [$data->id];
}
else {
throw new BadRequest();
}
}
$this->getInboxService()->retrieveFromTrashIdList($idList);
return true;
}
public function getActionGetFoldersNotReadCounts(): stdClass
{
return (object) $this->getInboxService()->getFoldersNotReadCounts();
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
*/
public function postActionMoveToFolder(Request $request): bool
{
$data = $request->getParsedBody();
if (!empty($data->ids)) {
$idList = $data->ids;
}
else if (!empty($data->id)) {
$idList = [$data->id];
}
else {
throw new BadRequest();
}
if (empty($data->folderId)) {
throw new BadRequest();
}
if (count($idList) === 1) {
$this->getInboxService()->moveToFolder($idList[0], $data->folderId);
return true;
}
$this->getInboxService()->moveToFolderIdList($idList, $data->folderId);
return true;
}
/**
* @throws Forbidden
*/
public function getActionGetInsertFieldData(Request $request): stdClass
{
if (!$this->acl->checkScope(EmailEntity::ENTITY_TYPE, Table::ACTION_CREATE)) {
throw new Forbidden();
}
return $this->injectableFactory
->create(InsertFieldService::class)
->getData(
$request->getQueryParam('parentType'),
$request->getQueryParam('parentId'),
$request->getQueryParam('to')
);
}
private function getInboxService(): InboxService
{
return $this->injectableFactory->create(InboxService::class);
}
private function getSendService(): SendService
{
return $this->injectableFactory->create(SendService::class);
}
}
{}
+1 -46
View File
@@ -29,52 +29,7 @@
namespace Espo\Controllers;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\BadRequest;
use Espo\Tools\Email\AddressService as Service;
use Espo\Core\Api\Request;
use Espo\Core\Controllers\RecordBase;
class EmailAddress extends RecordBase
{
private const ADDRESS_MAX_SIZE = 50;
/**
* @return array<int,array<string,mixed>>
* @throws Forbidden
* @throws BadRequest
*/
public function actionSearchInAddressBook(Request $request): array
{
if (!$this->acl->checkScope('Email')) {
throw new Forbidden();
}
if (!$this->acl->checkScope('Email', 'create')) {
throw new Forbidden();
}
$q = $request->getQueryParam('q');
if ($q === null) {
throw new BadRequest("No `q` parameter.");
}
$maxSize = intval($request->getQueryParam('maxSize'));
if (!$maxSize || $maxSize > self::ADDRESS_MAX_SIZE) {
$maxSize = (int) $this->config->get('recordsPerPage');
}
$onlyActual = $request->getQueryParam('onlyActual') === 'true';
return $this->getEmailAddressService()->searchInAddressBook($q, $maxSize, $onlyActual);
}
private function getEmailAddressService(): Service
{
return $this->injectableFactory->create(Service::class);
}
}
{}
@@ -31,5 +31,4 @@ namespace Espo\Controllers;
class EmailFilter extends \Espo\Core\Controllers\Record
{
}
+1 -42
View File
@@ -29,48 +29,7 @@
namespace Espo\Controllers;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\ForbiddenSilent;
use Espo\Core\Exceptions\NotFound;
use Espo\Tools\EmailTemplate\Data;
use Espo\Tools\EmailTemplate\Service;
use Espo\Core\Api\Request;
use Espo\Core\Controllers\Record;
use stdClass;
class EmailTemplate extends Record
{
/**
* @throws BadRequest
* @throws NotFound
* @throws ForbiddenSilent
*/
public function actionParse(Request $request): stdClass
{
$id = $request->getQueryParam('id');
if ($id === null) {
throw new BadRequest("No `id`.");
}
$data = Data::create()
->withRelatedType($request->getQueryParam('relatedType'))
->withRelatedId($request->getQueryParam('relatedId'))
->withParentType($request->getQueryParam('parentType'))
->withParentId($request->getQueryParam('parentId'))
->withEmailAddress($request->getQueryParam('emailAddress'));
$result = $this->getEmailTemplateService()->process($id, $data);
return $result->getValueMap();
}
private function getEmailTemplateService(): Service
{
return $this->injectableFactory->create(Service::class);
}
}
{}
@@ -31,5 +31,4 @@ namespace Espo\Controllers;
class EmailTemplateCategory extends \Espo\Core\Templates\Controllers\CategoryTree
{
}
+9 -16
View File
@@ -29,27 +29,20 @@
namespace Espo\Controllers;
use Espo\{
Entities\User,
Tools\EntityManager\EntityManager as EntityManagerTool,
};
use Espo\Entities\User;
use Espo\Tools\EntityManager\EntityManager as EntityManagerTool;
use Espo\Core\{
Exceptions\Forbidden,
Exceptions\BadRequest,
Api\Request,
};
use Espo\Core\Api\Request;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
class EntityManager
{
private $user;
private $entityManagerTool;
public function __construct(User $user, EntityManagerTool $entityManagerTool)
{
$this->user = $user;
$this->entityManagerTool = $entityManagerTool;
public function __construct(
private User $user,
private EntityManagerTool $entityManagerTool
) {
if (!$this->user->isAdmin()) {
throw new Forbidden();
+4 -6
View File
@@ -32,12 +32,10 @@ namespace Espo\Controllers;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\{
Upgrades\ExtensionManager,
Controllers\RecordBase,
Api\Request,
Api\Response,
};
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Controllers\RecordBase;
use Espo\Core\Upgrades\ExtensionManager;
use stdClass;
+27 -31
View File
@@ -29,36 +29,25 @@
namespace Espo\Controllers;
use Espo\{
Entities\User,
Tools\FieldManager\FieldManager as FieldManagerTool,
};
use Espo\Core\{
Exceptions\Conflict,
Exceptions\Error,
Exceptions\Forbidden,
Exceptions\BadRequest,
Api\Request,
DataManager};
use Espo\Entities\User;
use Espo\Tools\FieldManager\FieldManager as FieldManagerTool;
use Espo\Core\Api\Request;
use Espo\Core\DataManager;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Conflict;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
class FieldManager
{
private $user;
private $dataManager;
private $fieldManagerTool;
/**
* @throws Forbidden
*/
public function __construct(User $user, DataManager $dataManager, FieldManagerTool $fieldManagerTool)
{
$this->user = $user;
$this->dataManager = $dataManager;
$this->fieldManagerTool = $fieldManagerTool;
public function __construct(
private User $user,
private DataManager $dataManager,
private FieldManagerTool $fieldManagerTool
) {
$this->checkControllerAccess();
}
@@ -90,7 +79,7 @@ class FieldManager
}
/**
* @return array<string,mixed>
* @return array<string, mixed>
* @throws BadRequest
* @throws Conflict
* @throws Error
@@ -123,7 +112,7 @@ class FieldManager
}
/**
* @return array<string,mixed>
* @return array<string, mixed>
* @throws BadRequest
* @throws Error
*/
@@ -133,7 +122,7 @@ class FieldManager
}
/**
* @return array<string,mixed>
* @return array<string, mixed>
* @throws BadRequest
* @throws Error
*/
@@ -176,6 +165,7 @@ class FieldManager
$result = $this->fieldManagerTool->delete($scope, $name);
$this->dataManager->clearCache();
$this->dataManager->rebuildMetadata();
return $result;
@@ -189,14 +179,20 @@ class FieldManager
{
$data = $request->getParsedBody();
if (empty($data->scope) || empty($data->name)) {
$scope = $data->scope ?? null;
$name = $data->name ?? null;
if (!$scope || !$name) {
throw new BadRequest();
}
$this->fieldManagerTool->resetToDefault($data->scope, $data->name);
if (!is_string($scope) || !is_string($name)) {
throw new BadRequest();
}
$this->dataManager->clearCache();
$this->dataManager->rebuildMetadata();
$this->fieldManagerTool->resetToDefault($scope, $name);
$this->dataManager->rebuild([$scope]);
return true;
}
+5 -111
View File
@@ -30,16 +30,16 @@
namespace Espo\Controllers;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Tools\Import\Params as ImportParams;
use Espo\Tools\Import\Service as Service;
use Espo\Core\{
Controllers\Record,
Api\Request,
Api\Response,
};
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Controllers\Record;
use Espo\Core\Di\InjectableFactoryAware;
use Espo\Core\Di\InjectableFactorySetter;
@@ -57,112 +57,6 @@ class Import extends Record
return $this->acl->check('Import');
}
private function getImportService(): Service
{
return $this->injectableFactory->create(Service::class);
}
public function postActionUploadFile(Request $request): stdClass
{
$contents = $request->getBodyContents() ?? '';
$attachmentId = $this->getImportService()->uploadFile($contents);
return (object) [
'attachmentId' => $attachmentId
];
}
public function postActionRevert(Request $request): bool
{
$data = $request->getParsedBody();
$this->getImportService()->revert($data->id);
return true;
}
public function postActionRemoveDuplicates(Request $request): bool
{
$data = $request->getParsedBody();
if (empty($data->id)) {
throw new BadRequest();
}
$this->getImportService()->removeDuplicates($data->id);
return true;
}
public function postActionCreate(Request $request, Response $response): stdClass
{
$data = $request->getParsedBody();
$entityType = $data->entityType ?? null;
$attributeList = $data->attributeList ?? null;
$attachmentId = $data->attachmentId ?? null;
if (!is_array($attributeList)) {
throw new BadRequest("No attributeList.");
}
if (!$attachmentId) {
throw new BadRequest("No attachmentId.");
}
if (!$entityType) {
throw new BadRequest("No entityType.");
}
$params = ImportParams::fromRaw($data);
$result = $this->getImportService()->import(
$entityType,
$attributeList,
$attachmentId,
$params
);
return $result->getValueMap();
}
public function postActionUnmarkAsDuplicate(Request $request): bool
{
$data = $request->getParsedBody();
if (
empty($data->id) ||
empty($data->entityType) ||
empty($data->entityId)
) {
throw new BadRequest();
}
$this->getImportService()->unmarkAsDuplicate($data->id, $data->entityType, $data->entityId);
return true;
}
/**
* @throws BadRequest
* @throws \Espo\Core\Exceptions\NotFound
*/
public function postActionExportErrors(Request $request): stdClass
{
$id = $request->getParsedBody()->id ?? null;
if (!$id) {
throw new BadRequest("No `id`.");
}
$attachmentId = $this->getImportService()->exportErrors($id);
return (object) [
'attachmentId' => $attachmentId,
];
}
public function putActionUpdate(Request $request, Response $response): stdClass
{
throw new Forbidden();
+2 -1
View File
@@ -58,9 +58,10 @@ class Oidc
$response->writeBody(Json::encode($data));
}
/**
* @throws BadRequest
* @throws ForbiddenSilent
* @throws Forbidden
*/
public function postActionBackchannelLogout(Request $request, Response $response): void
{
@@ -55,7 +55,8 @@ class TwoFactorEmail
if (
!$this->user->isAdmin() &&
!$this->user->isRegular()
!$this->user->isRegular() &&
!$this->user->isPortal()
) {
throw new Forbidden();
}
@@ -55,7 +55,8 @@ class TwoFactorSms
if (
!$this->user->isAdmin() &&
!$this->user->isRegular()
!$this->user->isRegular() &&
!$this->user->isPortal()
) {
throw new Forbidden();
}
-182
View File
@@ -29,191 +29,14 @@
namespace Espo\Controllers;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Api\Request;
use Espo\Core\Controllers\Record;
use Espo\Core\Mail\Exceptions\SendingError;
use Espo\Tools\UserSecurity\ApiService;
use Espo\Tools\UserSecurity\Password\RecoveryService;
use Espo\Core\Select\SearchParams;
use Espo\Core\Select\Where\Item as WhereItem;
use Espo\Tools\UserSecurity\Password\Service as PasswordService;
use stdClass;
class User extends Record
{
/**
* @throws Forbidden
* @throws NotFound
* @throws Error
*/
public function getActionAcl(Request $request): stdClass
{
$userId = $request->getQueryParam('id');
if (empty($userId)) {
throw new Error();
}
if (
!$this->user->isAdmin() &&
$this->user->getId() !== $userId
) {
throw new Forbidden();
}
$user = $this->entityManager->getEntityById(\Espo\Entities\User::ENTITY_TYPE, $userId);
if (empty($user)) {
throw new NotFound();
}
return $this->aclManager->getMapData($user);
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws Error
* @throws NotFound
*/
public function postActionChangeOwnPassword(Request $request): bool
{
$data = $request->getParsedBody();
$password = $data->password ?? null;
$currentPassword = $data->currentPassword ?? null;
if (
!is_string($password) ||
!is_string($currentPassword)
) {
throw new BadRequest();
}
$this->getPasswordService()->changePasswordWithCheck($this->user->getId(), $password, $currentPassword);
return true;
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws Error
* @throws NotFound
*/
public function postActionChangePasswordByRequest(Request $request): stdClass
{
$data = $request->getParsedBody();
if (empty($data->requestId) || empty($data->password)) {
throw new BadRequest();
}
$url = $this->getPasswordService()->changePasswordByRecovery($data->requestId, $data->password);
return (object) [
'url' => $url,
];
}
/**
* @throws BadRequest
* @throws Forbidden
*/
public function postActionPasswordChangeRequest(Request $request): bool
{
$data = $request->getParsedBody();
$userName = $data->userName ?? null;
$emailAddress = $data->emailAddress ?? null;
$url = $data->url ?? null;
if (!$userName || !$emailAddress) {
throw new BadRequest();
}
$this->injectableFactory
->create(RecoveryService::class)
->request($emailAddress, $userName, $url);
return true;
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
*/
public function postActionGenerateNewApiKey(Request $request): stdClass
{
$data = $request->getParsedBody();
if (empty($data->id)) {
throw new BadRequest();
}
if (!$this->user->isAdmin()) {
throw new Forbidden();
}
return $this->injectableFactory
->create(ApiService::class)
->generateNewApiKey($data->id)
->getValueMap();
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws Error
* @throws SendingError
* @throws NotFound
*/
public function postActionGenerateNewPassword(Request $request): bool
{
$data = $request->getParsedBody();
if (empty($data->id)) {
throw new BadRequest();
}
if (!$this->user->isAdmin()) {
throw new Forbidden();
}
$this->getPasswordService()->generateAndSendNewPasswordForUser($data->id);
return true;
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
* @throws Error
*/
public function postActionSendPasswordChangeLink(Request $request): bool
{
if (!$this->user->isAdmin()) {
throw new Forbidden();
}
$id = $request->getParsedBody()->id ?? null;
if (!$id) {
throw new BadRequest();
}
$this->getPasswordService()->createAndSendPasswordRecovery($id);
return true;
}
public function postActionCreateLink(Request $request): bool
{
if (!$this->user->isAdmin()) {
@@ -250,9 +73,4 @@ class User extends Record
])
);
}
private function getPasswordService(): PasswordService
{
return $this->injectableFactory->create(PasswordService::class);
}
}
@@ -57,7 +57,8 @@ class UserSecurity
if (
!$this->user->isAdmin() &&
!$this->user->isRegular()
!$this->user->isRegular() &&
!$this->user->isPortal()
) {
throw new Forbidden();
}
+10 -15
View File
@@ -29,10 +29,9 @@
namespace Espo\Core;
use Espo\Core\{
Acl\GlobalRestriction,
Acl\Table,
Acl\Exceptions\NotImplemented};
use Espo\Core\Acl\Exceptions\NotImplemented;
use Espo\Core\Acl\GlobalRestriction;
use Espo\Core\Acl\Table;
use Espo\ORM\Entity;
use Espo\Entities\User;
@@ -44,14 +43,10 @@ use stdClass;
*/
class Acl
{
protected AclManager $aclManager;
protected User $user;
public function __construct(AclManager $aclManager, User $user)
{
$this->aclManager = $aclManager;
$this->user = $user;
}
public function __construct(
protected AclManager $aclManager,
protected User $user
) {}
/**
* Get a full access data map.
@@ -343,7 +338,7 @@ class Acl
*/
public function checkIsOwner(Entity $entity): bool
{
return $this->aclManager->checkIsOwner($this->user, $entity);
return $this->aclManager->checkOwnershipOwn($this->user, $entity);
}
/**
@@ -351,11 +346,11 @@ class Acl
*/
public function checkInTeam(Entity $entity): bool
{
return $this->aclManager->checkInTeam($this->user, $entity);
return $this->aclManager->checkOwnershipTeam($this->user, $entity);
}
/**
* @deprecated
* @deprecated Use `checkUserPermission` instead.
*/
public function checkUser(string $permission, User $entity): bool
{
+3 -4
View File
@@ -30,9 +30,8 @@
namespace Espo\Core\Acl;
/**
* @deprecated Use AccessChecker interfaces instead.
* @deprecated As of v7.0. Use AccessChecker interfaces instead.
* @see https://docs.espocrm.com/development/metadata/acl-defs/
*/
class Acl extends Base
{
}
{}
+2 -1
View File
@@ -44,7 +44,8 @@ use Espo\Core\{
};
/**
* @deprecated Use AccessChecker interfaces instead.
* @deprecated As of v6.0. Use AccessChecker interfaces instead.
* @see https://docs.espocrm.com/development/metadata/acl-defs/
*/
class Base implements AccessChecker, Injectable
{
@@ -30,16 +30,12 @@
namespace Espo\Core\Acl;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\Core\{
Acl\Table,
Acl\AccessChecker\ScopeCheckerData,
Acl\AccessChecker\ScopeChecker,
AclManager,
Utils\Config,
};
use Espo\Core\Acl\AccessChecker\ScopeChecker;
use Espo\Core\Acl\AccessChecker\ScopeCheckerData;
use Espo\Core\AclManager;
use Espo\Core\Utils\Config;
use DateTime;
use Exception;
@@ -62,28 +58,15 @@ class DefaultAccessChecker implements
AccessEntityStreamChecker
{
private const ATTR_CREATED_BY_ID = 'createdById';
private const ATTR_CREATED_AT = 'createdAt';
private const ATTR_ASSIGNED_USER_ID = 'assignedUserId';
private const ALLOW_DELETE_OWN_CREATED_PERIOD = '24 hours';
private AclManager $aclManager;
private Config $config;
private ScopeChecker $scopeChecker;
public function __construct(
AclManager $aclManager,
Config $config,
ScopeChecker $scopeChecker
) {
$this->aclManager = $aclManager;
$this->config = $config;
$this->scopeChecker = $scopeChecker;
}
private AclManager $aclManager,
private Config $config,
private ScopeChecker $scopeChecker
) {}
private function checkEntity(User $user, Entity $entity, ScopeData $data, string $action): bool
{
@@ -235,7 +218,7 @@ class DefaultAccessChecker implements
try {
$dt = new DateTime($value);
}
catch (Exception $e) {
catch (Exception) {
return false;
}
@@ -33,19 +33,12 @@ use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Repositories\User as UserRepository;
use Espo\ORM\{
Entity,
EntityManager,
Defs,
};
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Core\{
AclManager,
Acl\AssignmentChecker,
Acl\Table,
};
use Espo\Core\AclManager;
/**
* @implements AssignmentChecker<CoreEntity>
@@ -53,27 +46,16 @@ use Espo\Core\{
class DefaultAssignmentChecker implements AssignmentChecker
{
protected const FIELD_ASSIGNED_USERS = 'assignedUsers';
protected const FIELD_TEAMS = 'teams';
protected const ATTR_ASSIGNED_USER_ID = 'assignedUserId';
protected const ATTR_TEAMS_IDS = 'teamsIds';
protected const ATTR_ASSIGNED_USERS_IDS = 'assignedUsersIds';
private AclManager $aclManager;
private EntityManager $entityManager;
private Defs $ormDefs;
public function __construct(AclManager $aclManager, EntityManager $entityManager, Defs $ormDefs)
{
$this->aclManager = $aclManager;
$this->entityManager = $entityManager;
$this->ormDefs = $ormDefs;
}
public function __construct(
private AclManager $aclManager,
private EntityManager $entityManager,
private Defs $ormDefs
) {}
public function check(User $user, Entity $entity): bool
{
@@ -154,7 +136,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
}
if ($assignmentPermission === Table::LEVEL_NO) {
if ($user->id !== $assignedUserId) {
if ($user->getId() !== $assignedUserId) {
return false;
}
}
@@ -42,13 +42,9 @@ use Espo\Entities\User;
class DefaultOwnershipChecker implements OwnershipOwnChecker, OwnershipTeamChecker
{
private const ATTR_CREATED_BY_ID = 'createdById';
private const ATTR_ASSIGNED_USER_ID = 'assignedUserId';
private const ATTR_ASSIGNED_TEAMS_IDS = 'teamsIds';
private const FIELD_TEAMS = 'teams';
private const FIELD_ASSIGNED_USERS = 'assignedUsers';
public function checkOwn(User $user, Entity $entity): bool
+1 -1
View File
@@ -38,7 +38,7 @@ use RuntimeException;
class FieldData
{
/**
* @var array<string,string>
* @var array<string, string>
*/
private $actionData = [];
+10 -23
View File
@@ -29,12 +29,10 @@
namespace Espo\Core\Acl;
use Espo\Core\{
Utils\Metadata,
Utils\DataCache,
Utils\FieldUtil,
Utils\Config,
};
use Espo\Core\Utils\Config;
use Espo\Core\Utils\DataCache;
use Espo\Core\Utils\FieldUtil;
use Espo\Core\Utils\Metadata;
use stdClass;
@@ -46,21 +44,17 @@ class GlobalRestriction
{
/** Totally forbidden. */
public const TYPE_FORBIDDEN = 'forbidden';
/** Reading forbidden, writing allowed. */
public const TYPE_INTERNAL = 'internal';
/** Forbidden for non-admin users. */
public const TYPE_ONLY_ADMIN = 'onlyAdmin';
/** Read-only for all users. */
public const TYPE_READ_ONLY = 'readOnly';
/** Read-only for non-admin users. */
public const TYPE_NON_ADMIN_READ_ONLY = 'nonAdminReadOnly';
/**
* @var array<int,self::TYPE_*>
* @var array<int, self::TYPE_*>
*/
private $fieldTypeList = [
self::TYPE_FORBIDDEN,
@@ -71,7 +65,7 @@ class GlobalRestriction
];
/**
* @var array<int,self::TYPE_*>
* @var array<int, self::TYPE_*>
*/
private $linkTypeList = [
self::TYPE_FORBIDDEN,
@@ -83,7 +77,7 @@ class GlobalRestriction
/**
* Types that should also be taken from entityDefs.
* @var array<int,self::TYPE_*>
* @var array<int, self::TYPE_*>
*/
private array $entityDefsTypeList = [
self::TYPE_READ_ONLY,
@@ -93,19 +87,12 @@ class GlobalRestriction
private string $cacheKey = 'entityAcl';
private Metadata $metadata;
private DataCache $dataCache;
private FieldUtil $fieldUtil;
public function __construct(
Metadata $metadata,
DataCache $dataCache,
FieldUtil $fieldUtil,
private Metadata $metadata,
private DataCache $dataCache,
private FieldUtil $fieldUtil,
Config $config
) {
$this->metadata = $metadata;
$this->dataCache = $dataCache;
$this->fieldUtil = $fieldUtil;
$useCache = $config->get('useCache');
+48
View File
@@ -0,0 +1,48 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl;
use Espo\Entities\User;
use Espo\ORM\Entity;
/**
* Checks access for linking/unlinking two records.
*
* @template TEntity of Entity
* @template TForeignEntity of Entity
*/
interface LinkChecker
{
/**
* @param TEntity $entity
* @param TForeignEntity $foreignEntity
*/
public function check(User $user, Entity $entity, Entity $foreignEntity): bool;
}
@@ -0,0 +1,74 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\LinkChecker;
use Espo\Core\Acl\LinkChecker;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Entity;
use RuntimeException;
class LinkCheckerFactory
{
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory
) {}
/**
* Create a link checker.
*
* @return LinkChecker<Entity, Entity>
*/
public function create(string $scope, string $link): LinkChecker
{
$className = $this->getClassName($scope, $link);
if (!$className) {
throw new RuntimeException("Link checker is not implemented for {$scope}.{$link}.");
}
return $this->injectableFactory->create($className);
}
public function isCreatable(string $scope, string $link): bool
{
return (bool) $this->getClassName($scope, $link);
}
/**
* @return ?class-string<LinkChecker<Entity, Entity>>
*/
private function getClassName(string $scope, string $link): ?string
{
/** @var ?class-string<LinkChecker<Entity, Entity>> */
return $this->metadata->get(['aclDefs', $scope, 'linkCheckerClassNameMap', $link]);
}
}
@@ -31,16 +31,12 @@ namespace Espo\Core\Acl\Table;
use Espo\Entities\User;
use Espo\Core\{
Acl\Table,
Acl\Table\RoleListProvider,
Acl\Table\CacheKeyProvider,
Acl\ScopeData,
Acl\FieldData,
Utils\Config,
Utils\Metadata,
Utils\DataCache,
};
use Espo\Core\Acl\FieldData;
use Espo\Core\Acl\ScopeData;
use Espo\Core\Acl\Table;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\DataCache;
use Espo\Core\Utils\Metadata;
use stdClass;
use RuntimeException;
@@ -54,7 +50,6 @@ class DefaultTable implements Table
private const LEVEL_NOT_SET = 'not-set';
protected string $type = 'acl';
protected string $defaultAclType = 'recordAllTeamOwnNo';
/**
@@ -111,21 +106,14 @@ class DefaultTable implements Table
*/
private $valuePermissionList = [];
private RoleListProvider $roleListProvider;
protected User $user;
protected Metadata $metadata;
public function __construct(
RoleListProvider $roleListProvider,
private RoleListProvider $roleListProvider,
CacheKeyProvider $cacheKeyProvider,
User $user,
protected User $user,
Config $config,
Metadata $metadata,
protected Metadata $metadata,
DataCache $dataCache
) {
$this->roleListProvider = $roleListProvider;
$this->data = (object) [
'scopes' => (object) [],
@@ -133,9 +121,6 @@ class DefaultTable implements Table
'permissions' => (object) [],
];
$this->user = $user;
$this->metadata = $metadata;
if (!$this->user->isFetched()) {
throw new RuntimeException('User must be fetched before ACL check.');
}
+48 -56
View File
@@ -30,61 +30,61 @@
namespace Espo\Core;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use Espo\Core\{
Acl,
Acl\GlobalRestriction,
Acl\OwnerUserFieldProvider,
Acl\Table\TableFactory,
Acl\Table,
Acl\Map\Map,
Acl\Map\MapFactory,
Acl\OwnershipChecker\OwnershipCheckerFactory,
Acl\OwnershipChecker,
Acl\OwnershipOwnChecker,
Acl\OwnershipTeamChecker,
Acl\AccessChecker\AccessCheckerFactory,
Acl\AccessChecker,
Acl\AccessCreateChecker,
Acl\AccessReadChecker,
Acl\AccessEditChecker,
Acl\AccessDeleteChecker,
Acl\AccessStreamChecker,
Acl\AccessEntityCreateChecker,
Acl\AccessEntityReadChecker,
Acl\AccessEntityEditChecker,
Acl\AccessEntityDeleteChecker,
Acl\AccessEntityStreamChecker,
Acl\Exceptions\NotImplemented,
};
use Espo\Core\Acl\AccessChecker;
use Espo\Core\Acl\AccessChecker\AccessCheckerFactory;
use Espo\Core\Acl\AccessCreateChecker;
use Espo\Core\Acl\AccessDeleteChecker;
use Espo\Core\Acl\AccessEditChecker;
use Espo\Core\Acl\AccessEntityCreateChecker;
use Espo\Core\Acl\AccessEntityDeleteChecker;
use Espo\Core\Acl\AccessEntityEditChecker;
use Espo\Core\Acl\AccessEntityReadChecker;
use Espo\Core\Acl\AccessEntityStreamChecker;
use Espo\Core\Acl\AccessReadChecker;
use Espo\Core\Acl\AccessStreamChecker;
use Espo\Core\Acl\Exceptions\NotImplemented;
use Espo\Core\Acl\GlobalRestriction;
use Espo\Core\Acl\Map\Map;
use Espo\Core\Acl\Map\MapFactory;
use Espo\Core\Acl\OwnershipChecker;
use Espo\Core\Acl\OwnershipChecker\OwnershipCheckerFactory;
use Espo\Core\Acl\OwnershipOwnChecker;
use Espo\Core\Acl\OwnershipTeamChecker;
use Espo\Core\Acl\OwnerUserFieldProvider;
use Espo\Core\Acl\Table;
use Espo\Core\Acl\Table\TableFactory;
use stdClass;
use InvalidArgumentException;
/**
* A central access point for access checking.
*
* @todo Refactor. Replace with an interface `Espo\Core\Acl\AclManager`.
* Keep `Espo\Core\AclManager` as an extending interface for bc, bind it to the service.
* Implementation in `Espo\Core\Acl\DefaultAclManager`.
* The same for `Portal\AclManager`.
*/
class AclManager
{
protected const PERMISSION_ASSIGNMENT = 'assignment';
/** @var array<string,AccessChecker> */
/** @var array<string, AccessChecker> */
private $accessCheckerHashMap = [];
/** @var array<string,OwnershipChecker> */
/** @var array<string, OwnershipChecker> */
private $ownershipCheckerHashMap = [];
/** @var array<string,Table> */
/** @var array<string, Table> */
protected $tableHashMap = [];
/** @var array<string,Map> */
/** @var array<string, Map> */
protected $mapHashMap = [];
/** @var class-string */
protected $userAclClassName = Acl::class;
/** @var array<string,class-string<AccessChecker>> */
/** @var array<string, class-string<AccessChecker>> */
private $entityActionInterfaceMap = [
Table::ACTION_CREATE => AccessEntityCreateChecker::class,
Table::ACTION_READ => AccessEntityReadChecker::class,
@@ -92,7 +92,7 @@ class AclManager
Table::ACTION_DELETE => AccessEntityDeleteChecker::class,
Table::ACTION_STREAM => AccessEntityStreamChecker::class,
];
/** @var array<string,class-string<AccessChecker>> */
/** @var array<string, class-string<AccessChecker>> */
private $actionInterfaceMap = [
Table::ACTION_CREATE => AccessCreateChecker::class,
Table::ACTION_READ => AccessReadChecker::class,
@@ -101,37 +101,29 @@ class AclManager
Table::ACTION_STREAM => AccessStreamChecker::class,
];
/** @var AccessCheckerFactory|\Espo\Core\Portal\Acl\AccessChecker\AccessCheckerFactory */
/** @var AccessCheckerFactory|Portal\Acl\AccessChecker\AccessCheckerFactory */
protected $accessCheckerFactory;
/** @var OwnershipCheckerFactory|\Espo\Core\Portal\Acl\OwnershipChecker\OwnershipCheckerFactory */
/** @var OwnershipCheckerFactory|Portal\Acl\OwnershipChecker\OwnershipCheckerFactory */
protected $ownershipCheckerFactory;
/** @var TableFactory */
/** @var TableFactory */
private $tableFactory;
/** @var MapFactory */
/** @var MapFactory */
private $mapFactory;
/** @var GlobalRestriction */
protected $globalRestriction;
/** @var OwnerUserFieldProvider */
protected $ownerUserFieldProvider;
/** @var EntityManager */
protected $entityManager;
public function __construct(
AccessCheckerFactory $accessCheckerFactory,
OwnershipCheckerFactory $ownershipCheckerFactory,
TableFactory $tableFactory,
MapFactory $mapFactory,
GlobalRestriction $globalRestriction,
OwnerUserFieldProvider $ownerUserFieldProvider,
EntityManager $entityManager
protected GlobalRestriction $globalRestriction,
protected OwnerUserFieldProvider $ownerUserFieldProvider,
protected EntityManager $entityManager
) {
$this->accessCheckerFactory = $accessCheckerFactory;
$this->ownershipCheckerFactory = $ownershipCheckerFactory;
$this->tableFactory = $tableFactory;
$this->mapFactory = $mapFactory;
$this->globalRestriction = $globalRestriction;
$this->ownerUserFieldProvider = $ownerUserFieldProvider;
$this->entityManager = $entityManager;
}
/**
@@ -639,7 +631,7 @@ class AclManager
/**
* Get a restricted field list for a specific scope by a restriction type.
*
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
* @param GlobalRestriction::TYPE_*|array<int, GlobalRestriction::TYPE_*> $type
* @return string[]
*/
public function getScopeRestrictedFieldList(string $scope, $type): array
@@ -691,7 +683,7 @@ class AclManager
/**
* Get a restricted link list for a specific scope by a restriction type.
*
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
* @param GlobalRestriction::TYPE_*|array<int, GlobalRestriction::TYPE_*> $type
* @return string[]
*/
public function getScopeRestrictedLinkList(string $scope, $type): array
@@ -724,7 +716,7 @@ class AclManager
}
/**
* @deprecated User `checkOwnershipOwn`.
* @deprecated As of v7.0. Use `checkOwnershipOwn`.
*/
public function checkIsOwner(User $user, Entity $entity): bool
{
@@ -732,7 +724,7 @@ class AclManager
}
/**
* @deprecated User `checkOwnershipTeam`.
* @deprecated As of v7.0. Use `checkOwnershipTeam`.
*/
public function checkInTeam(User $user, Entity $entity): bool
{
@@ -740,7 +732,7 @@ class AclManager
}
/**
* @deprecated
* @deprecated As of v7.0. Access checkers not to be exposed.
*/
public function getImplementation(string $scope): object
{
+10 -14
View File
@@ -29,24 +29,20 @@
namespace Espo\Core\Action;
use Espo\Core\{
Exceptions\NotFound,
Exceptions\Forbidden,
Utils\Metadata,
InjectableFactory,
};
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
class ActionFactory
{
private Metadata $metadata;
private InjectableFactory $injectableFactory;
public function __construct(Metadata $metadata, InjectableFactory $injectableFactory)
{
$this->metadata = $metadata;
$this->injectableFactory = $injectableFactory;
}
public function __construct(private Metadata $metadata, private InjectableFactory $injectableFactory)
{}
/**
* @throws Forbidden
* @throws NotFound
*/
public function create(string $action, ?string $entityType = null): Action
{
$className = $this->getClassName($action, $entityType);
@@ -155,7 +155,7 @@ class ConvertCurrency implements Action
$ratesArray[$baseCurrency] = 1.0;
return CurrencyRates::fromArray($ratesArray, $baseCurrency);
return CurrencyRates::fromAssoc($ratesArray, $baseCurrency);
}
/**
@@ -42,7 +42,6 @@ use stdClass;
class Merge implements Action
{
public function __construct(private Acl $acl, private Merger $merger)
{}
@@ -29,48 +29,31 @@
namespace Espo\Core\Action\Actions\Merge;
use Espo\Core\{
Exceptions\Forbidden,
Exceptions\NotFound,
Action\Params,
Acl,
Acl\Table,
ORM\EntityManager,
Utils\Metadata,
Utils\ObjectUtil,
Record\ServiceContainer,
};
use Espo\Core\Acl;
use Espo\Core\Acl\Table;
use Espo\Core\Action\Params;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Record\ServiceContainer;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\ObjectUtil;
use Espo\ORM\Entity;
use Espo\Entities\{
PhoneNumber,
EmailAddress,
};
use Espo\Entities\EmailAddress;
use Espo\Entities\PhoneNumber;
use stdClass;
class Merger
{
private Acl $acl;
private Metadata $metadata;
private EntityManager $entityManager;
private ServiceContainer $serviceContainer;
public function __construct(
Acl $acl,
Metadata $metadata,
EntityManager $entityManager,
ServiceContainer $serviceContainer
) {
$this->acl = $acl;
$this->metadata = $metadata;
$this->entityManager = $entityManager;
$this->serviceContainer = $serviceContainer;
}
private Acl $acl,
private Metadata $metadata,
private EntityManager $entityManager,
private ServiceContainer $serviceContainer
) {}
/**
* @param string[] $sourceIdList
@@ -27,29 +27,24 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
namespace Espo\Core\Action\Api;
use Espo\Core\{
Exceptions\BadRequest,
Action\Service,
Api\Request,
};
use stdClass;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Action\Service;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\BadRequest;
/**
* Action framework.
* Processes actions.
*/
class Action
class PostProcess implements Action
{
private $service;
public function __construct(private Service $service)
{}
public function __construct(Service $service)
{
$this->service = $service;
}
public function postActionProcess(Request $request): stdClass
public function process(Request $request): Response
{
$body = $request->getParsedBody();
@@ -64,6 +59,6 @@ class Action
$entity = $this->service->process($entityType, $action, $id, $data);
return $entity->getValueMap();
return ResponseComposer::json($entity->getValueMap());
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ use stdClass;
class Data
{
private $data;
private stdClass $data;
private function __construct()
{
+1 -9
View File
@@ -36,16 +36,8 @@ use RuntimeException;
*/
class Params
{
private string $entityType;
private string $id;
/**
* @throws RuntimeException
*/
public function __construct(string $entityType, string $id)
public function __construct(private string $entityType, private string $id)
{
$this->entityType = $entityType;
$this->id = $id;
if (!$entityType || !$id) {
throw new RuntimeException();
+12 -26
View File
@@ -29,15 +29,13 @@
namespace Espo\Core\Action;
use Espo\Core\{
Exceptions\Forbidden,
Exceptions\ForbiddenSilent,
Exceptions\BadRequest,
Exceptions\NotFound,
Record\ServiceContainer as RecordServiceContainer,
Record\ReadParams,
Acl,
};
use Espo\Core\Acl;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\ForbiddenSilent;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Record\ReadParams;
use Espo\Core\Record\ServiceContainer as RecordServiceContainer;
use Espo\ORM\Entity;
@@ -45,21 +43,11 @@ use stdClass;
class Service
{
private $factory;
private $acl;
private $recordServiceContainer;
public function __construct(
ActionFactory $factory,
Acl $acl,
RecordServiceContainer $recordServiceContainer
) {
$this->factory = $factory;
$this->acl = $acl;
$this->recordServiceContainer = $recordServiceContainer;
}
private ActionFactory $factory,
private Acl $acl,
private RecordServiceContainer $recordServiceContainer
) {}
/**
* Perform an action.
@@ -89,8 +77,6 @@ class Service
$service = $this->recordServiceContainer->get($entityType);
$entity = $service->read($id, ReadParams::create());
return $entity;
return $service->read($id, ReadParams::create());
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
/**
* A route action.
*/
interface Action
{
/**
* Process.
*
* @param Request $request A request.
* @return Response A response. Use ResponseComposer for building.
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
* @throws Error
*/
public function process(Request $request): Response;
}
+119
View File
@@ -0,0 +1,119 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Utils\Config;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ResponseInterface as Psr7Response;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Factory\ResponseFactory;
/**
* @internal
*/
class ActionHandler implements RequestHandlerInterface
{
private const DEFAULT_CONTENT_TYPE = 'application/json';
public function __construct(
private Action $action,
private ProcessData $processData,
private Config $config
) {}
/**
* @throws BadRequest
* @throws Forbidden
* @throws Error
* @throws NotFound
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$requestWrapped = new RequestWrapper(
$request,
$this->processData->getBasePath(),
$this->processData->getRouteParams()
);
$response = $this->action->process($requestWrapped);
return $this->prepareResponse($response);
}
private function prepareResponse(Response $response): Psr7Response
{
if (!$response->hasHeader('Content-Type')) {
$response->setHeader('Content-Type', self::DEFAULT_CONTENT_TYPE);
}
if (!$response->hasHeader('Cache-Control')) {
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
}
if (!$response->hasHeader('Expires')) {
$response->setHeader('Expires', '0');
}
if (!$response->hasHeader('Last-Modified')) {
$response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
}
$response->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'));
return $response instanceof ResponseWrapper ?
$response->toPsr7() :
self::responseToPsr7($response);
}
private static function responseToPsr7(Response $response): Psr7Response
{
$psr7Response = (new ResponseFactory())->createResponse();
$statusCode = $response->getStatusCode();
$reason = $response->getReasonPhrase();
$body = $response->getBody();
$psr7Response = $psr7Response
->withStatus($statusCode, $reason)
->withBody($body);
foreach ($response->getHeaderNames() as $name) {
$psr7Response = $psr7Response->withHeader($name, $response->getHeaderAsArray($name));
}
return $psr7Response;
}
}
+12 -8
View File
@@ -69,7 +69,7 @@ class Auth
$authenticationMethod = $this->obtainAuthenticationMethodFromRequest($request);
if (!$authenticationMethod) {
list($username, $password) = $this->obtainUsernamePasswordFromRequest($request);
[$username, $password] = $this->obtainUsernamePasswordFromRequest($request);
}
$authenticationData = AuthenticationData::create()
@@ -183,7 +183,7 @@ class Auth
}
/**
* @return array{string,string}
* @return array{string, string}
* @throws BadRequest
*/
private function decodeAuthorizationString(string $string): array
@@ -195,11 +195,11 @@ class Auth
throw new BadRequest("Auth: Bad authorization string provided.");
}
/** @var array{string,string} */
/** @var array{string, string} */
return explode(':', $stringDecoded, 2);
}
protected function handleSecondStepRequired(Response $response, Result $result): void
private function handleSecondStepRequired(Response $response, Result $result): void
{
$response->setStatus(401);
$response->setHeader('X-Status-Reason', 'second-step-required');
@@ -218,7 +218,7 @@ class Auth
/**
* @throws Exception
*/
protected function handleException(Response $response, Exception $e): void
private function handleException(Response $response, Exception $e): void
{
if (
$e instanceof BadRequest ||
@@ -233,6 +233,10 @@ class Auth
$response->setStatus($e->getCode());
if ($e->getBody()) {
$response->writeBody($e->getBody());
}
$this->log->notice("Auth: " . $e->getMessage());
return;
@@ -286,13 +290,13 @@ class Auth
}
/**
* @return array{?string,?string}
* @return array{?string, ?string}
* @throws BadRequest
*/
private function obtainUsernamePasswordFromRequest(Request $request): array
{
if ($request->hasHeader(self::HEADER_ESPO_AUTHORIZATION)) {
list($username, $password) = $this->decodeAuthorizationString(
[$username, $password] = $this->decodeAuthorizationString(
$request->getHeader(self::HEADER_ESPO_AUTHORIZATION) ?? ''
);
@@ -313,7 +317,7 @@ class Auth
$request->getHeader('Redirect-Http-Espo-Cgi-Auth');
if ($cgiAuthString) {
list($username, $password) = $this->decodeAuthorizationString(substr($cgiAuthString, 6));
[$username, $password] = $this->decodeAuthorizationString(substr($cgiAuthString, 6));
return [$username, $password];
}
@@ -33,12 +33,9 @@ use Espo\Core\InjectableFactory;
class AuthBuilderFactory
{
private InjectableFactory $injectableFactory;
public function __construct(InjectableFactory $injectableFactory)
{
$this->injectableFactory = $injectableFactory;
}
public function __construct(private InjectableFactory $injectableFactory)
{}
public function create(): AuthBuilder
{
@@ -0,0 +1,91 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Utils\Config;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* @internal
*/
class ControllerActionHandler implements RequestHandlerInterface
{
public function __construct(
private string $controllerName,
private string $actionName,
private ProcessData $processData,
private ResponseWrapper $responseWrapped,
private ControllerActionProcessor $controllerActionProcessor,
private Config $config
) {}
/**
* @throws NotFound
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$requestWrapped = new RequestWrapper(
$request,
$this->processData->getBasePath(),
$this->processData->getRouteParams()
);
$this->beforeProceed();
$responseWrapped = $this->controllerActionProcessor->process(
$this->controllerName,
$this->actionName,
$requestWrapped,
$this->responseWrapped
);
$this->afterProceed($responseWrapped);
return $responseWrapped->toPsr7();
}
private function beforeProceed(): void
{
$this->responseWrapped->setHeader('Content-Type', 'application/json');
}
private function afterProceed(Response $responseWrapped): void
{
$responseWrapped
->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'))
->setHeader('Expires', '0')
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0')
->setHeader('Pragma', 'no-cache');
}
}
@@ -30,6 +30,7 @@
namespace Espo\Core\Api;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\NotFoundSilent;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\ClassFinder;
use Espo\Core\Utils\Json;
@@ -41,7 +42,7 @@ use stdClass;
/**
* Creates controller instances and processes actions.
*/
class ActionProcessor
class ControllerActionProcessor
{
public function __construct(
private InjectableFactory $injectableFactory,
@@ -55,8 +56,8 @@ class ActionProcessor
string $controllerName,
string $actionName,
Request $request,
Response $response
): void {
ResponseWrapper $response
): ResponseWrapper {
$controller = $this->createController($controllerName);
@@ -69,9 +70,7 @@ class ActionProcessor
$actionName = $controller::$defaultAction ?? 'index';
}
$actionNameUcfirst = ucfirst($actionName);
$actionMethodName = 'action' . $actionNameUcfirst;
$actionMethodName = 'action' . ucfirst($actionName);
$fullActionMethodName = strtolower($requestMethod) . ucfirst($actionMethodName);
@@ -80,7 +79,7 @@ class ActionProcessor
$actionMethodName;
if (!method_exists($controller, $primaryActionMethodName)) {
throw new NotFound(
throw new NotFoundSilent(
"Action {$requestMethod} '{$actionName}' does not exist in controller '{$controllerName}'.");
}
@@ -89,7 +88,7 @@ class ActionProcessor
$this->handleResult($response, $result);
return;
return $response;
}
// Below is a legacy way.
@@ -102,7 +101,7 @@ class ActionProcessor
$params = $request->getRouteParams();
$beforeMethodName = 'before' . $actionNameUcfirst;
$beforeMethodName = 'before' . ucfirst($actionName);
if (method_exists($controller, $beforeMethodName)) {
$controller->$beforeMethodName($params, $data, $request, $response);
@@ -110,13 +109,15 @@ class ActionProcessor
$result = $controller->$primaryActionMethodName($params, $data, $request, $response) ?? null;
$afterMethodName = 'after' . $actionNameUcfirst;
$afterMethodName = 'after' . ucfirst($actionName);
if (method_exists($controller, $afterMethodName)) {
$controller->$afterMethodName($params, $data, $request, $response);
}
$this->handleResult($response, $result);
return $response;
}
/**
@@ -0,0 +1,128 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Psr\Http\Server\MiddlewareInterface;
/**
* @internal
*/
class MiddlewareProvider
{
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory
) {}
/**
* @return MiddlewareInterface[]
*/
public function getGlobalMiddlewareList(): array
{
return $this->createFromClassNameList($this->getGlobalMiddlewareClassNameList());
}
/**
* @return MiddlewareInterface[]
*/
public function getRouteMiddlewareList(Route $route): array
{
$key = strtolower($route->getMethod()) . '_' . $route->getRoute();
/** @var class-string<MiddlewareInterface>[] $classNameList */
$classNameList = $this->metadata->get(['app', 'api', 'routeMiddlewareClassNameListMap', $key]) ?? [];
return $this->createFromClassNameList($classNameList);
}
/**
* @return MiddlewareInterface[]
*/
public function getActionMiddlewareList(Route $route): array
{
$key = strtolower($route->getMethod()) . '_' . $route->getRoute();
/** @var class-string<MiddlewareInterface>[] $classNameList */
$classNameList = $this->metadata->get(['app', 'api', 'actionMiddlewareClassNameListMap', $key]) ?? [];
return $this->createFromClassNameList($classNameList);
}
/**
* @return MiddlewareInterface[]
*/
public function getControllerMiddlewareList(string $controller): array
{
/** @var class-string<MiddlewareInterface>[] $classNameList */
$classNameList = $this->metadata
->get(['app', 'api', 'controllerMiddlewareClassNameListMap', $controller]) ?? [];
return $this->createFromClassNameList($classNameList);
}
/**
* @return MiddlewareInterface[]
*/
public function getControllerActionMiddlewareList(string $method, string $controller, string $action): array
{
$key = $controller . '_' . strtolower($method) . '_' . $action;
/** @var class-string<MiddlewareInterface>[] $classNameList */
$classNameList = $this->metadata
->get(['app', 'api', 'controllerActionMiddlewareClassNameListMap', $key]) ?? [];
return $this->createFromClassNameList($classNameList);
}
/**
* @return class-string<MiddlewareInterface>[]
*/
private function getGlobalMiddlewareClassNameList(): array
{
return $this->metadata->get(['app', 'api', 'globalMiddlewareClassNameList']) ?? [];
}
/**
* @param class-string<MiddlewareInterface>[] $classNameList
* @return MiddlewareInterface[]
*/
private function createFromClassNameList(array $classNameList): array
{
$list = [];
foreach ($classNameList as $className) {
$list[] = $this->injectableFactory->create($className);
}
return $list;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
class ProcessData
{
/**
* @param array<string, string> $routeParams
*/
public function __construct(
private Route $route,
private string $basePath,
private array $routeParams
) {}
/**
* @return Route
*/
public function getRoute(): Route
{
return $this->route;
}
/**
* @return string
*/
public function getBasePath(): string
{
return $this->basePath;
}
/**
* @return array<string, string>
*/
public function getRouteParams(): array
{
return $this->routeParams;
}
}
@@ -1,171 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Authentication\AuthenticationFactory;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
use Espo\Core\ApplicationUser;
use Exception;
use Throwable;
use LogicException;
/**
* Processes requests. Handles authentication. Obtains a controller name, action, body from a request.
* Then passes them to the action processor.
*/
class RequestProcessor
{
public function __construct(
private AuthenticationFactory $authenticationFactory,
private ActionProcessor $actionProcessor,
private AuthBuilderFactory $authBuilderFactory,
private ErrorOutput $errorOutput,
private Config $config,
private Log $log,
private ApplicationUser $applicationUser
) {}
public function process(Route $route, Request $request, Response $response): void
{
try {
$this->processInternal($route, $request, $response);
}
catch (Exception $exception) {
$this->handleException($exception, $request, $response, $route->getRoute());
}
}
/**
* @throws BadRequest
* @throws NotFound
*/
private function processInternal(Route $route, Request $request, Response $response): void
{
$authRequired = !$route->noAuth();
$apiAuth = $this->authBuilderFactory
->create()
->setAuthentication($this->authenticationFactory->create())
->setAuthRequired($authRequired)
->build();
$authResult = $apiAuth->process($request, $response);
if (!$authResult->isResolved()) {
return;
}
if ($authResult->isResolvedUseNoAuth()) {
$this->applicationUser->setupSystemUser();
}
ob_start();
$this->proceed($request, $response);
ob_clean();
}
/**
* @throws NotFound
* @throws BadRequest
*/
private function proceed(Request $request, Response $response): void
{
$this->beforeProceed($response);
$controllerName = $this->getControllerName($request);
$actionName = $request->getRouteParam('action');
$requestMethod = $request->getMethod();
if (!$actionName) {
$httpMethod = strtolower($requestMethod);
$crudList = $this->config->get('crud') ?? [];
$actionName = $crudList[$httpMethod] ?? null;
if (!$actionName) {
throw new BadRequest("No action for method {$httpMethod}.");
}
}
$this->actionProcessor->process($controllerName, $actionName, $request, $response);
$this->afterProceed($response);
}
private function getControllerName(Request $request): string
{
$controllerName = $request->getRouteParam('controller');
if (!$controllerName) {
throw new LogicException("Route doesn't have specified controller.");
}
return ucfirst($controllerName);
}
private function handleException(
Exception $exception,
Request $request,
Response $response,
string $route
): void {
try {
$this->errorOutput->process($request, $response, $exception, $route);
}
catch (Throwable $exceptionAnother) {
$this->log->error($exceptionAnother->getMessage());
$response->setStatus(500);
}
}
private function beforeProceed(Response $response): void
{
$response->setHeader('Content-Type', 'application/json');
}
private function afterProceed(Response $response): void
{
$response
->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'))
->setHeader('Expires', '0')
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0')
->setHeader('Pragma', 'no-cache');
}
}
+29 -37
View File
@@ -31,14 +31,11 @@ namespace Espo\Core\Api;
use Espo\Core\Utils\Json;
use Espo\Core\Exceptions\BadRequest;
use Psr\Http\Message\{
ServerRequestInterface as Psr7Request,
UriInterface,
};
use Espo\Core\Api\Request as ApiRequest;
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
use Psr\Http\Message\UriInterface;
use stdClass;
/**
@@ -46,27 +43,22 @@ use stdClass;
*/
class RequestWrapper implements ApiRequest
{
private Psr7Request $request;
private string $basePath;
private ?stdClass $parsedBody = null;
/** @var array<string,string> */
private array $routeParams;
/**
* @param array<string,string> $routeParams
* @param array<string, string> $routeParams
*/
public function __construct(Psr7Request $request, string $basePath = '', array $routeParams = [])
{
$this->request = $request;
$this->basePath = $basePath;
$this->routeParams = $routeParams;
}
public function __construct(
private Psr7Request $psr7Request,
private string $basePath = '',
private array $routeParams = []
) {}
/**
* Get a route or query parameter. Route params have a higher priority.
*
* @todo Don't support NULL $name.
* @deprecated For backward compatibility.
* @deprecated As of v6.0. Use getQueryParam & getRouteParam. Left for backward compatibility.
*
* @return mixed
*/
@@ -83,7 +75,7 @@ class RequestWrapper implements ApiRequest
return $this->getRouteParam($name);
}
return $this->request->getQueryParams()[$name] ?? null;
return $this->psr7Request->getQueryParams()[$name] ?? null;
}
public function hasRouteParam(string $name): bool
@@ -97,7 +89,7 @@ class RequestWrapper implements ApiRequest
}
/**
* @return array<string,string>
* @return array<string, string>
*/
public function getRouteParams(): array
{
@@ -106,12 +98,12 @@ class RequestWrapper implements ApiRequest
public function hasQueryParam(string $name): bool
{
return array_key_exists($name, $this->request->getQueryParams());
return array_key_exists($name, $this->psr7Request->getQueryParams());
}
public function getQueryParam(string $name): ?string
{
$value = $this->request->getQueryParams()[$name] ?? null;
$value = $this->psr7Request->getQueryParams()[$name] ?? null;
if (!is_string($value)) {
return null;
@@ -122,21 +114,21 @@ class RequestWrapper implements ApiRequest
public function getQueryParams(): array
{
return $this->request->getQueryParams();
return $this->psr7Request->getQueryParams();
}
public function getHeader(string $name): ?string
{
if (!$this->request->hasHeader($name)) {
if (!$this->psr7Request->hasHeader($name)) {
return null;
}
return $this->request->getHeaderLine($name);
return $this->psr7Request->getHeaderLine($name);
}
public function hasHeader(string $name): bool
{
return $this->request->hasHeader($name);
return $this->psr7Request->hasHeader($name);
}
/**
@@ -144,16 +136,16 @@ class RequestWrapper implements ApiRequest
*/
public function getHeaderAsArray(string $name): array
{
if (!$this->request->hasHeader($name)) {
if (!$this->psr7Request->hasHeader($name)) {
return [];
}
return $this->request->getHeader($name);
return $this->psr7Request->getHeader($name);
}
public function getMethod(): string
{
return $this->request->getMethod();
return $this->psr7Request->getMethod();
}
public function getContentType(): ?string
@@ -164,7 +156,7 @@ class RequestWrapper implements ApiRequest
$contentType = explode(
';',
$this->request->getHeader('Content-Type')[0]
$this->psr7Request->getHeader('Content-Type')[0]
)[0];
return strtolower($contentType);
@@ -172,9 +164,9 @@ class RequestWrapper implements ApiRequest
public function getBodyContents(): ?string
{
$contents = $this->request->getBody()->getContents();
$contents = $this->psr7Request->getBody()->getContents();
$this->request->getBody()->rewind();
$this->psr7Request->getBody()->rewind();
return $contents;
}
@@ -226,7 +218,7 @@ class RequestWrapper implements ApiRequest
in_array($contentType, ['application/x-www-form-urlencoded', 'multipart/form-data']) &&
$contents
) {
$parsedBody = $this->request->getParsedBody();
$parsedBody = $this->psr7Request->getParsedBody();
if (is_array($parsedBody)) {
$this->parsedBody = (object) $parsedBody;
@@ -246,7 +238,7 @@ class RequestWrapper implements ApiRequest
public function getCookieParam(string $name): ?string
{
$params = $this->request->getCookieParams();
$params = $this->psr7Request->getCookieParams();
return $params[$name] ?? null;
}
@@ -256,19 +248,19 @@ class RequestWrapper implements ApiRequest
*/
public function getServerParam(string $name)
{
$params = $this->request->getServerParams();
$params = $this->psr7Request->getServerParams();
return $params[$name] ?? null;
}
public function getUri(): UriInterface
{
return $this->request->getUri();
return $this->psr7Request->getUri();
}
public function getResourcePath(): string
{
$path = $this->request->getUri()->getPath();
$path = $this->psr7Request->getUri()->getPath();
return substr($path, strlen($this->basePath));
}
+22
View File
@@ -36,6 +36,16 @@ use Psr\Http\Message\StreamInterface;
*/
interface Response
{
/**
* Get a status code.
*/
public function getStatusCode(): int;
/**
* Get a status reason phrase.
*/
public function getReasonPhrase(): string;
/**
* Set a status code.
*/
@@ -61,6 +71,13 @@ interface Response
*/
public function hasHeader(string $name): bool;
/**
* Get all set header names.
*
* @return string[]
*/
public function getHeaderNames(): array;
/**
* Get a header values as an array.
*
@@ -77,4 +94,9 @@ interface Response
* Set a body.
*/
public function setBody(StreamInterface $body): self;
/**
* Get a body.
*/
public function getBody(): StreamInterface;
}
@@ -0,0 +1,59 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Slim\Psr7\Factory\ResponseFactory;
use Espo\Core\Utils\Json;
use stdClass;
class ResponseComposer
{
/**
* Compose a JSON response.
*
* @param array<string|int, mixed>|stdClass|scalar|null $data A data to encode.
*/
public static function json(mixed $data): Response
{
return self::empty()
->writeBody(Json::encode($data))
->setHeader('Content-Type', 'application/json');
}
/**
* Compose an empty response.
*/
public static function empty(): Response
{
$psr7Response = (new ResponseFactory())->createResponse();
return new ResponseWrapper($psr7Response);
}
}
+39 -22
View File
@@ -29,10 +29,8 @@
namespace Espo\Core\Api;
use Psr\Http\Message\{
ResponseInterface as Psr7Response,
StreamInterface,
};
use Psr\Http\Message\ResponseInterface as Psr7Response;
use Psr\Http\Message\StreamInterface;
use Espo\Core\Api\Response as ApiResponse;
@@ -41,49 +39,55 @@ use Espo\Core\Api\Response as ApiResponse;
*/
class ResponseWrapper implements ApiResponse
{
private Psr7Response $response;
public function __construct(Psr7Response $response)
public function __construct(private Psr7Response $psr7Response)
{
$this->response = $response;
// Slim adds Authorization header. It's not needed.
$this->response = $this->response->withoutHeader('Authorization');
$this->psr7Response = $this->psr7Response->withoutHeader('Authorization');
}
public function setStatus(int $code, ?string $reason = null): Response
{
$this->response = $this->response->withStatus($code, $reason ?? '');
$this->psr7Response = $this->psr7Response->withStatus($code, $reason ?? '');
return $this;
}
public function getStatusCode(): int
{
return $this->psr7Response->getStatusCode();
}
public function getReasonPhrase(): string
{
return $this->psr7Response->getReasonPhrase();
}
public function setHeader(string $name, string $value): Response
{
$this->response = $this->response->withHeader($name, $value);
$this->psr7Response = $this->psr7Response->withHeader($name, $value);
return $this;
}
public function addHeader(string $name, string $value): Response
{
$this->response = $this->response->withAddedHeader($name, $value);
$this->psr7Response = $this->psr7Response->withAddedHeader($name, $value);
return $this;
}
public function getHeader(string $name): ?string
{
if (!$this->response->hasHeader($name)) {
if (!$this->psr7Response->hasHeader($name)) {
return null;
}
return $this->response->getHeaderLine($name);
return $this->psr7Response->getHeaderLine($name);
}
public function hasHeader(string $name): bool
{
return $this->response->hasHeader($name);
return $this->psr7Response->hasHeader($name);
}
/**
@@ -91,29 +95,42 @@ class ResponseWrapper implements ApiResponse
*/
public function getHeaderAsArray(string $name): array
{
if (!$this->response->hasHeader($name)) {
if (!$this->psr7Response->hasHeader($name)) {
return [];
}
return $this->response->getHeader($name);
return $this->psr7Response->getHeader($name);
}
/**
* @return string[]
*/
public function getHeaderNames(): array
{
return array_keys($this->psr7Response->getHeaders());
}
public function writeBody(string $string): Response
{
$this->response->getBody()->write($string);
$this->psr7Response->getBody()->write($string);
return $this;
}
public function setBody(StreamInterface $body): Response
{
$this->response = $this->response->withBody($body);
$this->psr7Response = $this->psr7Response->withBody($body);
return $this;
}
public function getResponse(): Psr7Response
public function getBody(): StreamInterface
{
return $this->response;
return $this->psr7Response->getBody();
}
public function toPsr7(): Psr7Response
{
return $this->psr7Response;
}
}
+26 -11
View File
@@ -32,24 +32,28 @@ namespace Espo\Core\Api;
class Route
{
private string $method;
private string $route;
/** @var array<string, string> */
private array $params;
private bool $noAuth;
/**
* @param array<string, string> $params
* @param ?class-string<Action> $actionClassName
*/
public function __construct(
string $method,
string $route,
array $params,
bool $noAuth
private string $route,
private string $adjustedRoute,
private array $params,
private bool $noAuth,
private ?string $actionClassName
) {
$this->method = strtoupper($method);
$this->route = $route;
$this->params = $params;
$this->noAuth = $noAuth;
}
/**
* @return ?class-string<Action>
*/
public function getActionClassName(): ?string
{
return $this->actionClassName;
}
public function getMethod(): string
@@ -57,13 +61,24 @@ class Route
return $this->method;
}
/**
* Get a route.
*/
public function getRoute(): string
{
return $this->route;
}
/**
* @return array<string,string>
* Get an adjusted route for FastRoute.
*/
public function getAdjustedRoute(): string
{
return $this->adjustedRoute;
}
/**
* @return array<string, string>
*/
public function getParams(): array
{
@@ -34,8 +34,8 @@ use Espo\Core\Api\Route;
class RouteParamsFetcher
{
/**
* @param array<string,mixed> $args
* @return array<string,mixed>
* @param array<string, mixed> $args
* @return array<string, mixed>
*/
public function fetch(Route $item, array $args): array
{
@@ -0,0 +1,274 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Authentication\AuthenticationFactory;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
use Espo\Core\ApplicationUser;
use Psr\Http\Message\ResponseInterface as Psr7Response;
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
use Slim\MiddlewareDispatcher;
use Throwable;
use LogicException;
use Exception;
/**
* Processes routes. Handles authentication. Obtains a controller name, action, body from a request.
* Then processes a controller action or an action.
*
* @internal
*/
class RouteProcessor
{
public function __construct(
private AuthenticationFactory $authenticationFactory,
private AuthBuilderFactory $authBuilderFactory,
private ErrorOutput $errorOutput,
private Config $config,
private Log $log,
private ApplicationUser $applicationUser,
private ControllerActionProcessor $actionProcessor,
private MiddlewareProvider $middlewareProvider,
private InjectableFactory $injectableFactory
) {}
public function process(
ProcessData $processData,
Psr7Request $request,
Psr7Response $response
): Psr7Response {
$requestWrapped = new RequestWrapper($request, $processData->getBasePath(), $processData->getRouteParams());
$responseWrapped = new ResponseWrapper($response);
try {
return $this->processInternal(
$processData,
$request,
$requestWrapped,
$responseWrapped
);
}
catch (Exception $exception) {
$this->handleException(
$exception,
$requestWrapped,
$responseWrapped,
$processData->getRoute()->getAdjustedRoute()
);
return $responseWrapped->toPsr7();
}
}
/**
* @throws BadRequest
*/
private function processInternal(
ProcessData $processData,
Psr7Request $psrRequest,
RequestWrapper $request,
ResponseWrapper $response
): Psr7Response {
$authRequired = !$processData->getRoute()->noAuth();
$apiAuth = $this->authBuilderFactory
->create()
->setAuthentication($this->authenticationFactory->create())
->setAuthRequired($authRequired)
->build();
$authResult = $apiAuth->process($request, $response);
if (!$authResult->isResolved()) {
return $response->toPsr7();
}
if ($authResult->isResolvedUseNoAuth()) {
$this->applicationUser->setupSystemUser();
}
ob_start();
$response = $this->processAfterAuth($processData, $psrRequest, $response);
ob_clean();
return $response;
}
/**
* @throws BadRequest
*/
private function processAfterAuth(
ProcessData $processData,
Psr7Request $request,
ResponseWrapper $responseWrapped
): Psr7Response {
$actionClassName = $processData->getRoute()->getActionClassName();
if ($actionClassName) {
return $this->processAction($actionClassName, $processData, $request, $responseWrapped);
}
return $this->processControllerAction($processData, $request, $responseWrapped);
}
/**
* @param class-string<Action> $actionClassName
*/
private function processAction(
string $actionClassName,
ProcessData $processData,
Psr7Request $request,
ResponseWrapper $responseWrapped
): Psr7Response {
/** @var Action $action */
$action = $this->injectableFactory->create($actionClassName);
$handler = new ActionHandler(
action: $action,
processData: $processData,
config: $this->config,
);
$dispatcher = new MiddlewareDispatcher($handler);
foreach ($this->middlewareProvider->getActionMiddlewareList($processData->getRoute()) as $middleware) {
$dispatcher->addMiddleware($middleware);
}
$response = $dispatcher->handle($request);
// Apply headers added by the authentication.
foreach ($responseWrapped->getHeaderNames() as $name) {
$response = $response->withHeader($name, $responseWrapped->getHeaderAsArray($name));
}
return $response;
}
/**
* @throws BadRequest
*/
private function processControllerAction(
ProcessData $processData,
Psr7Request $request,
ResponseWrapper $responseWrapped
): Psr7Response {
$controller = $this->getControllerName($processData);
$action = $processData->getRouteParams()['action'] ?? null;
$method = $request->getMethod();
if (!$action) {
$crudMethodActionMap = $this->config->get('crud') ?? [];
$action = $crudMethodActionMap[strtolower($method)] ?? null;
if (!$action) {
throw new BadRequest("No action for method `{$method}`.");
}
}
$handler = new ControllerActionHandler(
controllerName: $controller,
actionName: $action,
processData: $processData,
responseWrapped: $responseWrapped,
controllerActionProcessor: $this->actionProcessor,
config: $this->config,
);
$dispatcher = new MiddlewareDispatcher($handler);
$this->addControllerMiddlewares($dispatcher, $method, $controller, $action);
return $dispatcher->handle($request);
}
private function getControllerName(ProcessData $processData): string
{
$controllerName = $processData->getRouteParams()['controller'] ?? null;
if (!$controllerName) {
throw new LogicException("Route doesn't have specified controller.");
}
return ucfirst($controllerName);
}
private function handleException(
Exception $exception,
Request $request,
Response $response,
string $route
): void {
try {
$this->errorOutput->process($request, $response, $exception, $route);
}
catch (Throwable $exceptionAnother) {
$this->log->error($exceptionAnother->getMessage());
$response->setStatus(500);
}
}
private function addControllerMiddlewares(
MiddlewareDispatcher $dispatcher,
string $method,
string $controller,
string $action
): void {
$controllerActionMiddlewareList = $this->middlewareProvider
->getControllerActionMiddlewareList($method, $controller, $action);
foreach ($controllerActionMiddlewareList as $middleware) {
$dispatcher->addMiddleware($middleware);
}
$controllerMiddlewareList = $this->middlewareProvider
->getControllerMiddlewareList($controller);
foreach ($controllerMiddlewareList as $middleware) {
$dispatcher->addMiddleware($middleware);
}
}
}
+39 -12
View File
@@ -30,6 +30,7 @@
namespace Espo\Core\Api;
use Espo\Core\Api\Route\RouteParamsFetcher;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Route as RouteUtil;
use Espo\Core\Utils\Log;
@@ -44,24 +45,44 @@ use Psr\Http\Message\ServerRequestInterface as Psr7Request;
*/
class Starter
{
private string $routeCacheFile = 'data/cache/application/slim-routes.php';
public function __construct(
private RequestProcessor $requestProcessor,
private RouteProcessor $routeProcessor,
private RouteUtil $routeUtil,
private RouteParamsFetcher $routeParamsFetcher,
private Log $log
) {}
private MiddlewareProvider $middlewareProvider,
private Log $log,
private Config $config,
?string $routeCacheFile = null
) {
$this->routeCacheFile = $routeCacheFile ?? $this->routeCacheFile;
}
public function start(): void
{
$slim = SlimAppFactory::create();
if ($this->config->get('useCache')) {
$slim->getRouteCollector()->setCacheFile($this->routeCacheFile);
}
$slim->setBasePath(RouteUtil::detectBasePath());
$this->addGlobalMiddlewares($slim);
$slim->addRoutingMiddleware();
$this->addRoutes($slim);
$slim->addErrorMiddleware(false, true, true, $this->log);
$slim->run();
}
private function addGlobalMiddlewares(SlimApp $slim): void
{
foreach ($this->middlewareProvider->getGlobalMiddlewareList() as $middleware) {
$slim->add($middleware);
}
}
private function addRoutes(SlimApp $slim): void
{
$routeList = $this->routeUtil->getFullList();
@@ -73,20 +94,26 @@ class Starter
private function addRoute(SlimApp $slim, Route $item): void
{
$slim->map(
$slimRoute = $slim->map(
[$item->getMethod()],
$item->getRoute(),
function (Psr7Request $request, Psr7Response $response, array $args) use ($slim, $item)
{
$item->getAdjustedRoute(),
function (Psr7Request $request, Psr7Response $response, array $args) use ($slim, $item) {
$routeParams = $this->routeParamsFetcher->fetch($item, $args);
$requestWrapped = new RequestWrapper($request, $slim->getBasePath(), $routeParams);
$responseWrapped = new ResponseWrapper($response);
$processData = new ProcessData(
route: $item,
basePath: $slim->getBasePath(),
routeParams: $routeParams,
);
$this->requestProcessor->process($item, $requestWrapped, $responseWrapped);
return $responseWrapped->getResponse();
return $this->routeProcessor->process($processData, $request, $response);
}
);
$middlewareList = $this->middlewareProvider->getRouteMiddlewareList($item);
foreach ($middlewareList as $middleware) {
$slimRoute->addMiddleware($middleware);
}
}
}
+5 -10
View File
@@ -100,32 +100,27 @@ class Application
protected function getInjectableFactory(): InjectableFactory
{
/** @var InjectableFactory */
return $this->container->get('injectableFactory');
return $this->container->getByClass(InjectableFactory::class);
}
protected function getApplicationUser(): ApplicationUser
{
/** @var ApplicationUser */
return $this->container->get('applicationUser');
return $this->container->getByClass(ApplicationUser::class);
}
protected function getClientManager(): ClientManager
{
/** @var ClientManager */
return $this->container->get('clientManager');
return $this->container->getByClass(ClientManager::class);
}
protected function getMetadata(): Metadata
{
/** @var Metadata */
return $this->container->get('metadata');
return $this->container->getByClass(Metadata::class);
}
protected function getConfig(): Config
{
/** @var Config */
return $this->container->get('config');
return $this->container->getByClass(Config::class);
}
protected function initAutoloads(): void
@@ -100,6 +100,6 @@ class PortalClient implements RunnerParameterized
{
$this->errorOutput->processWithBodyPrinting($request, $response, $exception);
(new ResponseEmitter())->emit($response->getResponse());
(new ResponseEmitter())->emit($response->toPsr7());
}
}
@@ -31,6 +31,7 @@ namespace Espo\Core\ApplicationRunners;
use Espo\Core\Application\Runner;
use Espo\Core\DataManager;
use Espo\Core\Utils\Log;
use Exception;
/**
@@ -40,7 +41,7 @@ class Rebuild implements Runner
{
use Cli;
public function __construct(private DataManager $dataManager)
public function __construct(private DataManager $dataManager, private Log $log)
{}
public function run(): void
@@ -51,6 +52,11 @@ class Rebuild implements Runner
catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
$this->log->error('Rebuild: ' . $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
exit(1);
}
}
+9 -11
View File
@@ -29,7 +29,6 @@
namespace Espo\Core;
use Espo\Core\Exceptions\Error;
use Espo\Entities\Portal as PortalEntity;
use Espo\Entities\User as UserEntity;
@@ -40,19 +39,18 @@ use LogicException;
*/
class ApplicationState
{
private Container $container;
private const KEY_USER = 'user';
private const KEY_PORTAL = 'portal';
public function __construct(Container $container)
{
$this->container = $container;
}
public function __construct(private Container $container)
{}
/**
* Whether an application is initialized as a portal.
*/
public function isPortal(): bool
{
return $this->container->has('portal');
return $this->container->has(self::KEY_PORTAL);
}
/**
@@ -77,7 +75,7 @@ class ApplicationState
}
/** @var PortalEntity */
return $this->container->get('portal');
return $this->container->get(self::KEY_PORTAL);
}
/**
@@ -85,7 +83,7 @@ class ApplicationState
*/
public function hasUser(): bool
{
return $this->container->has('user');
return $this->container->has(self::KEY_USER);
}
/**
@@ -98,7 +96,7 @@ class ApplicationState
}
/** @var UserEntity */
return $this->container->get('user');
return $this->container->get(self::KEY_USER);
}
/**
@@ -114,7 +112,7 @@ class ApplicationState
*/
public function isLogged(): bool
{
if (!$this->container->has('user')) {
if (!$this->container->has(self::KEY_USER)) {
return false;
}
+7 -9
View File
@@ -29,6 +29,7 @@
namespace Espo\Core;
use Espo\Core\Utils\SystemUser;
use Espo\Entities\User;
use Espo\Core\ORM\EntityManagerProxy;
@@ -39,16 +40,13 @@ use RuntimeException;
*/
class ApplicationUser
{
/** @deprecated As of v7.4. Different IDs may be used. Use Espo\Core\Utils\SystemUser. */
public const SYSTEM_USER_ID = 'system';
private Container $container;
private EntityManagerProxy $entityManagerProxy;
public function __construct(Container $container, EntityManagerProxy $entityManagerProxy)
{
$this->container = $container;
$this->entityManagerProxy = $entityManagerProxy;
}
public function __construct(
private Container $container,
private EntityManagerProxy $entityManagerProxy
) {}
/**
* Set up the system user as a current user. The system user is used when no user is logged in.
@@ -67,7 +65,7 @@ class ApplicationUser
'lastName',
'deleted',
])
->where(['id' => self::SYSTEM_USER_ID])
->where(['userName' => SystemUser::NAME])
->findOne();
if (!$user) {
@@ -45,8 +45,7 @@ class Data
private bool $createSecret = false;
private function __construct()
{
}
{}
public function getUserId(): string
{
@@ -74,7 +73,7 @@ class Data
}
/**
* @param array<string,mixed> $data
* @param array<string, mixed> $data
*/
public static function create(array $data): self
{
@@ -35,9 +35,15 @@ use Espo\Entities\AuthToken as AuthTokenEntity;
use RuntimeException;
/**
* A default auth token manager. Auth tokens are stored in database.
* Consider creating a custom implementation if you need to store auth tokens
* in another storage. E.g. a single Redis data store can be utilized with
* multiple Espo replicas (for scalability purposes).
* Defined at metadata > app > containerServices > authTokenManager.
*/
class EspoManager implements Manager
{
private EntityManager $entityManager;
/** @var RDBRepository<AuthTokenEntity> */
private RDBRepository $repository;
@@ -45,19 +51,13 @@ class EspoManager implements Manager
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
/** @var RDBRepository<AuthTokenEntity> $repository */
$repository = $entityManager->getRDBRepository(AuthTokenEntity::ENTITY_TYPE);
$this->repository = $repository;
$this->repository = $entityManager->getRDBRepositoryByClass(AuthTokenEntity::class);
}
public function get(string $token): ?AuthToken
{
/** @var ?AuthTokenEntity $authToken */
$authToken = $this->entityManager
->getRDBRepository(AuthTokenEntity::ENTITY_TYPE)
$authToken = $this->repository
->select([
'id',
'isActive',
@@ -70,9 +70,7 @@ class EspoManager implements Manager
'lastAccess',
'modifiedAt',
])
->where([
'token' => $token,
])
->where(['token' => $token])
->findOne();
return $authToken;
@@ -83,17 +81,16 @@ class EspoManager implements Manager
/** @var AuthTokenEntity $authToken */
$authToken = $this->repository->getNew();
$authToken->set([
'userId' => $data->getUserId(),
'portalId' => $data->getPortalId(),
'hash' => $data->getHash(),
'ipAddress' => $data->getIpAddress(),
'lastAccess' => date('Y-m-d H:i:s'),
'token' => $this->generateToken(),
]);
$authToken
->setUserId($data->getUserId())
->setPortalId($data->getPortalId())
->setHash($data->getHash())
->setIpAddress($data->getIpAddress())
->setToken($this->generateToken())
->setLastAccessNow();
if ($data->toCreateSecret()) {
$authToken->set('secret', $this->generateToken());
$authToken->setSecret($this->generateToken());
}
$this->validate($authToken);
@@ -111,7 +108,7 @@ class EspoManager implements Manager
$this->validateNotChanged($authToken);
$authToken->set('isActive', false);
$authToken->setIsActive(false);
$this->repository->save($authToken);
}
@@ -128,12 +125,12 @@ class EspoManager implements Manager
throw new RuntimeException("Can renew only not new auth token.");
}
$authToken->set('lastAccess', date('Y-m-d H:i:s'));
$authToken->setLastAccessNow();
$this->repository->save($authToken);
}
protected function validate(AuthToken $authToken): void
private function validate(AuthToken $authToken): void
{
if (!$authToken->getToken()) {
throw new RuntimeException("Empty token.");
@@ -144,7 +141,7 @@ class EspoManager implements Manager
}
}
protected function validateNotChanged(AuthTokenEntity $authToken): void
private function validateNotChanged(AuthTokenEntity $authToken): void
{
if (
$authToken->isAttributeChanged('token') ||
@@ -157,7 +154,7 @@ class EspoManager implements Manager
}
}
protected function generateToken(): string
private function generateToken(): string
{
$length = self::TOKEN_RANDOM_LENGTH;
@@ -29,7 +29,8 @@
namespace Espo\Core\Authentication;
use Espo\Core\Authentication\Logout\Params as LogoutParams;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Repositories\UserData as UserDataRepository;
use Espo\Entities\Portal;
use Espo\Entities\User;
@@ -37,6 +38,9 @@ use Espo\Entities\AuthLogRecord;
use Espo\Entities\AuthToken as AuthTokenEntity;
use Espo\Entities\UserData;
use Espo\Core\Exceptions\Error\Body;
use Espo\Core\Authentication\Logout\Params as LogoutParams;
use Espo\Core\Authentication\Util\MethodProvider;
use Espo\Core\Authentication\Result\FailReason;
use Espo\Core\Authentication\TwoFactor\LoginFactory as TwoFactorLoginFactory;
use Espo\Core\Authentication\AuthToken\Manager as AuthTokenManager;
@@ -44,7 +48,6 @@ use Espo\Core\Authentication\AuthToken\Data as AuthTokenData;
use Espo\Core\Authentication\AuthToken\AuthToken;
use Espo\Core\Authentication\Hook\Manager as HookManager;
use Espo\Core\Authentication\Login\Data as LoginData;
use Espo\Core\ApplicationUser;
use Espo\Core\ApplicationState;
use Espo\Core\Api\Request;
@@ -53,6 +56,7 @@ use Espo\Core\Utils\Log;
use Espo\Core\ORM\EntityManagerProxy;
use Espo\Core\Exceptions\ServiceUnavailable;
use LogicException;
use RuntimeException;
/**
@@ -70,50 +74,22 @@ class Authentication
private const COOKIE_AUTH_TOKEN_SECRET = 'auth-token-secret';
private bool $allowAnyAccess;
private ?Portal $portal = null;
private ApplicationUser $applicationUser;
private ApplicationState $applicationState;
private ConfigDataProvider $configDataProvider;
private EntityManagerProxy $entityManager;
private LoginFactory $loginFactory;
private TwoFactorLoginFactory $twoFactorLoginFactory;
private AuthTokenManager $authTokenManager;
private HookManager $hookManager;
private LogoutFactory $logoutFactory;
private Log $log;
public function __construct(
ApplicationUser $applicationUser,
ApplicationState $applicationState,
ConfigDataProvider $configDataProvider,
EntityManagerProxy $entityManagerProxy,
LoginFactory $loginFactory,
TwoFactorLoginFactory $twoFactorLoginFactory,
AuthTokenManager $authTokenManager,
HookManager $hookManager,
Log $log,
LogoutFactory $logoutFactory,
bool $allowAnyAccess = false
) {
$this->allowAnyAccess = $allowAnyAccess;
$this->applicationUser = $applicationUser;
$this->applicationState = $applicationState;
$this->configDataProvider = $configDataProvider;
$this->entityManager = $entityManagerProxy;
$this->loginFactory = $loginFactory;
$this->twoFactorLoginFactory = $twoFactorLoginFactory;
$this->authTokenManager = $authTokenManager;
$this->hookManager = $hookManager;
$this->logoutFactory = $logoutFactory;
$this->log = $log;
}
private ApplicationUser $applicationUser,
private ApplicationState $applicationState,
private ConfigDataProvider $configDataProvider,
private EntityManagerProxy $entityManager,
private LoginFactory $loginFactory,
private TwoFactorLoginFactory $twoFactorLoginFactory,
private AuthTokenManager $authTokenManager,
private HookManager $hookManager,
private Log $log,
private LogoutFactory $logoutFactory,
private MethodProvider $methodProvider
) {}
/**
* Process logging in.
* Note: This method can change the state of the object (by setting the `portal` property.).
*
* @throws ServiceUnavailable
*/
@@ -121,22 +97,22 @@ class Authentication
{
$username = $data->getUsername();
$password = $data->getPassword();
$authenticationMethod = $data->getMethod();
$method = $data->getMethod();
$byTokenOnly = $data->byTokenOnly();
if (
$authenticationMethod &&
!$this->configDataProvider->authenticationMethodIsApi($authenticationMethod)
$method &&
!$this->configDataProvider->authenticationMethodIsApi($method)
) {
$this->log
->warning("AUTH: Trying to use not allowed authentication method '{$authenticationMethod}'.");
->warning("AUTH: Trying to use not allowed authentication method '{$method}'.");
return $this->processFail(Result::fail(FailReason::METHOD_NOT_ALLOWED), $data, $request);
}
$this->hookManager->processBeforeLogin($data, $request);
if (!$authenticationMethod && $password === null) {
if (!$method && $password === null) {
$this->log->error("AUTH: Trying to login w/o password.");
return Result::fail(FailReason::NO_PASSWORD);
@@ -144,7 +120,7 @@ class Authentication
$authToken = null;
if (!$authenticationMethod) {
if (!$method) {
$authToken = $this->authTokenManager->get($password);
}
@@ -172,7 +148,7 @@ class Authentication
$byTokenAndUsername = $request->getHeader(self::HEADER_BY_TOKEN) === 'true';
if ($authenticationMethod && $byTokenAndUsername) {
if ($method && $byTokenAndUsername) {
return Result::fail(FailReason::DISCREPANT_DATA);
}
@@ -194,9 +170,9 @@ class Authentication
}
}
$authenticationMethod ??= $this->configDataProvider->getDefaultAuthenticationMethod();
$method ??= $this->methodProvider->get();
$login = $this->loginFactory->create($authenticationMethod, $this->isPortal());
$login = $this->loginFactory->create($method, $this->isPortal());
$loginData = LoginData
::createBuilder()
@@ -210,7 +186,7 @@ class Authentication
$user = $result->getUser();
$authLogRecord = !$authTokenIsFound ?
$this->createAuthLogRecord($username, $user, $request, $authenticationMethod) :
$this->createAuthLogRecord($username, $user, $request, $method) :
null;
if ($result->isFail()) {
@@ -223,7 +199,12 @@ class Authentication
}
if (!$user->isAdmin() && $this->configDataProvider->isMaintenanceMode()) {
throw new ServiceUnavailable("Application is in maintenance mode.");
throw ServiceUnavailable::createWithBody(
"Application is in maintenance mod1e.",
Body::create()
->withMessageTranslation('maintenanceModeError', 'messages')
->encode()
);
}
if (!$this->processUserCheck($user, $authLogRecord)) {
@@ -315,9 +296,7 @@ class Authentication
$loggedUser->set('token', $authToken->getToken());
$loggedUser->set('authTokenId', $authTokenId);
if ($authLogRecord) {
$authLogRecord->set('authTokenId', $authTokenId);
}
$authLogRecord?->setAuthTokenId($authTokenId);
return $authToken;
}
@@ -334,7 +313,7 @@ class Authentication
if (
!$authLogRecord &&
$authToken instanceof AuthLogRecord &&
$authToken instanceof AuthTokenEntity &&
$authToken->hasId()
) {
$authLogRecord = $this->entityManager
@@ -350,40 +329,18 @@ class Authentication
}
}
private function setPortal(Portal $portal): void
{
$this->portal = $portal;
}
private function isPortal(): bool
{
return $this->portal || $this->applicationState->isPortal();
return $this->applicationState->isPortal();
}
private function getPortal(): Portal
{
if ($this->portal) {
return $this->portal;
}
return $this->applicationState->getPortal();
}
private function processAuthTokenCheck(AuthToken $authToken): bool
{
if ($this->allowAnyAccess && $authToken->getPortalId() && !$this->isPortal()) {
/** @var ?Portal $portal */
$portal = $this->entityManager->getEntity('Portal', $authToken->getPortalId());
if ($portal) {
$this->setPortal($portal);
}
}
if ($this->allowAnyAccess) {
return true;
}
if ($this->isPortal() && $authToken->getPortalId() !== $this->getPortal()->getId()) {
$this->log->info("AUTH: Trying to login to portal with a token not related to portal.");
@@ -446,8 +403,7 @@ class Authentication
if (!$isPortalRelatedToUser) {
$this->log->info(
"AUTH: Trying to login to portal as user '" . $user->getUserName() . "' ".
"which is portal user but does not belongs to portal."
);
"which is portal user but does not belongs to portal.");
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_USER_IS_NOT_IN_PORTAL);
@@ -546,52 +502,80 @@ class Authentication
return $authToken;
}
public function destroyAuthToken(string $token, Request $request, Response $response): bool
/**
* Destroy an auth token.
*
* @param string $token A token to destroy.
* @param Request $request A request.
* @param Response $response A response.
* @throws Forbidden
* @throws NotFound
*/
public function destroyAuthToken(string $token, Request $request, Response $response): void
{
$authToken = $this->authTokenManager->get($token);
if (!$authToken) {
return false;
throw new NotFound("Auth token not found.");
}
if (!$this->applicationState->hasUser()) {
throw new LogicException("No logged user.");
}
$user = $this->applicationState->getUser();
$this->authTokenManager->inactivate($authToken);
if ($authToken->getSecret()) {
$sentSecret = $request->getCookieParam(self::COOKIE_AUTH_TOKEN_SECRET);
if (
// Still need the ability to destroy auth tokens of another users
// for login-as-another-user feature.
$authToken->getUserId() !== $user->getId() &&
$sentSecret !== $authToken->getSecret()
) {
throw new Forbidden("Can't destroy auth token.");
}
if ($sentSecret === $authToken->getSecret()) {
$this->setSecretInCookie(null, $response);
}
}
$method = $this->configDataProvider->getDefaultAuthenticationMethod();
$method = $this->methodProvider->get();
if ($this->logoutFactory->isCreatable($method)) {
$logout = $this->logoutFactory->create($method);
$result = $logout->logout($authToken, LogoutParams::create());
$redirectUrl = $result->getRedirectUrl();
if ($redirectUrl) {
$response->setHeader(self::HEADER_LOGOUT_REDIRECT_URL, $redirectUrl);
}
if (!$this->logoutFactory->isCreatable($method)) {
return;
}
return true;
$result = $this->logoutFactory
->create($method)
->logout($authToken, LogoutParams::create());
$redirectUrl = $result->getRedirectUrl();
if ($redirectUrl) {
$response->setHeader(self::HEADER_LOGOUT_REDIRECT_URL, $redirectUrl);
}
}
private function createAuthLogRecord(
?string $username,
?User $user,
Request $request,
?string $authenticationMethod = null
?string $method = null
): ?AuthLogRecord {
if ($username === self::LOGOUT_USERNAME) {
return null;
}
if ($this->configDataProvider->isAuthLogDisabled()) {
return null;
}
/** @var AuthLogRecord $authLogRecord */
$authLogRecord = $this->entityManager->getNewEntity(AuthLogRecord::ENTITY_TYPE);
@@ -604,27 +588,28 @@ class Authentication
$username = $user->getUserName();
}
$authLogRecord->set([
'username' => $username,
'ipAddress' => $request->getServerParam('REMOTE_ADDR'),
'requestTime' => $request->getServerParam('REQUEST_TIME_FLOAT'),
'requestMethod' => $request->getMethod(),
'requestUrl' => $requestUrl,
'authenticationMethod' => $authenticationMethod,
]);
$authLogRecord
->setUsername($username)
->setIpAddress($request->getServerParam('REMOTE_ADDR'))
->setRequestTime($request->getServerParam('REQUEST_TIME_FLOAT'))
->setRequestMethod($request->getMethod())
->setRequestUrl($requestUrl)
->setAuthenticationMethod($method)
->setPortalId($this->isPortal() ? $this->getPortal()->getId() : null);
if ($this->isPortal()) {
$authLogRecord->set('portalId', $this->getPortal()->getId());
if ($user && $user->isApi() && $this->configDataProvider->isApiUserAuthLogDisabled()) {
return null;
}
if ($user) {
$authLogRecord->set('userId', $user->hasId() ? $user->getId() : null);
$authLogRecord->setUserId($user->hasId() ? $user->getId() : null);
return $authLogRecord;
}
$authLogRecord->set('isDenied', true);
$authLogRecord->set('denialReason', AuthLogRecord::DENIAL_REASON_CREDENTIALS);
$authLogRecord
->setIsDenied()
->setDenialReason(AuthLogRecord::DENIAL_REASON_CREDENTIALS);
$this->entityManager->saveEntity($authLogRecord);
@@ -637,7 +622,9 @@ class Authentication
return;
}
$authLogRecord->set('denialReason', $denialReason);
$authLogRecord
->setIsDenied()
->setDenialReason($denialReason);
$this->entityManager->saveEntity($authLogRecord);
}
@@ -33,22 +33,11 @@ use Espo\Core\InjectableFactory;
class AuthenticationFactory
{
private InjectableFactory $injectableFactory;
public function __construct(InjectableFactory $injectableFactory)
{
$this->injectableFactory = $injectableFactory;
}
public function __construct(private InjectableFactory $injectableFactory)
{}
public function create(): Authentication
{
return $this->injectableFactory->create(Authentication::class);
}
public function createWithAnyAccessAllowed(): Authentication
{
return $this->injectableFactory->createWith(Authentication::class, [
'allowAnyAccess' => true,
]);
}
}
@@ -129,6 +129,16 @@ class ConfigDataProvider
return (bool) $this->config->get('authAnotherUserDisabled');
}
public function isAuthLogDisabled(): bool
{
return (bool) $this->config->get('authLogDisabled');
}
public function isApiUserAuthLogDisabled(): bool
{
return (bool) $this->config->get('authApiUserLogDisabled');
}
/**
* @return MetadataParams[]
*/
@@ -32,31 +32,25 @@ namespace Espo\Core\Authentication\Hook\Hooks;
use Espo\Core\Authentication\Hook\BeforeLogin;
use Espo\Core\Authentication\AuthenticationData;
use Espo\Core\Api\Request;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Authentication\ConfigDataProvider;
use Espo\ORM\EntityManager;
use Espo\Core\Utils\Log;
use Espo\ORM\EntityManager;
use Espo\Entities\AuthLogRecord;
use DateTime;
class FailedAttemptsLimit implements BeforeLogin
{
private $configDataProvider;
private $entityManager;
private $log;
public function __construct(ConfigDataProvider $configDataProvider, EntityManager $entityManager, Log $log)
{
$this->configDataProvider = $configDataProvider;
$this->entityManager = $entityManager;
$this->log = $log;
}
public function __construct(
private ConfigDataProvider $configDataProvider,
private EntityManager $entityManager,
private Log $log
) {}
/**
* @throws Forbidden
*/
public function process(AuthenticationData $data, Request $request): void
{
$isByTokenOnly = !$data->getMethod() && $request->getHeader('Espo-Authorization-By-Token') === 'true';
@@ -65,6 +59,10 @@ class FailedAttemptsLimit implements BeforeLogin
return;
}
if ($this->configDataProvider->isAuthLogDisabled()) {
return;
}
$failedAttemptsPeriod = $this->configDataProvider->getFailedAttemptsPeriod();
$maxFailedAttempts = $this->configDataProvider->getMaxFailedAttemptNumber();

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