Compare commits

...

449 Commits
5.4.4 ... 5.5.2

Author SHA1 Message Date
barwi
f512bbc9fa Fix removing element from "one of" filter (#1171) 2018-12-25 14:49:20 +02:00
yuri
b8f5fe2b21 record service naming change 2018-12-25 12:43:30 +02:00
yuri
be73390fde select manager apply joins from foreign 2018-12-25 11:52:02 +02:00
yuri
17cd2bc543 fix find linked 2018-12-25 11:39:03 +02:00
yuri
74712ba931 coding style 2018-12-25 11:17:27 +02:00
yuri
0d22a238dd fix layout reset to default 2018-12-25 11:06:34 +02:00
yuri
56e9170a6b fix foreign field 2018-12-25 11:01:27 +02:00
yuri
b333fd6772 fix email template, signature image 2018-12-25 10:54:48 +02:00
yuri
4eb469eb59 multi enum allow custom options param 2018-12-24 16:30:12 +02:00
yuri
4ff0d3c654 role enum label 2018-12-24 15:56:19 +02:00
yuri
4f1dd0673e fix fa lang 2018-12-24 15:14:16 +02:00
yuri
caf05b5c26 email to case update fetched values 2018-12-24 14:39:39 +02:00
yuri
2643ab7a17 fix calls meetings dashlets order 2018-12-24 13:59:57 +02:00
yuri
6caea136a5 Merge branch 'hotfix/5.5.2' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.5.2 2018-12-24 12:59:43 +02:00
yuri
1d23d65910 formula string test function 2018-12-24 12:45:10 +02:00
yuri
8cc3de5807 enum display as label param 2018-12-24 12:36:37 +02:00
yuri
9b022df709 css sticked fix 2018-12-24 12:03:12 +02:00
yuri
aba061cb72 css sticked fix 2018-12-24 11:56:37 +02:00
yuri
ae4d725595 formule maxLineDetailCount 80 2018-12-24 11:28:00 +02:00
yuri
6a76dc41d1 formula field fit height 2018-12-24 11:26:12 +02:00
yuri
2b649c64a3 dropdown menu vertival padding 2018-12-24 11:12:18 +02:00
yuri
db2944cb15 fix email filter manager order 2018-12-24 11:02:26 +02:00
yuri
3d40184373 processLinkMultipleFieldSave improvement 2018-12-24 10:59:55 +02:00
Taras Machyshyn
e788c51ff3 Merge branch 'hotfix/5.5.2' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.5.2 2018-12-21 18:15:28 +02:00
Taras Machyshyn
d7fc389182 DBAL: fixed a problem of changing text field type 2018-12-21 18:15:10 +02:00
yuri
ecafbe823e fix rtl 2018-12-21 13:29:27 +02:00
yuri
8abde38ebf email address phone number validate improvements 2018-12-21 12:56:56 +02:00
yuri
0fd7f768a9 email address lookup improvements 2018-12-21 12:26:28 +02:00
yuri
9c38d68b65 fix autocomplete email address autoselect 2018-12-21 11:43:22 +02:00
yuri
f0de5dde53 code style fix 2018-12-21 11:27:50 +02:00
yuri
2cdbf0ba76 email address autocomplete max size 2018-12-21 11:23:02 +02:00
yuri
34e4f4d7c2 fix date time filter after 2018-12-21 11:03:57 +02:00
yuri
61c0b58798 Merge branch 'hotfix/5.5.2' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.5.2 2018-12-20 17:44:33 +02:00
yuri
b2db0ea683 rtl theme fix 2018-12-20 17:44:27 +02:00
yuri
bd0fb02bb2 fix logo 2018-12-20 17:34:31 +02:00
yuri
f0aed23416 navbar changes 2018-12-20 17:25:05 +02:00
Taras Machyshyn
65368519c7 DBAL: compare text length 2018-12-20 17:10:42 +02:00
yuri
53db6f22d6 event acceptance status colors 2018-12-20 14:03:07 +02:00
yuri
5d47a987f4 preferences data longtext 2018-12-20 13:48:39 +02:00
yuri
a1f03e22d4 update readme 2018-12-20 11:45:01 +02:00
Eymen Elkum
cddaa7b7a1 small fix to activities service (#1158)
$selectParams in the line https://github.com/espocrm/espocrm/blob/master/application/Espo/Modules/Crm/Services/Activities.php#L1057 was not defined so I moved it to be after initialized
$selectManager->addLeftJoin(['assignedUsers', 'assignedUsers'], $selectParams); seems not needed as it is added in the line 1095
2018-12-20 11:32:49 +02:00
yuri
58dee36f1b fix timezone issue in datetime on/today filters 2018-12-20 11:26:26 +02:00
yuri
ae24fc07ba preset filter all label 2018-12-20 11:14:07 +02:00
yuri
13ca0db761 fix autocomplete max size 2018-12-20 11:09:56 +02:00
yuri
d2b20154de duplicate function in view 2018-12-19 11:41:52 +02:00
yuri
b176d866d0 fix template image 2018-12-19 11:22:55 +02:00
yuri
07a9a79626 chart fix 2 2018-12-18 15:54:08 +02:00
yuri
b60da458d5 fix chart 2018-12-18 15:47:15 +02:00
yuri
40ffbc550f fix opp by stage 2018-12-18 15:18:16 +02:00
yuri
35076148cc opp dashlets fiscal year filters 2018-12-18 15:13:40 +02:00
yuri
3175ff9a76 settings test 2018-12-18 13:39:46 +02:00
yuri
81457b1e48 fix accounts tasks 2018-12-18 13:15:40 +02:00
yuri
e9d5472d72 config test update 2018-12-18 12:52:33 +02:00
yuri
7c4bd371e7 Merge branch 'hotfix/5.5.2' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.5.2 2018-12-18 12:48:57 +02:00
Taras Machyshyn
98ed7e4dd3 Added systemItems 2018-12-18 12:45:54 +02:00
yuri
8404ef1d83 update fontawesome 2018-12-18 12:39:28 +02:00
yuri
0d99d84adf xmlwriter requirement 2018-12-18 12:14:12 +02:00
yuri
8492962923 settings filter smtp info 2018-12-18 11:25:50 +02:00
yuri
99627f856f job fail not started ready 2018-12-17 16:33:30 +02:00
yuri
b917cc20f6 fix relationship order 2018-12-17 14:25:44 +02:00
yuri
c582e88b7b version 2018-12-17 14:09:17 +02:00
yuri
9a00603600 theme navbarAdjustmentHandler 2018-12-17 14:07:51 +02:00
yuri
550b5e3bb4 iconv requirement 2018-12-17 13:49:28 +02:00
yuri
5ceb57a2cf config refactoring 2018-12-17 13:47:01 +02:00
yuri
16e96f6454 outboundEmailBccAddress user items 2018-12-17 11:06:08 +02:00
yuri
c162ee5337 email invitation and reminder allow link usage 2018-12-17 10:58:50 +02:00
yuri
66d34e7035 Merge branch 'hotfix/5.5.1' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.5.1 2018-12-14 16:25:05 +02:00
Taras Machyshyn
f57f7f72ed Added skipBackup option for extensions 2018-12-14 16:21:39 +02:00
yuri
c911cfe310 datetime hasSeconds param 2018-12-14 12:43:35 +02:00
yuri
223c8b53b4 scheduled job log status style 2018-12-14 12:15:01 +02:00
yuri
add1154014 jobPeriodForEndedProcess param 2018-12-14 12:06:10 +02:00
yuri
6ede731cfe fix jobs 2018-12-13 18:26:31 +02:00
yuri
0619cc0e3d fix htmlizer 2018-12-13 18:12:52 +02:00
yuri
06a686ad1a fix cron 2018-12-13 18:07:53 +02:00
yuri
15aed06f9d Merge branch 'hotfix/5.5.1' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.5.1 2018-12-13 18:05:03 +02:00
yuri
012936dc7d jobs running processing 2018-12-13 18:04:45 +02:00
Taras Machyshyn
0fb5b4a323 LDAP: Bug fixes for a portal user 2018-12-13 17:51:25 +02:00
Taras Machyshyn
dc3cfb4c23 Merge branch 'hotfix/5.5.1' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.5.1 2018-12-13 16:53:07 +02:00
Taras Machyshyn
450e7894aa File manager improvements 2018-12-13 16:36:39 +02:00
yuri
b8e7dbd147 followers link forbidden 2018-12-13 12:44:43 +02:00
yuri
a41c53d7f4 css audited fields fix 2018-12-12 16:22:23 +02:00
yuri
419faa2a47 email phone fields audited param 2018-12-12 15:39:40 +02:00
yuri
1d3b181a1c readme update 2018-12-12 12:37:57 +02:00
yuri
e0d4ec4a28 fix as soon as possible job scheduling 2018-12-12 12:34:55 +02:00
yuri
f95d8cbdde version 2018-12-12 12:30:54 +02:00
yuri
e22c9151d1 email message id disable text fitler 2018-12-12 12:29:13 +02:00
yuri
f6ae58a05d radio contanier css 2018-12-12 12:27:36 +02:00
yuri
cb3fc1c951 access info email fix 2018-12-12 11:48:21 +02:00
yuri
7f84fd2b4c password change request fix 2018-12-12 11:39:38 +02:00
yuri
518a67bf8a attachment document icon 2018-12-11 18:13:25 +02:00
yuri
cc6af53eff stream dashlet lit icon 2018-12-11 17:42:52 +02:00
Taras Machyshyn
7b27f62f10 Bug fixes for integration tests 2018-12-11 14:16:24 +02:00
yuri
16d69a1925 async from fork 2018-12-11 11:24:02 +02:00
yuri
66fb39c98d fix email to contact 2018-12-10 13:01:10 +02:00
yuri
9bfd68ad24 fix link parent 2018-12-10 10:50:16 +02:00
yuri
846665bd57 css fix 2018-12-07 19:20:34 +02:00
yuri
04f92584d4 global search input change 2018-12-07 19:14:32 +02:00
yuri
3891422735 fix autocomplete 2018-12-07 18:56:01 +02:00
yuri
a806c3336d entity acl 2018-12-07 17:49:44 +02:00
yuri
5310fd38bf user field level access fix 2018-12-07 12:30:18 +02:00
yuri
507df94427 portal admin forbid access 2018-12-07 12:16:19 +02:00
yuri
f55ea8ddbb fix task overdue date end 2018-12-07 11:54:39 +02:00
yuri
865030914e css 2018-12-07 11:52:51 +02:00
yuri
e5baa91433 fix autocomplete 2018-12-07 11:09:59 +02:00
yuri
059eaccbe2 fix css 2018-12-06 17:44:09 +02:00
yuri
b432e1ad2c login fixed 2018-12-06 17:28:41 +02:00
yuri
e9c25c1330 fix template variables ui 2018-12-06 15:24:36 +02:00
yuri
bc1a263b13 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-12-06 14:58:58 +02:00
Taras Machyshyn
98ec83aa36 Orm converter fixes 2018-12-06 14:58:25 +02:00
yuri
fe2b527561 email phone fieldType 2018-12-06 14:06:50 +02:00
yuri
57d57160dd css fix 2018-12-06 13:46:34 +02:00
yuri
c60964ac16 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-12-06 13:20:06 +02:00
Taras Machyshyn
9d4c5a4d1b Orm converter changes 2018-12-06 13:19:31 +02:00
yuri
2e6cd93908 ditch name attributes 2018-12-06 11:39:30 +02:00
yuri
f9e4d35cad lead filters layout 2018-12-05 16:58:27 +02:00
yuri
96ca03371e more autofill ditch 2018-12-05 15:14:44 +02:00
yuri
f3e4da9e0b ditch chrome autofill 2018-12-05 15:00:24 +02:00
yuri
bcde89b52e Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-12-04 17:24:32 +02:00
Taras Machyshyn
6becdc495d Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-12-04 17:24:05 +02:00
Taras Machyshyn
483130176c Integration tests for Field Manager 2018-12-04 17:23:53 +02:00
yuri
4481a3d10e fix css 2018-12-04 16:44:35 +02:00
yuri
0caf9a63fc Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-12-04 15:06:35 +02:00
Taras Machyshyn
82a08d3a6a Tests: bug fixes 2018-12-04 15:06:26 +02:00
yuri
a4bbf889ec Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-12-04 14:44:55 +02:00
yuri
a4beb96754 fix email import last date today 2018-12-04 14:44:51 +02:00
Taras Machyshyn
e4212161b5 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-12-04 14:31:08 +02:00
Taras Machyshyn
f271ded65b FieldManager: bug fixes 2018-12-04 14:30:57 +02:00
yuri
5eff7ff3fe email import fixes 2018-12-04 13:35:10 +02:00
yuri
342542622f fix css 2018-12-03 17:14:07 +02:00
yuri
0be995a108 user layout 2018-12-03 16:26:32 +02:00
yuri
6486b0b4f8 fix global search button 2018-12-03 16:17:50 +02:00
yuri
c3f79087fd global search tpl changes 2018-12-03 16:10:26 +02:00
yuri
87caf8b58f Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-12-03 12:56:34 +02:00
yuri
4a94813509 emailForceUseExternalClient param 2018-12-03 12:52:26 +02:00
yuri
8c9429d77a success color fix 2018-12-03 12:26:58 +02:00
yuri
a0c3524056 side navbar changes 2018-12-03 12:10:13 +02:00
Taras Machyshyn
beba210031 Test fixes 2018-12-03 10:31:07 +02:00
Taras Machyshyn
c50f28325a Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-30 19:18:36 +02:00
Taras Machyshyn
4efa575201 FieldManager: bug fixes 2018-11-30 19:18:28 +02:00
yuri
bf8bfb1d23 email from, address not storable fields 2018-11-30 17:23:53 +02:00
Taras Machyshyn
40eaa026b2 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-30 16:38:35 +02:00
Taras Machyshyn
4c7d2d66f0 FieldManager: bug fixes 2018-11-30 16:38:26 +02:00
yuri
cfcf5bacea Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-30 14:28:24 +02:00
yuri
1f12365518 lang 2018-11-30 14:28:17 +02:00
yuri
538a00b3eb job number bigint 2018-11-30 14:28:12 +02:00
Taras Machyshyn
ba623fe3f0 Metadata bug fixes 2018-11-30 14:20:15 +02:00
yuri
5a27041a54 job number int 2018-11-30 13:33:29 +02:00
yuri
c899cf4098 auth log auth method 2018-11-30 12:37:04 +02:00
yuri
a99b84bb86 job number 2018-11-30 11:59:54 +02:00
yuri
c0bede1670 fix notice 2018-11-30 10:56:33 +02:00
yuri
868f80340b entity icon names 2018-11-29 18:27:36 +02:00
yuri
5f0b692bc4 css fix 2018-11-29 17:35:06 +02:00
yuri
73ad04c049 e0 q0 queues 2018-11-29 16:31:51 +02:00
yuri
5d6cdc2fee job queue 2018-11-29 15:16:42 +02:00
yuri
fc712e0869 increase email portion size 2018-11-29 13:31:39 +02:00
yuri
f049e3f482 group email checking in separate jobs 2018-11-29 13:29:36 +02:00
yuri
19e517558b hack for imap not supporting search by UID 2018-11-29 13:29:08 +02:00
yuri
7c9a115574 process note acl job 2018-11-29 12:31:10 +02:00
yuri
93f51f7087 fix internal jobs 2018-11-29 12:17:04 +02:00
yuri
c9bb6dfdde fix note collection 2018-11-28 17:25:49 +02:00
yuri
5c14eda02a enum style 2018-11-28 16:45:58 +02:00
yuri
8ee0cb3af5 update zend mail 2018-11-28 12:50:08 +02:00
yuri
1404e2a90a mass email small fixes 2018-11-28 12:40:29 +02:00
yuri
b58d72e9fd cleanup 2018-11-28 11:16:38 +02:00
yuri
be03a3e175 fix create from email 2018-11-28 11:16:16 +02:00
yuri
bf46d970b8 css fix 2018-11-27 16:53:07 +02:00
yuri
c36e405acd fix naming 2018-11-27 16:24:23 +02:00
yuri
ebe4f78a62 css fix 2018-11-27 16:18:45 +02:00
yuri
5bda3e14d3 css colors fixes 2018-11-27 16:13:01 +02:00
yuri
cac0684696 status enum refactoring 2018-11-27 12:02:56 +02:00
yuri
6b89707c6e field search params naming 2018-11-27 11:10:06 +02:00
yuri
efcdbbe30c link link multiple search data naming 2018-11-26 16:41:24 +02:00
yuri
ce4c86de9d fix tooltip 2018-11-26 11:54:45 +02:00
yuri
d9b69ae61b lt lang fix 2018-11-26 11:33:10 +02:00
yuri
412174e8bb lv lang 2018-11-26 11:32:35 +02:00
yuri
2691a5dbaa ProcessNoteAclQueue scheduling 1 minute 2018-11-26 11:27:54 +02:00
yuri
eef34cec7c fix edit model reset 2018-11-26 11:26:48 +02:00
yuri
0449396678 record service link lists access lists 2018-11-26 11:18:06 +02:00
yuri
5b456b9d44 email to case: fill description and attachments 2018-11-23 16:22:33 +02:00
yuri
adb44b1b0a email note received improvements 2018-11-23 16:03:37 +02:00
yuri
4f5bd4b705 fix detail header sync 2018-11-22 13:21:57 +02:00
yuri
9370031805 cleanup 2018-11-22 13:20:19 +02:00
yuri
8ee258272b htmlizer prevent looping 2018-11-22 12:02:03 +02:00
yuri
fd5dfd769a htmlizer loop through collection 2018-11-21 16:55:03 +02:00
yuri
fb6d6e7599 scope icons 2018-11-21 13:22:05 +02:00
yuri
8c90cafcba hide access button for non admin users 2018-11-21 13:00:54 +02:00
yuri
36f3d3b479 htmlizer entity collection 2018-11-21 11:53:37 +02:00
yuri
3647fcb48e fix thousand separator 2018-11-20 17:28:28 +02:00
yuri
2368c3c8a7 css color 2018-11-20 17:01:14 +02:00
yuri
60a041e454 stick buttons all the way 2018-11-20 16:43:29 +02:00
yuri
5613938877 css fix 2018-11-20 16:32:17 +02:00
yuri
8817a1a19e email notification read 2018-11-20 15:46:06 +02:00
yuri
5b4caedbef list mode buttons size 2018-11-20 13:37:31 +02:00
yuri
b431b1e095 last view icon color 2018-11-20 13:26:44 +02:00
yuri
dac295ea19 has attachment tpl fix 2018-11-19 15:39:52 +02:00
yuri
2c2ac08bf7 fix upload attacment 2018-11-19 15:20:20 +02:00
yuri
3b8e9d8e47 css fixes 2018-11-19 14:56:47 +02:00
yuri
9682671de2 centered logo 2018-11-19 13:35:27 +02:00
yuri
29559e571c css colors 2018-11-19 13:06:41 +02:00
yuri
cfb25d068d Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-19 12:43:38 +02:00
yuri
9b8c6bef99 task: complete button on detail modal 2018-11-19 12:08:15 +02:00
yuri
168a1ea6e5 email compose: restore last html value 2018-11-19 11:20:46 +02:00
yuri
1515517bc3 kanban icon 2018-11-19 11:12:06 +02:00
yuri
7b20c9e448 fix css 2018-11-19 11:07:42 +02:00
yuri
008c9e6f7b fix email import duplicate fromString 2018-11-19 11:05:58 +02:00
yuri
d26edd74d1 fix email import 2018-11-16 12:37:25 +02:00
yuri
6f3be6fdde fix email notification 2018-11-16 11:32:50 +02:00
yuri
603019116b email import fix 3 2018-11-16 11:19:18 +02:00
Taras Machyshyn
0f7b5ea1f4 Tests: bug fixes 2018-11-16 11:14:38 +02:00
yuri
d62a83ba0c sorting icon 2018-11-15 17:53:32 +02:00
yuri
b873f850af logo float change 2018-11-15 17:30:28 +02:00
yuri
16e45f6c4e fix email import 2 2018-11-15 16:17:43 +02:00
yuri
5c34747a74 email fix body plain action 2018-11-15 13:44:17 +02:00
yuri
cd4099cf4b email import parallel fixes 2018-11-15 13:21:39 +02:00
yuri
4ceb22c0ad category tree expanded icon 2018-11-14 16:43:58 +02:00
yuri
cd76a63c2e cleanup 2018-11-14 16:38:42 +02:00
yuri
6871416bc1 text cut improvement 2018-11-14 15:41:35 +02:00
yuri
f401e08d0e ui action handler 2018-11-14 12:29:39 +02:00
yuri
c8dd17ffa7 fix lang 2018-11-14 11:35:34 +02:00
yuri
766e8cbcf7 category tree menu 2018-11-14 11:34:16 +02:00
yuri
86ad6a262b last viewed layout change 2018-11-13 17:21:50 +02:00
yuri
614900443d fix last viewed 2 2018-11-13 17:14:22 +02:00
yuri
0be7bbd57e fix last viewed 2018-11-13 17:07:29 +02:00
yuri
5b45cc552f fix mass email smtp account field 2018-11-13 16:55:41 +02:00
yuri
b6653f99be inline edit disabled param 2018-11-13 16:38:08 +02:00
yuri
520b3c5e89 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-13 15:51:24 +02:00
yuri
ebe2ca9e9d preferecnes button change 2018-11-13 15:46:54 +02:00
yuri
63740b17bb update timepicker version 2018-11-13 15:42:57 +02:00
Taras Machyshyn
800a7af874 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-13 15:37:43 +02:00
Taras Machyshyn
75bdd68779 DBAL: default value for MariaDB 10.2.7+ 2018-11-13 15:37:28 +02:00
yuri
0f9ac24f52 datetime minuteStep param 2018-11-13 15:35:18 +02:00
yuri
5ac39c53b3 fix stream refresh icon 2018-11-13 15:09:45 +02:00
yuri
3edc895edc fix search 2018-11-13 14:52:49 +02:00
yuri
ecc06c5917 last viewed show more 2018-11-13 13:01:58 +02:00
yuri
c672e1b435 css fixes 2018-11-13 12:19:21 +02:00
yuri
9165660286 follow button icon 2018-11-13 12:14:42 +02:00
yuri
60673c579f css fixes 2018-11-13 12:11:17 +02:00
yuri
3967864df7 fix css 2018-11-12 16:51:23 +02:00
yuri
c76e288b6b fix css 2018-11-12 16:41:22 +02:00
yuri
a4549b0c12 entity manager check client defs exists 2018-11-12 16:24:05 +02:00
yuri
3fc838a2b4 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-12 16:03:03 +02:00
yuri
a81fbe6c92 side menu improvement 2018-11-12 16:01:10 +02:00
Taras Machyshyn
34429bbcf5 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-12 12:16:05 +02:00
Taras Machyshyn
1cb7d943ec Test fixes 2018-11-12 12:15:50 +02:00
yuri
a57af66b2b lang fix 2018-11-12 11:52:29 +02:00
yuri
59c388a3f5 fix naming 2018-11-12 11:50:36 +02:00
yuri
282c3e32fc fix de lang 2018-11-12 11:48:38 +02:00
yuri
e841d43a06 fix css 2018-11-12 11:24:02 +02:00
yuri
474d7b638c fix list sticked 2018-11-12 11:23:56 +02:00
yuri
44c9d10cbb Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-12 10:59:54 +02:00
yuri
e863e3f56d jquery length instead of size 2018-11-12 10:53:55 +02:00
yuri
432f5a5647 fix stick 2018-11-09 17:27:27 +02:00
yuri
3545b560fe css fix 2018-11-09 17:21:11 +02:00
yuri
4e86ce2a77 search add field label 2018-11-09 17:13:31 +02:00
yuri
4881787cf0 stick list view actions 2018-11-09 17:08:09 +02:00
Taras Machyshyn
3172cdbd55 DBAL: bug fixes 2018-11-09 15:18:54 +02:00
yuri
6601922c13 fix cleanup jobs 2018-11-09 12:15:40 +02:00
yuri
049d6bc4de fix typo 2018-11-09 11:08:50 +02:00
yuri
9fddd6b1ba it lang 2018-11-09 11:03:35 +02:00
yuri
ea4762e3d0 fix css style 2018-11-08 18:36:40 +02:00
yuri
cb44f0fe13 relationship panel menu order change 2018-11-08 18:07:29 +02:00
yuri
a8fa776713 fix css 2018-11-08 17:53:55 +02:00
yuri
cb82f0df0c icons change 2018-11-08 17:25:53 +02:00
yuri
c6fc914a4e dropdown button style change 2018-11-08 15:04:54 +02:00
yuri
c7096574ab fix body plain modal 2018-11-08 15:03:06 +02:00
yuri
c98a3ae40a Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-08 13:56:36 +02:00
yuri
39e4250998 fix entity manager create ntity 2018-11-08 13:48:18 +02:00
Taras Machyshyn
6c44253798 Integration tests improvements 2018-11-08 13:35:08 +02:00
yuri
4836bce5b0 record service unset id 2018-11-08 13:32:18 +02:00
yuri
184a889301 entity manager createEntity 2018-11-08 13:14:05 +02:00
yuri
9b0609a7f1 acl tests 2018-11-08 12:50:38 +02:00
yuri
824216035f wysiwyg: support on list 2018-11-08 11:37:23 +02:00
yuri
e690231f05 fix filter check icon 2018-11-07 15:59:52 +02:00
yuri
22201f83b2 entity manager reserved names 2018-11-07 15:38:17 +02:00
yuri
be4ab13f6d fix formula field 2018-11-07 14:23:33 +02:00
yuri
24186ae143 custom client routes 2018-11-07 12:28:41 +02:00
yuri
eba8cc630d router cleanup and ditch logout route 2018-11-07 11:57:22 +02:00
yuri
e835e0d269 cleanup 2018-11-07 11:46:08 +02:00
yuri
c420892800 disable ability to edit scheduled job job 2018-11-07 11:39:48 +02:00
yuri
ada7c067b3 fix cleanup deleted records 2018-11-07 11:32:52 +02:00
yuri
8109c8460e code formatting fix 2018-11-06 17:05:29 +02:00
yuri
aeddff23ad gruntfile addition 2018-11-06 16:40:41 +02:00
Taras Machyshyn
c93c05fe8c Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-06 16:33:30 +02:00
Taras Machyshyn
01d0020b28 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-06 16:28:06 +02:00
yuri
aa78f4f9db fix upgrade id 2018-11-06 16:24:13 +02:00
yuri
7879b57ea3 controller manager fix 2018-11-06 16:18:34 +02:00
yuri
a4794cac60 super admin only config params 2018-11-06 16:08:10 +02:00
yuri
6bd6510ce9 style fix 2018-11-06 15:28:43 +02:00
yuri
88225eb590 Persian lang 2018-11-06 15:24:22 +02:00
yuri
ef1c1d2a24 placeholders info 2018-11-06 15:04:57 +02:00
yuri
f461f1968d email template today now placeholders 2018-11-06 14:56:04 +02:00
yuri
2513edc53a phone number link wo spaces 2018-11-06 14:32:55 +02:00
yuri
40d03c6848 jobs fixes improvements 2018-11-06 12:27:34 +02:00
yuri
75d6a728b1 about tpl 2018-11-05 16:58:43 +02:00
yuri
739d1dd3d1 fix msg 2018-11-05 16:50:12 +02:00
yuri
97b338fc5d cleanup 2018-11-05 16:49:48 +02:00
yuri
5350cf8e52 parallel jobs and daemon 2018-11-05 16:45:42 +02:00
yuri
0c2959da80 calendar dashlet icon 2018-11-02 15:03:45 +02:00
yuri
37cba66ad7 timeline shared by default 2018-11-02 15:00:00 +02:00
yuri
03606ae644 fix currency 2018-11-02 13:53:26 +02:00
yuri
292453e425 call date end on layout 2018-11-02 13:07:19 +02:00
yuri
9639840b46 Merge branch 'master' of github.com:espocrm/espocrm 2018-11-02 12:56:09 +02:00
Arkady
db6cdec9b5 fix typo (#1103) 2018-11-02 12:55:51 +02:00
yuri
b74c17c582 fix timeline 2018-11-02 12:54:05 +02:00
yuri
20a139ca0a timeline bust timeframes 2018-11-02 12:44:20 +02:00
yuri
4fd5555146 timeline create datestart 2018-11-01 16:17:32 +02:00
yuri
2e4cc5f0be activities controller change 2018-11-01 15:52:47 +02:00
yuri
983dc7c376 fix ipad more menu 2018-11-01 14:50:59 +02:00
Taras Machyshyn
f461434fb4 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-11-01 10:17:49 +02:00
yuri
907655bb09 uniqid fix 2018-10-31 15:58:02 +02:00
yuri
41d6a0a099 is replied css 2018-10-31 13:15:39 +02:00
yuri
573d14f854 fix panel actions 2018-10-31 13:04:11 +02:00
yuri
c0719694ba panel actions improvement 2018-10-31 12:59:52 +02:00
yuri
49aea5aad1 load empty link name fields 2018-10-30 12:45:03 +02:00
yuri
076569027a loadNotJoinedLinkFields optimization 2018-10-30 12:37:18 +02:00
yuri
2a7cc3aeb0 fiscal year start 2018-10-30 11:57:22 +02:00
Taras Machyshyn
89dca6086d Installation: Added user type 2018-10-29 16:45:14 +02:00
Taras Machyshyn
c00bbdb0fe Merge commits 2018-10-29 16:32:58 +02:00
yuri
60bdf881b1 ORM: fiscal year and quarter functions 2018-10-29 15:58:17 +02:00
yuri
ae9237c4b9 portal user icon 2018-10-29 12:09:04 +02:00
yuri
51a221809b acl field-level additional attribute list 2018-10-29 11:44:12 +02:00
yuri
d7d176c169 merge 2018-10-29 10:51:49 +02:00
yuri
374413eaae version 2018-10-29 10:23:19 +02:00
yuri
96538a2b6f fix order by address 2018-10-29 10:16:07 +02:00
yuri
39a3a6dbd2 users nonAdminReadOnlyAttributeList 2018-10-29 10:15:37 +02:00
Taras Machyshyn
58d959fdef Installer: bug fixes 2018-10-26 15:14:30 +03:00
Taras Machyshyn
3de7238d7b Installer: bug fixes 2018-10-26 14:58:38 +03:00
yuri
66b2065d7c user fixes 2018-10-26 14:38:18 +03:00
Taras Machyshyn
e4b012c40f Installer: minor bug fixes 2018-10-26 14:03:56 +03:00
yuri
adf5283375 user and test fixes 2018-10-26 13:23:03 +03:00
yuri
1f5baa9d70 fix unit test 2018-10-26 12:54:52 +03:00
yuri
7d09306047 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-10-26 12:41:40 +03:00
Taras Machyshyn
c3ae8cdd62 Fixed unnecessary index 2018-10-26 12:07:32 +03:00
yuri
fef6ed5f34 api key authorization 2018-10-26 12:06:20 +03:00
yuri
778b212bd0 auth changes 2018-10-25 18:34:39 +03:00
yuri
d675bdfdf2 api users auth 2018-10-25 17:53:18 +03:00
Taras Machyshyn
0923414af1 Added requirements for MariaDB 2018-10-25 15:36:40 +03:00
yuri
d9b0dce5f4 fix check all results 2018-10-25 11:31:14 +03:00
yuri
b28bae20b7 user api type 2018-10-24 16:46:48 +03:00
yuri
a235ad1bc3 portal user side panel fields 2018-10-24 15:28:28 +03:00
yuri
db0114b600 fix portal row actions link 2018-10-24 15:03:55 +03:00
yuri
2aad48505e user type 2018-10-24 15:03:39 +03:00
yuri
347493dbbd fix password preview 2018-10-23 17:00:26 +03:00
yuri
f09e69e9b1 generate btn fix 2018-10-23 16:54:48 +03:00
yuri
2c410f4ecc fix 2018-10-23 15:25:31 +03:00
yuri
5e26f8da37 fix 2018-10-23 15:24:57 +03:00
yuri
8d6b86529b related list url 2018-10-23 15:20:55 +03:00
yuri
18859963e5 fix import 2018-10-23 12:31:41 +03:00
yuri
37828e5716 order by first name and last name 2018-10-23 12:30:20 +03:00
yuri
27bb69a4f7 orderBy backward compatibility 2018-10-23 11:35:06 +03:00
yuri
c41c994772 fix order by address 2018-10-22 16:30:39 +03:00
yuri
5610569fe7 ditch collection asc 2018-10-22 16:20:11 +03:00
yuri
ba0ad4dfe9 orderBy param 2018-10-22 15:11:06 +03:00
yuri
049443f4cc fix naming 2018-10-22 10:41:19 +03:00
yuri
ba76ea086c fix portal isDefault 2018-10-22 10:40:38 +03:00
yuri
a7beec2548 admin panel change 2018-10-19 14:52:14 +03:00
yuri
49bc5a5d5d fix cron manager 2018-10-19 13:53:27 +03:00
yuri
d9fd6bfc87 scheduled job delete pending jobs after scheduling change 2018-10-19 13:37:33 +03:00
yuri
4d5f0fce6c aclAllowDeleteCreatedThresholdPeriod 2018-10-19 13:23:59 +03:00
yuri
e7114d9c4c fix controller manager 2018-10-19 13:20:56 +03:00
yuri
8957a9ba5b css fix 2018-10-19 12:18:36 +03:00
yuri
a9ce82c8e9 ditch clear local cache from menu 2018-10-19 11:56:11 +03:00
yuri
85f1e05860 cron: ability to run more frequent than once per minute 2018-10-18 17:13:10 +03:00
yuri
5bdf4cb099 style fix 2018-10-18 16:48:32 +03:00
yuri
c52b1f8cc3 scheduled job fix 2018-10-18 16:09:26 +03:00
yuri
5176aef78c controller manager refactoring 2018-10-18 14:11:39 +03:00
yuri
8fc0ed81ff acl get level fix 2018-10-18 13:49:07 +03:00
yuri
b84838bfbb naming fix 2018-10-18 11:51:01 +03:00
yuri
2adf724458 menu debiders 2018-10-18 11:50:39 +03:00
yuri
fd8f546d1f email create contact in b2b mode 2018-10-18 11:11:48 +03:00
yuri
c33de304dc compose email relate with parent 2018-10-18 11:07:53 +03:00
yuri
c6afb86765 kanban sticking header 2018-10-17 16:47:04 +03:00
yuri
71e2af6c4c naming fix 2018-10-17 15:13:02 +03:00
yuri
15b74e19d7 hide popovers on modal open 2018-10-17 15:11:25 +03:00
yuri
74eece5956 naming fix 2018-10-17 15:04:34 +03:00
yuri
ada764bb7f text varchar null if empty 2018-10-17 14:50:47 +03:00
yuri
e2e99009a3 warn if old class naming is used 2018-10-17 14:39:09 +03:00
yuri
8e879e2d73 po module name in file name 2018-10-17 14:24:26 +03:00
yuri
7808586751 fix stylesheed loading 2018-10-17 14:03:43 +03:00
yuri
23e66d58d3 filling contact when create case from contact 2018-10-17 13:55:29 +03:00
yuri
214b8baf8f fix header buttons 2018-10-16 14:51:30 +03:00
yuri
756a913e94 po script improvement 2018-10-16 13:48:14 +03:00
yuri
720bdc7aaf ru lang 2018-10-16 13:34:06 +03:00
yuri
b49d1e2e2c load default theme on log out 2018-10-16 13:22:35 +03:00
yuri
c0e76b05e2 css refactoring 2018-10-16 12:59:29 +03:00
yuri
ca0e7c781e css fix 2018-10-16 11:06:12 +03:00
yuri
50bcc77cd0 layout manager css fix 2018-10-15 17:02:22 +03:00
yuri
6decfd05a0 layout manager css fix 2018-10-15 16:53:11 +03:00
yuri
6bae680e4a layout manager css fix 2018-10-15 16:37:36 +03:00
yuri
3532f4136c merge in action history 2018-10-15 15:52:23 +03:00
yuri
af406bb473 layout manager grid fix 2018-10-15 15:44:51 +03:00
yuri
78decd9b67 dynamic logic fix 2018-10-15 15:43:02 +03:00
yuri
4d2a27d908 side/relationship panel conditions 2018-10-15 15:40:15 +03:00
yuri
f2afdcba6f fix int field 2018-10-15 13:54:16 +03:00
yuri
2d4c43e51a version 2018-10-15 13:09:45 +03:00
yuri
43124e3c06 update php-mime-mail-parser 2018-10-15 11:31:07 +03:00
yuri
7b80a0a60e mass update permission 2018-10-15 11:08:16 +03:00
yuri
74fc9007dd Merge branch 'stable' 2018-10-12 16:08:38 +03:00
yuri
ef2d83ab08 panel filters change 2018-10-12 13:05:32 +03:00
yuri
ba9520d6f5 record post search 2018-10-11 17:03:30 +03:00
yuri
1cbab04c35 account role fix 2018-10-11 15:43:49 +03:00
yuri
e8deed9244 fix activity list 2018-10-11 12:52:00 +03:00
yuri
b3ff690497 contact listen to inactive 2018-10-11 12:43:19 +03:00
yuri
a8f03abd1d relation modal fixes 2018-10-11 12:38:26 +03:00
yuri
9be41a4e37 Merge branch 'hotfix/5.4.4' 2018-10-10 17:03:52 +03:00
yuri
b020cc1c39 activities list modal 2018-10-10 17:02:56 +03:00
Taras Machyshyn
4eab7f6a88 FieldManager improvements 2018-10-10 15:26:55 +03:00
yuri
8f213f819b fix activities email 2018-10-10 13:04:17 +03:00
yuri
4bd6f761e5 relatin view label 2018-10-10 11:33:18 +03:00
yuri
9dfcbb7c8e mass select only if allowed 2018-10-09 17:01:37 +03:00
yuri
488981753a related link modal 2018-10-09 16:50:37 +03:00
Taras Machyshyn
7622cdc1cf Merge branch 'hotfix/5.4.4' 2018-10-09 12:38:40 +03:00
Taras Machyshyn
fe6ffe4555 Merge branch 'hotfix/5.4.4' 2018-10-09 12:18:20 +03:00
yuri
a6d0a90c73 contact title label 2018-10-08 14:25:12 +03:00
yuri
716cd2e729 repository getEntityType 2018-10-08 11:04:18 +03:00
yuri
034e06f636 templateFileManager fix 2018-10-08 11:03:00 +03:00
yuri
dfa4627998 entity name plural 2018-10-08 11:02:05 +03:00
yuri
942b6f3305 lang fix 2018-10-03 14:51:46 +03:00
yuri
fda03757fc template manager 2018-10-03 14:47:33 +03:00
yuri
6955ea581c Merge branch 'hotfix/5.4.4' 2018-10-03 11:14:42 +03:00
yuri
2b3d1e9ce0 Merge branch 'hotfix/5.4.4' 2018-09-28 17:20:57 +03:00
yuri
ab93196432 css fix 2018-09-28 16:57:52 +03:00
yuri
d8c5955ba4 autocomplete max height 2018-09-28 16:44:48 +03:00
yuri
0f64611843 update autocomplete 2018-09-28 16:41:41 +03:00
yuri
7244204f35 fix address country 2018-09-28 15:25:20 +03:00
yuri
6eee3c3432 address country list 2018-09-28 14:46:42 +03:00
yuri
3801de3449 fix varchar autocomplete 2018-09-28 14:11:43 +03:00
yuri
d97fefebf8 varchar options additions 2018-09-28 13:40:41 +03:00
yuri
06d55f2746 Merge branch 'hotfix/5.4.4' 2018-09-28 12:42:13 +03:00
yuri
8d0bb8fc12 varchar suggestion picklist 2018-09-28 11:44:04 +03:00
yuri
57c5d4b15c stream post: record link shorter form 2018-09-27 13:26:51 +03:00
yuri
41433ecbf5 it_IT lang 2018-09-27 12:04:12 +03:00
yuri
e0a4c5b6a8 attachment image insert fix 2018-09-27 11:51:03 +03:00
yuri
219a77f121 manual marge 2018-09-26 16:17:01 +03:00
yuri
43c0299c25 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-09-26 16:13:43 +03:00
yuri
7b65cba5e4 stream image link inserting 2018-09-26 16:12:03 +03:00
Taras Machyshyn
35bbe00e17 Minor changes for a system requirements 2018-09-25 14:50:35 +03:00
yuri
673c18a5ce system config params 2018-09-25 13:31:18 +03:00
Taras Machyshyn
dc32b278f9 Added system-requirements 2018-09-25 13:07:15 +03:00
yuri
7678316b5a fix call 2018-09-25 12:48:49 +03:00
yuri
988622629a call load phone numbers after update 2018-09-25 12:46:57 +03:00
yuri
c090b4629b showing phone numbers on call detail view 2018-09-25 12:42:05 +03:00
yuri
a9a3759d6e Merge branch 'hotfix/5.4.4' 2018-09-25 12:11:31 +03:00
yuri
f0d98542e4 cleanup 2018-09-25 10:52:00 +03:00
yuri
a32ed9c8f2 manual merge 2018-09-25 10:49:24 +03:00
yuri
51286a2c46 user access info and password change request templates 2018-09-20 13:18:46 +03:00
yuri
16538077ee Merge branch 'hotfix/5.4.4' 2018-09-20 11:28:29 +03:00
yuri
63e78a8895 invitations use template file manager 2018-09-19 11:08:58 +03:00
yuri
4b54a962b2 cleanup relationships 2018-09-18 17:00:32 +03:00
1120 changed files with 26203 additions and 7112 deletions

View File

@@ -171,6 +171,7 @@ module.exports = function (grunt) {
'html/**',
'bootstrap.php',
'cron.php',
'daemon.php',
'rebuild.php',
'clear_cache.php',
'upgrade.php',

View File

@@ -9,7 +9,7 @@ Download the latest release from our [website](http://www.espocrm.com).
### Requirements
* PHP 5.6 or above (with pdo, json, gd, openssl, zip, imap, mbstring, curl extensions);
* MySQL 5.5.3 or above.
* MySQL 5.5.3 or above, or MariaDB.
For more information about server configuration see [this article](https://www.espocrm.com/documentation/administration/server-configuration/).
@@ -52,6 +52,12 @@ The build will be created in the `build` directory.
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.
Branches:
* hotfix/* an upcoming maintenance release; fixes should be pushed to this branch;
* master an upcoming minor or major release; new features should be pushed to this branch;
* stable a last stable release.
### How to make a translation
Build po file with command:

View File

@@ -48,7 +48,9 @@ class EmailAddress extends \Espo\Core\Acl\Base
if (!$this->getAclManager()->check($user, $e, 'edit')) {
$isFobidden = true;
if (
$e->get('isPortalUser') && $excludeEntity->getEntityType() === 'Contact' &&
$e->getEntityType() === 'User' &&
$e->isPortal() &&
$excludeEntity->getEntityType() === 'Contact' &&
$e->get('contactId') === $excludeEntity->id
) {
$isFobidden = false;

View File

@@ -48,7 +48,9 @@ class PhoneNumber extends \Espo\Core\Acl\Base
if (!$this->getAclManager()->check($user, $e, 'edit')) {
$isFobidden = true;
if (
$e->get('isPortalUser') && $excludeEntity->getEntityType() === 'Contact' &&
$e->getEntityType() === 'User' &&
$e->isPortal() &&
$excludeEntity->getEntityType() === 'Contact' &&
$e->get('contactId') === $excludeEntity->id
) {
$isFobidden = false;

View File

@@ -44,6 +44,9 @@ class User extends \Espo\Core\Acl\Base
if (!$user->isAdmin()) {
return false;
}
if ($entity->isSuperAdmin() && !$user->isSuperAdmin()) {
return false;
}
return $this->checkEntity($user, $entity, $data, 'create');
}
@@ -55,6 +58,12 @@ class User extends \Espo\Core\Acl\Base
if (!$user->isAdmin()) {
return false;
}
if ($entity->isSystem()) {
return false;
}
if ($entity->isSuperAdmin() && !$user->isSuperAdmin()) {
return false;
}
return parent::checkEntityDelete($user, $entity, $data);
}
@@ -63,11 +72,17 @@ class User extends \Espo\Core\Acl\Base
if ($entity->id === 'system') {
return false;
}
if ($entity->isSystem()) {
return false;
}
if (!$user->isAdmin()) {
if ($user->id !== $entity->id) {
return false;
}
}
if ($entity->isSuperAdmin() && !$user->isSuperAdmin()) {
return false;
}
return $this->checkEntity($user, $entity, $data, 'edit');
}
}

View File

@@ -69,7 +69,7 @@ class Admin extends \Espo\Core\Controllers\Base
public function postActionUploadUpgradePackage($params, $data)
{
if ($this->getConfig()->get('restrictedMode')) {
if (!$this->getUser()->get('isSuperAdmin')) {
if (!$this->getUser()->isSuperAdmin()) {
throw new Forbidden();
}
}
@@ -87,7 +87,7 @@ class Admin extends \Espo\Core\Controllers\Base
public function postActionRunUpgrade($params, $data)
{
if ($this->getConfig()->get('restrictedMode')) {
if (!$this->getUser()->get('isSuperAdmin')) {
if (!$this->getUser()->isSuperAdmin()) {
throw new Forbidden();
}
}
@@ -108,4 +108,10 @@ class Admin extends \Espo\Core\Controllers\Base
$adminNotificationManager = new \Espo\Core\Utils\AdminNotificationManager($this->getContainer());
return $adminNotificationManager->getNotificationList();
}
public function actionSystemRequirementList($params)
{
$systemRequirementManager = new \Espo\Core\Utils\SystemRequirements($this->getContainer());
return $systemRequirementManager->getAllRequiredList();
}
}

View File

@@ -41,4 +41,20 @@ class Attachment extends \Espo\Core\Controllers\Record
}
return parent::actionList($params, $data, $request);
}
public function postActionGetAttachmentFromImageUrl($params, $data)
{
if (empty($data->url)) throw new BadRequest();
if (empty($data->field)) throw new BadRequest('postActionGetAttachmentFromImageUrl: No field specified');
return $this->getRecordService()->getAttachmentFromImageUrl($data)->getValueMap();
}
public function postActionGetCopiedAttachment($params, $data)
{
if (empty($data->id)) throw new BadRequest();
if (empty($data->field)) throw new BadRequest('postActionGetCopiedAttachment copy: No field specified');
return $this->getRecordService()->getCopiedAttachment($data)->getValueMap();
}
}

View File

@@ -42,11 +42,13 @@ class EmailAddress extends \Espo\Core\Controllers\Record
throw new Forbidden();
}
$q = $request->get('q');
$limit = intval($request->get('limit'));
if (empty($limit) || $limit > 30) {
$limit = 5;
$maxSize = intval($request->get('maxSize'));
if (empty($maxSize) || $maxSize > 50) {
$maxSize = $this->getConfig()->get('recordsPerPage', 20);
}
return $this->getRecordService()->searchInAddressBook($q, $limit);
$onlyActual = $request->get('onlyActual') === 'true';
return $this->getRecordService()->searchInAddressBook($q, $maxSize, $onlyActual);
}
}

View File

@@ -90,7 +90,7 @@ class EntityManager extends \Espo\Core\Controllers\Base
$params['iconClass'] = $data['iconClass'];
}
if (isset($data['fullTextSearch'])) {
$params['fullTestSearch'] = $data['fullTextSearch'];
$params['fullTextSearch'] = $data['fullTextSearch'];
}
$params['kanbanViewMode'] = !empty($data['kanbanViewMode']);
@@ -130,10 +130,6 @@ class EntityManager extends \Espo\Core\Controllers\Base
$name = $data['name'];
$name = filter_var($name, \FILTER_SANITIZE_STRING);
if (!empty($data['sortDirection'])) {
$data['asc'] = $data['sortDirection'] === 'asc';
}
$result = $this->getContainer()->get('entityManagerUtil')->update($name, $data);
if ($result) {

View File

@@ -66,7 +66,7 @@ class Extension extends \Espo\Core\Controllers\Record
throw new Forbidden();
}
if ($this->getConfig()->get('restrictedMode')) {
if (!$this->getUser()->get('isSuperAdmin')) {
if (!$this->getUser()->isSuperAdmin()) {
throw new Forbidden();
}
}
@@ -84,7 +84,7 @@ class Extension extends \Espo\Core\Controllers\Record
throw new Forbidden();
}
if ($this->getConfig()->get('restrictedMode')) {
if (!$this->getUser()->get('isSuperAdmin')) {
if (!$this->getUser()->isSuperAdmin()) {
throw new Forbidden();
}
}
@@ -120,7 +120,7 @@ class Extension extends \Espo\Core\Controllers\Record
throw BadRequest();
}
if ($this->getConfig()->get('restrictedMode')) {
if (!$this->getUser()->get('isSuperAdmin')) {
if (!$this->getUser()->isSuperAdmin()) {
throw new Forbidden();
}
}

View File

@@ -35,12 +35,24 @@ class LastViewed extends \Espo\Core\Controllers\Base
{
public function getActionIndex($params, $data, $request)
{
$result = $this->getServiceFactory()->create('LastViewed')->get();
$params = [];
return [
'total' => $result['total'],
'list' => isset($result['collection']) ? $result['collection']->toArray() : $result['list']
$params['offset'] = $request->get('offset', 0);
$params['maxSize'] = $request->get('maxSize');
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', \Espo\Core\Controllers\Record::MAX_SIZE_LIMIT);
if (empty($params['maxSize'])) {
$params['maxSize'] = $maxSizeLimit;
}
if (!empty($params['maxSize']) && $params['maxSize'] > $maxSizeLimit) {
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$result = $this->getServiceFactory()->create('LastViewed')->getList($params);
return (object) [
'total' => $result->total,
'list' => $result->collection->getValueMapList()
];
}
}

View File

@@ -78,15 +78,18 @@ class Layout extends \Espo\Core\Controllers\Base
return $this->actionUpdate($params, $data, $request);
}
public function actionResetToDefault($params, $data, $request)
public function postActionResetToDefault($params, $data, $request)
{
if (!$request->isPost()) {
throw new BadRequest();
if (!$this->getUser()->isAdmin()) {
throw new Forbidden();
}
if (empty($data->scope) || empty($data->name)) {
throw new BadRequest();
}
$this->getContainer()->get('dataManager')->updateCacheTimestamp();
return $this->getContainer()->get('layout')->resetToDefault($data->scope, $data->name);
}
}

View File

@@ -132,7 +132,7 @@ class Preferences extends \Espo\Core\Controllers\Base
$entity->set('smtpEmailAddress', $user->get('emailAddress'));
$entity->set('name', $user->get('name'));
$entity->set('isPortalUser', $user->get('isPortalUser'));
$entity->set('isPortalUser', $user->isPortal());
$entity->clear('smtpPassword');

View File

@@ -35,23 +35,12 @@ use \Espo\Core\Exceptions\BadRequest;
class Settings extends \Espo\Core\Controllers\Base
{
protected function getConfigData()
{
if ($this->getUser()->id == 'system') {
$data = $this->getConfig()->getData();
} else {
$data = $this->getConfig()->getData($this->getUser()->isAdmin());
}
$data = $this->getServiceFactory()->create('Settings')->getConfigData();
$fieldDefs = $this->getMetadata()->get('entityDefs.Settings.fields');
foreach ($fieldDefs as $field => $d) {
if ($d['type'] === 'password') {
unset($data[$field]);
}
}
$data['jsLibs'] = $this->getMetadata()->get('app.jsLibs');
$data->jsLibs = $this->getMetadata()->get('app.jsLibs');
return $data;
}
@@ -76,23 +65,7 @@ class Settings extends \Espo\Core\Controllers\Base
throw new BadRequest();
}
if (
(isset($data->useCache) && $data->useCache !== $this->getConfig()->get('useCache'))
||
(isset($data->aclStrictMode) && $data->aclStrictMode !== $this->getConfig()->get('aclStrictMode'))
) {
$this->getContainer()->get('dataManager')->clearCache();
}
$this->getConfig()->setData($data, $this->getUser()->isAdmin());
$result = $this->getConfig()->save();
if ($result === false) {
throw new Error('Cannot save settings');
}
if (isset($data->defaultCurrency) || isset($data->baseCurrency) || isset($data->currencyRates)) {
$this->getContainer()->get('dataManager')->rebuildDatabase([]);
}
$this->getServiceFactory()->create('Settings')->setConfigData($data);
return $this->getConfigData();
}

View File

@@ -49,24 +49,59 @@ class Stream extends \Espo\Core\Controllers\Base
$service = $this->getService('Stream');
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($maxSize)) {
$maxSize = self::MAX_SIZE_LIMIT;
$maxSize = $maxSizeLimit;
}
if (!empty($maxSize) && $maxSize > self::MAX_SIZE_LIMIT) {
throw new Forbidden();
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$result = $service->find($scope, $id, array(
$result = $service->find($scope, $id, [
'offset' => $offset,
'maxSize' => $maxSize,
'after' => $after,
'filter' => $filter
));
]);
return array(
'total' => $result['total'],
'list' => $result['collection']->toArray()
);
return (object) [
'total' => $result->total,
'list' => $result->collection->getValueMapList()
];
}
public function getActionListPosts($params, $data, $request)
{
$scope = $params['scope'];
$id = isset($params['id']) ? $params['id'] : null;
$offset = intval($request->get('offset'));
$maxSize = intval($request->get('maxSize'));
$after = $request->get('after');
$where = $request->get('where');
$service = $this->getService('Stream');
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($maxSize)) {
$maxSize = $maxSizeLimit;
}
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$result = $service->find($scope, $id, [
'offset' => $offset,
'maxSize' => $maxSize,
'after' => $after,
'filter' => 'posts',
'where' => $where
]);
return (object) [
'total' => $result->total,
'list' => $result->collection->getValueMapList()
];
}
}

View File

@@ -0,0 +1,122 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://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\Utils as Utils;
use \Espo\Core\Exceptions\NotFound;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\BadRequest;
class TemplateManager extends \Espo\Core\Controllers\Base
{
protected function checkControllerAccess()
{
if (!$this->getUser()->isAdmin()) {
throw new Forbidden();
}
}
public function getActionGetTemplate($params, $data, $request)
{
$name = $request->get('name');
if (empty($name)) throw new BadRequest();
$scope = $request->get('scope');
$module = null;
$module = $this->getMetadata()->get(['app', 'templates', $name, 'module']);
$hasSubject = !$this->getMetadata()->get(['app', 'templates', $name, 'noSubject']);
$templateFileManager = $this->getContainer()->get('templateFileManager');
$returnData = (object) [];
$returnData->body = $templateFileManager->getTemplate($name, 'body', $scope, $module);
if ($hasSubject) {
$returnData->subject = $templateFileManager->getTemplate($name, 'subject', $scope, $module);
}
return $returnData;
}
public function postActionSaveTemplate($params, $data)
{
$scope = null;
if (empty($data->name)) {
throw new BadRequest();
}
if (!empty($data->scope)) {
$scope = $data->scope;
}
$templateFileManager = $this->getContainer()->get('templateFileManager');
if (isset($data->subject)) {
$templateFileManager->saveTemplate($data->name, 'subject', $data->subject, $scope);
}
if (isset($data->body)) {
$templateFileManager->saveTemplate($data->name, 'body', $data->body, $scope);
}
return true;
}
public function postActionResetTemplate($params, $data)
{
$scope = null;
if (empty($data->name)) {
throw new BadRequest();
}
if (!empty($data->scope)) {
$scope = $data->scope;
}
$module = null;
$module = $this->getMetadata()->get(['app', 'templates', $data->name, 'module']);
$hasSubject = !$this->getMetadata()->get(['app', 'templates', $data->name, 'noSubject']);
$templateFileManager = $this->getContainer()->get('templateFileManager');
if ($hasSubject) {
$templateFileManager->resetTemplate($data->name, 'subject', $scope);
}
$templateFileManager->resetTemplate($data->name, 'body', $scope);
$returnData = (object) [];
$returnData->body = $templateFileManager->getTemplate($data->name, 'body', $scope, $module);
if ($hasSubject) {
$returnData->subject = $templateFileManager->getTemplate($data->name, 'subject', $scope, $module);
}
return $returnData;
}
}

View File

@@ -105,5 +105,18 @@ class User extends \Espo\Core\Controllers\Record
return $this->getService('User')->passwordChangeRequest($userName, $emailAddress, $url);
}
}
public function actionCreateLink($params, $data, $request)
{
if (!$this->getUser()->isAdmin()) throw new Forbidden();
return parent::actionCreateLink($params, $data, $request);
}
public function actionRemoveLink($params, $data, $request)
{
if (!$this->getUser()->isAdmin()) throw new Forbidden();
return parent::actionRemoveLink($params, $data, $request);
}
}

View File

@@ -133,5 +133,19 @@ class Acl
{
return $this->getAclManager()->checkAssignmentPermission($this->getUser(), $target);
}
}
public function getScopeRestrictedFieldList($scope, $type)
{
return $this->getAclManager()->getScopeRestrictedFieldList($scope, $type);
}
public function getScopeRestrictedAttributeList($scope, $type)
{
return $this->getAclManager()->getScopeRestrictedAttributeList($scope, $type);
}
public function getScopeRestrictedLinkList($scope, $type)
{
return $this->getAclManager()->getScopeRestrictedLinkList($scope, $type);
}
}

View File

@@ -48,6 +48,8 @@ class Base implements Injectable
protected $ownerUserIdAttribute = null;
protected $allowDeleteCreatedThresholdPeriod = '24 hours';
public function inject($name, $object)
{
$this->injections[$name] = $object;
@@ -267,15 +269,26 @@ class Base implements Injectable
&&
$entity->has('createdById') && $entity->get('createdById') == $user->id
) {
$isDeletedAllowed = false;
if (!$entity->has('assignedUserId')) {
return true;
$isDeletedAllowed = true;
} else {
if (!$entity->get('assignedUserId')) {
return true;
$isDeletedAllowed = true;
} else if ($entity->get('assignedUserId') == $entity->get('createdById')) {
$isDeletedAllowed = true;
}
if ($entity->get('assignedUserId') == $entity->get('createdById')) {
return true;
}
if ($isDeletedAllowed) {
$createdAt = $entity->get('createdAt');
if ($createdAt) {
$deleteThresholdPeriod = $this->getConfig()->get('aclAllowDeleteCreatedThresholdPeriod', $this->allowDeleteCreatedThresholdPeriod);
if (\Espo\Core\Utils\DateTime::isAfterThreshold($createdAt, $deleteThresholdPeriod)) {
return false;
}
}
return true;
}
}
}

View File

@@ -0,0 +1,175 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://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;
class GlobalRestricton
{
protected $fieldTypeList = [
'forbidden', // totally forbidden
'internal', // reading forbidden, writing allowed
'onlyAdmin', // forbidden for non admin users
'readOnly', // read-only for all users
'nonAdminReadOnly' // read-only for non-admin users
];
protected $linkTypeList = [
'forbidden', // totally forbidden
'internal', // reading forbidden, writing allowed
'onlyAdmin', // forbidden for non admin users
'readOnly', // read-only for all users
'nonAdminReadOnly' // read-only for non-admin users
];
protected $cacheFilePath = 'data/cache/application/entityAcl.php';
private $metadata;
private $fileManager;
private $fieldManagerUtil;
private $data;
public function __construct(
\Espo\Core\Utils\Metadata $metadata,
\Espo\Core\Utils\File\Manager $fileManager,
\Espo\Core\Utils\FieldManagerUtil $fieldManagerUtil
)
{
$this->metadata = $metadata;
$this->fileManager = $fileManager;
$this->fieldManagerUtil = $fieldManagerUtil;
if (!file_exists($this->cacheFilePath)) {
$this->buildCacheFile();
}
$this->data = include($this->cacheFilePath);
}
protected function buildCacheFile()
{
$scopeList = array_keys($this->getMetadata()->get(['entityDefs'], []));
$data = (object) [];
foreach ($scopeList as $scope) {
$fieldList = array_keys($this->getMetadata()->get(['entityDefs', $scope, 'fields'], []));
$linkList = array_keys($this->getMetadata()->get(['entityDefs', $scope, 'links'], []));
$isNotEmpty = false;
$scopeData = (object) [
'fields' => (object) [],
'attributes' => (object) [],
'links' => (object) []
];
foreach ($this->fieldTypeList as $type) {
$resultFieldList = [];
$resultAttributeList = [];
foreach ($fieldList as $field) {
if ($this->getMetadata()->get(['entityAcl', $scope, 'fields', $field, $type])) {
$isNotEmpty = true;
$resultFieldList[] = $field;
$fieldAttributeList = $this->getFieldManagerUtil()->getAttributeList($scope, $field);
foreach ($fieldAttributeList as $attribute) {
$resultAttributeList[] = $attribute;
}
}
}
$scopeData->fields->$type = $resultFieldList;
$scopeData->attributes->$type = $resultAttributeList;
}
foreach ($this->linkTypeList as $type) {
$resultLinkList = [];
foreach ($linkList as $link) {
if ($this->getMetadata()->get(['entityAcl', $scope, 'links', $link, $type])) {
$isNotEmpty = true;
$resultLinkList[] = $link;
}
}
$scopeData->links->$type = $resultLinkList;
}
if ($isNotEmpty) {
$data->$scope = $scopeData;
}
}
$this->data = $data;
$this->getFileManager()->putPhpContents($this->cacheFilePath, $data, true);
}
protected function getMetadata()
{
return $this->metadata;
}
protected function getFileManager()
{
return $this->fileManager;
}
protected function getFieldManagerUtil()
{
return $this->fieldManagerUtil;
}
public function getScopeRestrictedFieldList($scope, $type)
{
if (!property_exists($this->data, $scope)) return [];
if (!property_exists($this->data->$scope, 'fields')) return [];
if (!property_exists($this->data->$scope->fields, $type)) return [];
return $this->data->$scope->fields->$type;
}
public function getScopeRestrictedAttributeList($scope, $type)
{
if (!property_exists($this->data, $scope)) return [];
if (!property_exists($this->data->$scope, 'attributes')) return [];
if (!property_exists($this->data->$scope->attributes, $type)) return [];
return $this->data->$scope->attributes->$type;
}
public function getScopeRestrictedLinkList($scope, $type)
{
if (!property_exists($this->data, $scope)) return [];
if (!property_exists($this->data->$scope, 'links')) return [];
if (!property_exists($this->data->$scope->links, $type)) return [];
return $this->data->$scope->links->$type;
}
}

View File

@@ -182,6 +182,15 @@ class Table
return 'no';
}
public function getHighestLevel($action)
{
if (in_array($action, $this->booleanActionList)) {
return 'yes';
} else {
return 'all';
}
}
private function load()
{
$valuePermissionLists = (object)[];
@@ -554,7 +563,7 @@ class Table
protected function applyAdditional(&$table, &$fieldTable, &$valuePermissionLists)
{
if ($this->getUser()->get('isPortalUser')) {
if ($this->getUser()->isPortal()) {
foreach ($this->getScopeList() as $scope) {
$table->$scope = false;
unset($fieldTable->$scope);

View File

@@ -49,10 +49,18 @@ class AclManager
protected $userAclClassName = '\\Espo\\Core\\Acl';
protected $globalRestricton;
public function __construct(Container $container)
{
$this->container = $container;
$this->metadata = $container->get('metadata');
$this->globalRestricton = new \Espo\Core\Acl\GlobalRestricton(
$container->get('metadata'),
$container->get('fileManager'),
$container->get('fieldManagerUtil')
);
}
protected function getContainer()
@@ -125,7 +133,7 @@ class AclManager
public function getLevel(User $user, $scope, $action)
{
if ($user->isAdmin()) {
return 'all';
return $this->getTable($user)->getHighestLevel($action);
}
return $this->getTable($user)->getLevel($scope, $action);
}
@@ -241,16 +249,64 @@ class AclManager
return true;
}
protected function getGlobalRestrictionTypeList(User $user, $action = 'read')
{
$typeList = ['forbidden'];
if ($action === 'read') {
$typeList[] = 'internal';
}
if (!$user->isAdmin()) {
$typeList[] = 'onlyAdmin';
}
if ($action === 'edit') {
$typeList[] = 'readOnly';
if (!$user->isAdmin()) {
$typeList[] = 'nonAdminReadOnly';
}
}
return $typeList;
}
public function getScopeForbiddenAttributeList(User $user, $scope, $action = 'read', $thresholdLevel = 'no')
{
if ($user->isAdmin()) return [];
return $this->getTable($user)->getScopeForbiddenAttributeList($scope, $action, $thresholdLevel);
$list = [];
if (!$user->isAdmin()) {
$list = $this->getTable($user)->getScopeForbiddenAttributeList($scope, $action, $thresholdLevel);
}
if ($thresholdLevel === 'no') {
$list = array_merge(
$list,
$this->getScopeRestrictedAttributeList($scope, $this->getGlobalRestrictionTypeList($user, $action))
);
$list = array_values($list);
}
return $list;
}
public function getScopeForbiddenFieldList(User $user, $scope, $action = 'read', $thresholdLevel = 'no')
{
if ($user->isAdmin()) return [];
return $this->getTable($user)->getScopeForbiddenFieldList($scope, $action, $thresholdLevel);
$list = [];
if (!$user->isAdmin()) {
$list = $this->getTable($user)->getScopeForbiddenFieldList($scope, $action, $thresholdLevel);
}
if ($thresholdLevel === 'no') {
$list = array_merge(
$list,
$this->getScopeRestrictedFieldList($scope, $this->getGlobalRestrictionTypeList($user, $action))
);
$list = array_values($list);
}
return $list;
}
public function checkUserPermission(User $user, $target, $permissionType = 'userPermission')
@@ -294,4 +350,46 @@ class AclManager
$acl = new $className($this, $user);
return $acl;
}
public function getScopeRestrictedFieldList($scope, $type)
{
if (is_array($type)) {
$typeList = $type;
$list = [];
foreach ($typeList as $type) {
$list = array_merge($list, $this->globalRestricton->getScopeRestrictedFieldList($scope, $type));
}
$list = array_values($list);
return $list;
}
return $this->globalRestricton->getScopeRestrictedFieldList($scope, $type);
}
public function getScopeRestrictedAttributeList($scope, $type)
{
if (is_array($type)) {
$typeList = $type;
$list = [];
foreach ($typeList as $type) {
$list = array_merge($list, $this->globalRestricton->getScopeRestrictedAttributeList($scope, $type));
}
$list = array_values($list);
return $list;
}
return $this->globalRestricton->getScopeRestrictedAttributeList($scope, $type);
}
public function getScopeRestrictedLinkList($scope, $type)
{
if (is_array($type)) {
$typeList = $type;
$list = [];
foreach ($typeList as $type) {
$list = array_merge($list, $this->globalRestricton->getScopeRestrictedLinkList($scope, $type));
}
$list = array_values($list);
return $list;
}
return $this->globalRestricton->getScopeRestrictedLinkList($scope, $type);
}
}

View File

@@ -151,6 +151,50 @@ class Application
$cronManager->run();
}
public function runDaemon()
{
$maxProcessNumber = $this->getConfig()->get('daemonMaxProcessNumber');
$interval = $this->getConfig()->get('daemonInterval');
$timeout = $this->getConfig()->get('daemonProcessTimeout');
if (!$maxProcessNumber || !$interval) {
$GLOBALS['log']->error("Daemon config params are not set.");
return;
}
$processList = [];
while (true) {
$toSkip = false;
$runningCount = 0;
foreach ($processList as $i => $process) {
if ($process->isRunning()) {
$runningCount++;
} else if ($process->isRunning()) {
unset($processList[$i]);
}
}
$processList = array_values($processList);
if (count($runningCount) >= $maxProcessNumber) {
$toSkip = true;
}
if (!$toSkip) {
$process = new \Symfony\Component\Process\Process(['php', 'cron.php']);
$process->setTimeout($timeout);
$process->run();
}
sleep($interval);
}
}
public function runJob($id)
{
$auth = $this->createAuth();
$auth->useNoAuth();
$cronManager = new \Espo\Core\CronManager($this->container);
$cronManager->runJobById($id);
}
public function runRebuild()
{
$dataManager = $this->getContainer()->get('dataManager');
@@ -338,9 +382,9 @@ class Application
public function setupSystemUser()
{
$user = $this->getContainer()->get('entityManager')->getEntity('User', 'system');
$user->set('isAdmin', true);
$user->set('isAdmin', true); // TODO remove in 5.7
$user->set('type', 'system');
$this->getContainer()->setUser($user);
$this->getContainer()->get('entityManager')->setUser($user);
}
}

View File

@@ -32,7 +32,7 @@ namespace Espo\Core;
class Container
{
private $data = array();
private $data = [];
/**
@@ -222,6 +222,13 @@ class Container
);
}
protected function loadNotificatorFactory()
{
return new \Espo\Core\NotificatorFactory(
$this
);
}
protected function loadMetadata()
{
return new \Espo\Core\Utils\Metadata(

View File

@@ -40,12 +40,16 @@ class ControllerManager
private $container;
private $controllersHash = null;
public function __construct(\Espo\Core\Container $container)
{
$this->container = $container;
$this->config = $this->container->get('config');
$this->metadata = $this->container->get('metadata');
$this->controllersHash = (object) [];
}
protected function getConfig()
@@ -58,7 +62,7 @@ class ControllerManager
return $this->metadata;
}
public function process($controllerName, $actionName, $params, $data, $request, $response)
protected function getControllerClassName($controllerName)
{
$customClassName = '\\Espo\\Custom\\Controllers\\' . Util::normilizeClassName($controllerName);
if (class_exists($customClassName)) {
@@ -72,27 +76,48 @@ class ControllerManager
}
}
if ($data && stristr($request->getContentType(), 'application/json')) {
$data = json_decode($data);
}
if (!class_exists($controllerClassName)) {
throw new NotFound("Controller '$controllerName' is not found");
}
$controller = new $controllerClassName($this->container, $request->getMethod());
return $controllerClassName;
}
public function createController($name)
{
$controllerClassName = $this->getControllerClassName($name);
$controller = new $controllerClassName($this->container);
return $controller;
}
public function getController($name)
{
if (!property_exists($this->controllersHash, $name)) {
$this->controllersHash->$name = $this->createController($name);
}
return $this->controllersHash->$name;
}
public function processRequest(\Espo\Core\Controllers\Base $controller, $actionName, $params, $data, $request, $response = null)
{
if ($data && stristr($request->getContentType(), 'application/json')) {
$data = json_decode($data);
}
if ($actionName == 'index') {
$actionName = $controllerClassName::$defaultAction;
$actionName = $controller::$defaultAction;
}
$requestMethod = $request->getMethod();
$actionNameUcfirst = ucfirst($actionName);
$beforeMethodName = 'before' . $actionNameUcfirst;
$actionMethodName = 'action' . $actionNameUcfirst;
$afterMethodName = 'after' . $actionNameUcfirst;
$fullActionMethodName = strtolower($request->getMethod()) . ucfirst($actionMethodName);
$fullActionMethodName = strtolower($requestMethod) . ucfirst($actionMethodName);
if (method_exists($controller, $fullActionMethodName)) {
$primaryActionMethodName = $fullActionMethodName;
@@ -101,12 +126,12 @@ class ControllerManager
}
if (!method_exists($controller, $primaryActionMethodName)) {
throw new NotFound("Action '$actionName' (".$request->getMethod().") does not exist in controller '$controllerName'");
throw new NotFound("Action {$requestMethod} '{$actionName}' does not exist in controller '".$controller->getName()."'.");
}
// TODO Remove in 5.1.0
if ($data instanceof \stdClass) {
if ($this->getMetadata()->get(['app', 'deprecatedControllerActions', $controllerName, $primaryActionMethodName])) {
if ($this->getMetadata()->get(['app', 'deprecatedControllerActions', $controller->getName(), $primaryActionMethodName])) {
$data = get_object_vars($data);
}
}
@@ -127,4 +152,10 @@ class ControllerManager
return $result;
}
public function process($controllerName, $actionName, $params, $data, $request, $response = null)
{
$controller = $this->getController($controllerName);
return $this->processRequest($controller, $actionName, $params, $data, $request, $response);
}
}

View File

@@ -38,18 +38,12 @@ abstract class Base
private $container;
private $requestMethod;
public static $defaultAction = 'index';
public function __construct(Container $container, $requestMethod = null)
public function __construct(Container $container)
{
$this->container = $container;
if (isset($requestMethod)) {
$this->setRequestMethod($requestMethod);
}
if (empty($this->name)) {
$name = get_class($this);
if (preg_match('@\\\\([\w]+)$@', $name, $matches)) {
@@ -61,6 +55,11 @@ abstract class Base
$this->checkControllerAccess();
}
public function getName()
{
return $this->name;
}
protected function checkControllerAccess()
{
return;
@@ -71,21 +70,6 @@ abstract class Base
return $this->container;
}
/**
* Get request method name (Uppercase)
*
* @return string
*/
protected function getRequestMethod()
{
return $this->requestMethod;
}
protected function setRequestMethod($requestMethod)
{
$this->requestMethod = strtoupper($requestMethod);
}
protected function getUser()
{
return $this->container->get('user');

View File

@@ -83,6 +83,8 @@ class Record extends Base
public function actionCreate($params, $data, $request)
{
if (!is_object($data)) throw new BadRequest();
if (!$request->isPost()) {
throw new BadRequest();
}
@@ -93,7 +95,7 @@ class Record extends Base
$service = $this->getRecordService();
if ($entity = $service->createEntity($data)) {
if ($entity = $service->create($data)) {
return $entity->getValueMap();
}
@@ -102,6 +104,8 @@ class Record extends Base
public function actionUpdate($params, $data, $request)
{
if (!is_object($data)) throw new BadRequest();
if (!$request->isPut() && !$request->isPatch()) {
throw new BadRequest();
}
@@ -112,7 +116,7 @@ class Record extends Base
$id = $params['id'];
if ($entity = $this->getRecordService()->updateEntity($id, $data)) {
if ($entity = $this->getRecordService()->update($id, $data)) {
return $entity->getValueMap();
}
@@ -125,35 +129,18 @@ class Record extends Base
throw new Forbidden();
}
$where = $request->get('where');
$offset = $request->get('offset');
$maxSize = $request->get('maxSize');
$asc = $request->get('asc', 'true') === 'true';
$sortBy = $request->get('sortBy');
$q = $request->get('q');
$textFilter = $request->get('textFilter');
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($maxSize)) {
$maxSize = $maxSizeLimit;
}
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$params = array(
'where' => $where,
'offset' => $offset,
'maxSize' => $maxSize,
'asc' => $asc,
'sortBy' => $sortBy,
'q' => $q,
'textFilter' => $textFilter
);
$params = [];
$this->fetchListParamsFromRequest($params, $request, $data);
$result = $this->getRecordService()->findEntities($params);
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($params['maxSize'])) {
$params['maxSize'] = $maxSizeLimit;
}
if (!empty($params['maxSize']) && $params['maxSize'] > $maxSizeLimit) {
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$result = $this->getRecordService()->find($params);
return array(
'total' => $result['total'],
@@ -167,34 +154,17 @@ class Record extends Base
throw new Forbidden();
}
$where = $request->get('where');
$offset = $request->get('offset');
$maxSize = $request->get('maxSize');
$asc = $request->get('asc', 'true') === 'true';
$sortBy = $request->get('sortBy');
$q = $request->get('q');
$textFilter = $request->get('textFilter');
$params = [];
$this->fetchListParamsFromRequest($params, $request, $data);
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($maxSize)) {
$maxSize = $maxSizeLimit;
if (empty($params['maxSize'])) {
$params['maxSize'] = $maxSizeLimit;
}
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
if (!empty($params['maxSize']) && $params['maxSize'] > $maxSizeLimit) {
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$params = array(
'where' => $where,
'offset' => $offset,
'maxSize' => $maxSize,
'asc' => $asc,
'sortBy' => $sortBy,
'q' => $q,
'textFilter' => $textFilter
);
$this->fetchListParamsFromRequest($params, $request, $data);
$result = $this->getRecordService()->getListKanban($params);
return (object) [
@@ -206,19 +176,7 @@ class Record extends Base
protected function fetchListParamsFromRequest(&$params, $request, $data)
{
if ($request->get('primaryFilter')) {
$params['primaryFilter'] = $request->get('primaryFilter');
}
if ($request->get('boolFilterList')) {
$params['boolFilterList'] = $request->get('boolFilterList');
}
if ($request->get('filterList')) {
$params['filterList'] = $request->get('filterList');
}
if ($request->get('select')) {
$params['select'] = explode(',', $request->get('select'));
}
\Espo\Core\Utils\ControllerUtil::fetchListParamsFromRequest($params, $request, $data);
}
public function actionListLinked($params, $data, $request)
@@ -226,35 +184,18 @@ class Record extends Base
$id = $params['id'];
$link = $params['link'];
$where = $request->get('where');
$offset = $request->get('offset');
$maxSize = $request->get('maxSize');
$asc = $request->get('asc', 'true') === 'true';
$sortBy = $request->get('sortBy');
$q = $request->get('q');
$textFilter = $request->get('textFilter');
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($maxSize)) {
$maxSize = $maxSizeLimit;
}
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$params = array(
'where' => $where,
'offset' => $offset,
'maxSize' => $maxSize,
'asc' => $asc,
'sortBy' => $sortBy,
'q' => $q,
'textFilter' => $textFilter
);
$params = [];
$this->fetchListParamsFromRequest($params, $request, $data);
$result = $this->getRecordService()->findLinkedEntities($id, $link, $params);
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($params['maxSize'])) {
$params['maxSize'] = $maxSizeLimit;
}
if (!empty($params['maxSize']) && $params['maxSize'] > $maxSizeLimit) {
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$result = $this->getRecordService()->findLinked($id, $link, $params);
return array(
'total' => $result['total'],
@@ -270,7 +211,7 @@ class Record extends Base
$id = $params['id'];
if ($this->getRecordService()->deleteEntity($id)) {
if ($this->getRecordService()->delete($id)) {
return true;
}
throw new Error();
@@ -278,6 +219,8 @@ class Record extends Base
public function actionExport($params, $data, $request)
{
if (!is_object($data)) throw new BadRequest();
if (!$request->isPost()) {
throw new BadRequest();
}
@@ -337,6 +280,10 @@ class Record extends Base
throw new BadRequest();
}
if ($this->getAcl()->get('massUpdatePermission') !== 'yes') {
throw new Forbidden();
}
$params = array();
if (property_exists($data, 'where') && !empty($data->byWhere)) {
$params['where'] = json_decode(json_encode($data->where), true);
@@ -375,7 +322,7 @@ class Record extends Base
$params['ids'] = $data->ids;
}
return $this->getRecordService()->massRemove($params);
return $this->getRecordService()->massDelete($params);
}
public function actionCreateLink($params, $data, $request)
@@ -402,7 +349,7 @@ class Record extends Base
$selectData = json_decode(json_encode($data->selectData), true);
}
return $this->getRecordService()->linkEntityMass($id, $link, $where, $selectData);
return $this->getRecordService()->linkMass($id, $link, $where, $selectData);
} else {
$foreignIdList = array();
if (isset($data->id)) {
@@ -416,7 +363,7 @@ class Record extends Base
$result = false;
foreach ($foreignIdList as $foreignId) {
if ($this->getRecordService()->linkEntity($id, $link, $foreignId)) {
if ($this->getRecordService()->link($id, $link, $foreignId)) {
$result = true;
}
}
@@ -453,7 +400,7 @@ class Record extends Base
$result = false;
foreach ($foreignIdList as $foreignId) {
if ($this->getRecordService()->unlinkEntity($id, $link, $foreignId)) {
if ($this->getRecordService()->unlink($id, $link, $foreignId)) {
$result = $result || true;
}
}

View File

@@ -28,9 +28,12 @@
************************************************************************/
namespace Espo\Core;
use \PDO;
use Espo\Core\Utils\Json;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Error;
class CronManager
{
@@ -48,8 +51,21 @@ class CronManager
private $cronScheduledJobUtil;
private $useProcessPool = false;
private $asSoonAsPossibleSchedulingList = [
'*',
'* *',
'* * *',
'* * * *',
'* * * * *',
'* * * * * *'
];
const PENDING = 'Pending';
const READY = 'Ready';
const RUNNING = 'Running';
const SUCCESS = 'Success';
@@ -70,6 +86,14 @@ class CronManager
$this->scheduledJobUtil = $this->container->get('scheduledJob');
$this->cronJobUtil = new \Espo\Core\Utils\Cron\Job($this->config, $this->entityManager);
$this->cronScheduledJobUtil = new \Espo\Core\Utils\Cron\ScheduledJob($this->config, $this->entityManager);
if ($this->getConfig()->get('jobRunInParallel')) {
if (\Spatie\Async\Pool::isSupported()) {
$this->useProcessPool = true;
} else {
$GLOBALS['log']->warning("CronManager: useProcessPool requires pcntl and posix extensions.");
}
}
}
protected function getContainer()
@@ -146,6 +170,16 @@ class CronManager
return false;
}
protected function useProcessPool()
{
return $this->useProcessPool;
}
public function setUseProcessPool($useProcessPool)
{
$this->useProcessPool = $useProcessPool;
}
/**
* Run Cron
*
@@ -160,17 +194,39 @@ class CronManager
$this->setLastRunTime(time());
$this->getCronJobUtil()->markFailedJobs();
$this->getCronJobUtil()->markJobsFailed();
$this->getCronJobUtil()->updateFailedJobAttempts();
$this->createJobsFromScheduledJobs();
$this->getCronJobUtil()->removePendingJobDuplicates();
$pendingJobList = $this->getCronJobUtil()->getPendingJobList();
$this->processPendingJobs();
}
public function processPendingJobs($queue = null, $limit = null, $poolDisabled = false, $noLock = false)
{
if (is_null($limit)) {
$limit = intval($this->getConfig()->get('jobMaxPortion', 0));
}
$pendingJobList = $this->getCronJobUtil()->getPendingJobList($queue, $limit);
$useProcessPool = $this->useProcessPool();
if ($poolDisabled) {
$useProcessPool = false;
}
if ($useProcessPool) {
$pool = \Spatie\Async\Pool::create()
->autoload(getcwd() . '/vendor/autoload.php')
->concurrency($this->getConfig()->get('jobPoolConcurrencyNumber'))
->timeout($this->getConfig()->get('jobPeriodForActiveProcess'));
}
foreach ($pendingJobList as $job) {
$skip = false;
$this->getEntityManager()->getPdo()->query('LOCK TABLES `job` WRITE');
if ($this->getCronJobUtil()->isJobPending($job->id)) {
if (!$noLock) $this->lockJobTable();
if ($noLock || $this->getCronJobUtil()->isJobPending($job->id)) {
if ($job->get('scheduledJobId')) {
if ($this->getCronJobUtil()->isScheduledJobRunning($job->get('scheduledJobId'), $job->get('targetId'), $job->get('targetType'))) {
$skip = true;
@@ -181,43 +237,102 @@ class CronManager
}
if ($skip) {
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
if (!$noLock) $this->unlockTables();
continue;
}
$job->set('status', self::RUNNING);
$job->set('pid', $this->getCronJobUtil()->getPid());
$this->getEntityManager()->saveEntity($job);
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
$job->set('startedAt', date('Y-m-d H:i:s'));
$isSuccess = true;
$skipLog = false;
try {
if ($job->get('scheduledJobId')) {
$this->runScheduledJob($job);
} else {
$this->runService($job);
}
} catch (\Exception $e) {
$isSuccess = false;
if ($e->getCode() === -1) {
$job->set('attempts', 0);
$skipLog = true;
} else {
$GLOBALS['log']->error('CronManager: Failed job running, job ['.$job->id.']. Error Details: '.$e->getMessage());
}
if ($useProcessPool) {
$job->set('status', self::READY);
} else {
$job->set('status', self::RUNNING);
$job->set('pid', \Espo\Core\Utils\System::getPid());
}
$status = $isSuccess ? self::SUCCESS : self::FAILED;
$job->set('status', $status);
$this->getEntityManager()->saveEntity($job);
if (!$noLock) $this->unlockTables();
if ($job->get('scheduledJobId') && !$skipLog) {
$this->getCronScheduledJobUtil()->addLogRecord($job->get('scheduledJobId'), $status, null, $job->get('targetId'), $job->get('targetType'));
if ($useProcessPool) {
$task = new \Espo\Core\Utils\Cron\JobTask($job->id);
$pool->add($task);
} else {
$this->runJob($job);
}
}
if ($useProcessPool) {
$pool->wait();
}
}
protected function lockJobTable()
{
$this->getEntityManager()->getPdo()->query('LOCK TABLES `job` WRITE');
}
protected function unlockTables()
{
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
}
public function runJobById($id)
{
if (empty($id)) throw new Error();
$job = $this->getEntityManager()->getEntity('Job', $id);
if (!$job) throw new Error("Job {$id} not found.");
if ($job->get('status') !== self::READY) {
throw new Error("Can't run job {$id} with no status Ready.");
}
if (!$job->get('startedAt')) {
$job->set('startedAt', date('Y-m-d H:i:s'));
}
$job->set('status', self::RUNNING);
$job->set('pid', \Espo\Core\Utils\System::getPid());
$this->getEntityManager()->saveEntity($job);
$this->runJob($job);
}
public function runJob($job)
{
$isSuccess = true;
$skipLog = false;
try {
if ($job->get('scheduledJobId')) {
$this->runScheduledJob($job);
} else {
$this->runService($job);
}
} catch (\Exception $e) {
$isSuccess = false;
if ($e->getCode() === -1) {
$job->set('attempts', 0);
$skipLog = true;
} else {
$GLOBALS['log']->error('CronManager: Failed job running, job ['.$job->id.']. Error Details: '.$e->getMessage());
}
}
$status = $isSuccess ? self::SUCCESS : self::FAILED;
$job->set('status', $status);
if ($isSuccess) {
$job->set('executedAt', date('Y-m-d H:i:s'));
}
$this->getEntityManager()->saveEntity($job);
if ($job->get('scheduledJobId') && !$skipLog) {
$this->getCronScheduledJobUtil()->addLogRecord($job->get('scheduledJobId'), $status, null, $job->get('targetId'), $job->get('targetType'));
}
}
protected function runScheduledJob($job)
@@ -268,12 +383,6 @@ class CronManager
$methodNameDeprecated = $job->get('method');
$methodName = $job->get('methodName');
$isDeprecated = false;
if (!$methodName) {
$isDeprecated = true;
$methodName = $methodNameDeprecated;
}
if (!$methodName) {
throw new Error('Job with empty methodName.');
}
@@ -284,40 +393,40 @@ class CronManager
$data = $job->get('data');
if ($isDeprecated) {
$data = Json::decode(Json::encode($data), true);
}
$service->$methodName($data, $job->get('targetId'), $job->get('targetType'));
}
protected function createJobsFromScheduledJobs()
{
$activeScheduledJobList = $this->getCronScheduledJobUtil()->getActiveScheduledJobList();
$runningScheduledJobIdList = $this->getCronJobUtil()->getRunningScheduledJobIdList();
$createdJobIdList = array();
$createdJobIdList = [];
foreach ($activeScheduledJobList as $scheduledJob) {
$scheduling = $scheduledJob->get('scheduling');
$asSoonAsPossible = in_array($scheduling, $this->asSoonAsPossibleSchedulingList);
try {
$cronExpression = \Cron\CronExpression::factory($scheduling);
} catch (\Exception $e) {
$GLOBALS['log']->error('CronManager (ScheduledJob ['.$scheduledJob->id.']): Scheduling string error - '. $e->getMessage() . '.');
continue;
if ($asSoonAsPossible) {
$nextDate = date('Y-m-d H:i:s');
} else {
try {
$cronExpression = \Cron\CronExpression::factory($scheduling);
} catch (\Exception $e) {
$GLOBALS['log']->error('CronManager (ScheduledJob ['.$scheduledJob->id.']): Scheduling string error - '. $e->getMessage() . '.');
continue;
}
try {
$nextDate = $cronExpression->getNextRunDate()->format('Y-m-d H:i:s');
} catch (\Exception $e) {
$GLOBALS['log']->error('CronManager (ScheduledJob ['.$scheduledJob->id.']): Unsupported CRON expression ['.$scheduling.']');
continue;
}
$existingJob = $this->getCronJobUtil()->getJobByScheduledJobIdOnMinute($scheduledJob->id, $nextDate);
if ($existingJob) continue;
}
try {
$nextDate = $cronExpression->getNextRunDate()->format('Y-m-d H:i:s');
} catch (\Exception $e) {
$GLOBALS['log']->error('CronManager (ScheduledJob ['.$scheduledJob->id.']): Unsupported CRON expression ['.$scheduling.']');
continue;
}
$existingJob = $this->getCronJobUtil()->getJobByScheduledJob($scheduledJob->id, $nextDate);
if ($existingJob) continue;
$className = $this->getScheduledJobUtil()->get($scheduledJob->get('job'));
if ($className) {
if (method_exists($className, 'prepare')) {
@@ -331,13 +440,25 @@ class CronManager
continue;
}
$pendingCount = $this->getCronJobUtil()->getPendingCountByScheduledJobId($scheduledJob->id);
if ($asSoonAsPossible) {
if ($pendingCount > 0) {
continue;
}
} else {
if ($pendingCount > 1) {
continue;
}
}
$jobEntity = $this->getEntityManager()->getEntity('Job');
$jobEntity->set(array(
$jobEntity->set([
'name' => $scheduledJob->get('name'),
'status' => self::PENDING,
'scheduledJobId' => $scheduledJob->id,
'executeTime' => $nextDate
));
]);
$this->getEntityManager()->saveEntity($jobEntity);
}
}

View File

@@ -130,18 +130,21 @@ class DataManager
$metadata = $this->getContainer()->get('metadata');
$entityManager = $this->getContainer()->get('entityManager');
$jobs = $metadata->get(['entityDefs', 'ScheduledJob', 'jobs'], array());
$jobs = $metadata->get(['entityDefs', 'ScheduledJob', 'jobs'], []);
$systemJobNameList = [];
foreach ($jobs as $jobName => $defs) {
if ($jobName && !empty($defs['isSystem']) && !empty($defs['scheduling'])) {
$systemJobNameList[] = $jobName;
if (!$entityManager->getRepository('ScheduledJob')->where(array(
'job' => $jobName,
'status' => 'Active',
'scheduling' => $defs['scheduling']
))->findOne()) {
$job = $entityManager->getRepository('ScheduledJob')->where(array(
$job = $entityManager->getRepository('ScheduledJob')->where([
'job' => $jobName
))->findOne();
])->findOne();
if ($job) {
$entityManager->removeEntity($job);
}
@@ -150,28 +153,34 @@ class DataManager
$name = $defs['name'];
}
$job = $entityManager->getEntity('ScheduledJob');
$job->set(array(
$job->set([
'job' => $jobName,
'status' => 'Active',
'scheduling' => $defs['scheduling'],
'isInternal' => true,
'name' => $name
));
]);
$entityManager->saveEntity($job);
}
}
}
$internalScheduledJobList = $entityManager->getRepository('ScheduledJob')->where([
'isInternal' => true
])->find();
foreach ($internalScheduledJobList as $scheduledJob) {
$jobName = $scheduledJob->get('job');
if (!in_array($jobName, $systemJobNameList)) {
$entityManager->getRepository('ScheduledJob')->deleteFromDb($scheduledJob->id);
}
}
}
/**
* Update cache timestamp
*
* @return bool
*/
public function updateCacheTimestamp()
{
$this->getContainer()->get('config')->updateCacheTimestamp();
$this->getContainer()->get('config')->save();
return true;
}
@@ -193,6 +202,12 @@ class DataManager
$config->set('fullTextSearchMinLength', $fullTextSearchMinLength);
$cryptKey = $config->get('cryptKey');
if (!$cryptKey) {
$cryptKey = \Espo\Core\Utils\Util::generateKey();
$config->set('cryptKey', $cryptKey);
}
$config->save();
}
}

View File

@@ -0,0 +1,56 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://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\Formula\Functions\StringGroup;
use \Espo\Core\Exceptions\Error;
class TestType extends \Espo\Core\Formula\Functions\Base
{
public function process(\StdClass $item)
{
if (!property_exists($item, 'value') || !is_array($item->value)) {
throw new Error('Value for \'String\\Test\' item is not an array.');
}
if (count($item->value) < 2) {
throw new Error('Bad arguments passed to \'String\\Test\'.');
}
$string = $this->evaluate($item->value[0]);
$regexp = $this->evaluate($item->value[1]);
if (!is_string($string)) {
return false;
}
if (!is_string($regexp)) {
return false;
}
return !!preg_match($regexp, $string);
}
}

View File

@@ -75,6 +75,11 @@ class Htmlizer
return $this->entityManager;
}
protected function getMetadata()
{
return $this->metadata;
}
protected function format($value)
{
if (is_float($value)) {
@@ -87,36 +92,74 @@ class Htmlizer
return $value;
}
protected function getDataFromEntity(Entity $entity, $skipLinks = false)
protected function getDataFromEntity(Entity $entity, $skipLinks = false, $level = 0)
{
$data = $entity->toArray();
$fieldDefs = $entity->getFields();
$fieldList = array_keys($fieldDefs);
$attributeDefs = $entity->getAttributes();
$attributeList = array_keys($attributeDefs);
$forbidenAttributeList = [];
$forbiddenAttributeList = [];
$skipAttributeList = [];
$forbiddenLinkList = [];
if ($this->getAcl()) {
$forbidenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList($entity->getEntityType(), 'read');
$forbiddenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList($entity->getEntityType(), 'read');
$forbiddenAttributeList = array_merge(
$forbiddenAttributeList,
$this->getAcl()->getScopeRestrictedAttributeList($entity->getEntityType(), ['forbidden', 'internal', 'onlyAdmin'])
);
$forbiddenLinkList = $this->getAcl()->getScopeRestrictedLinkList($entity->getEntityType(), ['forbidden', 'internal', 'onlyAdmin']);
}
$relationList = $entity->getRelationList();
foreach ($fieldList as $field) {
if (in_array($field, $forbidenAttributeList)) continue;
if (!$skipLinks && $level === 0) {
foreach ($relationList as $relation) {
if (!$entity->hasLinkMultipleField($relation)) continue;
$type = $entity->getAttributeType($field);
$collection = $entity->getLinkMultipleCollection($relation);
$data[$relation] = $collection;
}
}
foreach ($data as $key => $value) {
if ($value instanceof \Espo\ORM\EntityCollection) {
$skipAttributeList[] = $key;
$collection = $value;
$list = [];
foreach ($collection as $item) {
$list[] = $this->getDataFromEntity($item, $skipLinks, $level + 1);
}
$data[$key] = $list;
}
}
foreach ($attributeList as $attribute) {
if (in_array($attribute, $forbiddenAttributeList)) {
unset($data[$attribute]);
continue;
}
if (in_array($attribute, $skipAttributeList)) {
continue;
}
$type = $entity->getAttributeType($attribute);
if ($type == Entity::DATETIME) {
if (!empty($data[$field])) {
$data[$field] = $this->dateTime->convertSystemDateTime($data[$field]);
if (!empty($data[$attribute])) {
$data[$attribute] = $this->dateTime->convertSystemDateTime($data[$attribute]);
}
} else if ($type == Entity::DATE) {
if (!empty($data[$field])) {
$data[$field] = $this->dateTime->convertSystemDate($data[$field]);
if (!empty($data[$attribute])) {
$data[$attribute] = $this->dateTime->convertSystemDate($data[$attribute]);
}
} else if ($type == Entity::JSON_ARRAY) {
if (!empty($data[$field])) {
$list = $data[$field];
if (!empty($data[$attribute])) {
$list = $data[$attribute];
$newList = [];
foreach ($list as $item) {
$v = $item;
@@ -133,42 +176,43 @@ class Htmlizer
$newList[] = $v;
}
$data[$field] = $newList;
$data[$attribute] = $newList;
}
} else if ($type == Entity::JSON_OBJECT) {
if (!empty($data[$field])) {
$value = $data[$field];
if (!empty($data[$attribute])) {
$value = $data[$attribute];
if ($value instanceof \StdClass) {
$data[$field] = json_decode(json_encode($value, \JSON_PRESERVE_ZERO_FRACTION), true);
$data[$attribute] = json_decode(json_encode($value, \JSON_PRESERVE_ZERO_FRACTION), true);
}
foreach ($data[$field] as $k => $w) {
foreach ($data[$attribute] as $k => $w) {
$keyRaw = $k . '_RAW';
$data[$field][$keyRaw] = $data[$field][$k];
$data[$field][$k] = $this->format($data[$field][$k]);
$data[$attribute][$keyRaw] = $data[$attribute][$k];
$data[$attribute][$k] = $this->format($data[$attribute][$k]);
}
}
} else if ($type === Entity::PASSWORD) {
unset($data[$field]);
unset($data[$attribute]);
}
if (array_key_exists($field, $data)) {
$keyRaw = $field . '_RAW';
$data[$keyRaw] = $data[$field];
if (array_key_exists($attribute, $data)) {
$keyRaw = $attribute . '_RAW';
$data[$keyRaw] = $data[$attribute];
$fieldType = $this->getFieldType($entity->getEntityType(), $field);
$fieldType = $this->getFieldType($entity->getEntityType(), $attribute);
if ($fieldType === 'enum') {
if ($this->language) {
$data[$field] = $this->language->translateOption($data[$field], $field, $entity->getEntityType());
$data[$attribute] = $this->language->translateOption($data[$attribute], $attribute, $entity->getEntityType());
}
}
$data[$field] = $this->format($data[$field]);
$data[$attribute] = $this->format($data[$attribute]);
}
}
if (!$skipLinks) {
$relationDefs = $entity->getRelations();
foreach ($entity->getRelationList() as $relation) {
if (in_array($relation, $forbiddenLinkList)) continue;
if (
!empty($relationDefs[$relation]['type'])
&&
@@ -180,7 +224,7 @@ class Htmlizer
if (!$this->getAcl()->check($relatedEntity, 'read')) continue;
}
$data[$relation] = $this->getDataFromEntity($relatedEntity, true);
$data[$relation] = $this->getDataFromEntity($relatedEntity, true, $level + 1);
}
}
}
@@ -188,7 +232,7 @@ class Htmlizer
return $data;
}
public function render(Entity $entity, $template, $id = null, $additionalData = array(), $skipLinks = false)
public function render(Entity $entity, $template, $id = null, $additionalData = [], $skipLinks = false)
{
$code = \LightnCandy::compile($template, [
'flags' => \LightnCandy::FLAG_HANDLEBARSJS,
@@ -303,4 +347,4 @@ class Htmlizer
if (!$this->metadata) return;
return $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']);
}
}
}

View File

@@ -31,9 +31,9 @@ namespace Espo\Core;
abstract class Injectable implements \Espo\Core\Interfaces\Injectable
{
protected $dependencyList = array();
protected $dependencyList = [];
protected $injections = array();
protected $injections = [];
public function inject($name, $object)
{

View File

@@ -51,8 +51,21 @@ class InjectableFactory
foreach ($dependencyList as $name) {
$service->inject($name, $this->container->get($name));
}
if (method_exists($service, 'prepare')) {
$service->prepare();
}
return $service;
}
throw new Error("Class '$className' does not exist");
}
protected function getMetadata()
{
return $this->getContainer()->get('metadata');
}
protected function getContainer()
{
return $this->container;
}
}

View File

@@ -35,7 +35,8 @@ class TemplateFileManager extends Base
{
$templateFileManager = new \Espo\Core\Utils\TemplateFileManager(
$this->getContainer()->get('config'),
$this->getContainer()->get('metadata')
$this->getContainer()->get('metadata'),
$this->getContainer()->get('fileManager')
);
return $templateFileManager;

View File

@@ -42,11 +42,14 @@ class Importer
private $filtersMatcher;
public function __construct($entityManager, $config)
private $notificator = null;
public function __construct($entityManager, $config, $notificator = null)
{
$this->entityManager = $entityManager;
$this->config = $config;
$this->filtersMatcher = new FiltersMatcher();
$this->notificator = $notificator;
}
protected function getEntityManager()
@@ -64,6 +67,11 @@ class Importer
return $this->filtersMatcher;
}
protected function getNotificator()
{
return $this->notificator;
}
public function importMessage($parserType = 'ZendMail', $message, $assignedUserId = null, $teamsIdList = [], $userIdList = [], $filterList = [], $fetchOnlyHeader = false, $folderData = null)
{
$parser = $message->getParser();
@@ -153,10 +161,14 @@ class Importer
}
}
$duplicate = null;
if ($duplicate = $this->findDuplicate($email)) {
$duplicate = $this->getEntityManager()->getEntity('Email', $duplicate->id);
$this->processDuplicate($duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList);
return $duplicate;
if ($duplicate->get('status') != 'Being Imported') {
$duplicate = $this->getEntityManager()->getEntity('Email', $duplicate->id);
$this->processDuplicate($duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList);
return $duplicate;
}
}
if ($parser->checkMessageAttribute($message, 'date')) {
@@ -280,11 +292,38 @@ class Importer
}
}
$this->getEntityManager()->getPdo()->query('LOCK TABLES `email` WRITE');
if (!$duplicate) {
$this->lockEmailTable();
if ($duplicate = $this->findDuplicate($email)) {
$this->unlockTables();
if ($duplicate->get('status') != 'Being Imported') {
$duplicate = $this->getEntityManager()->getEntity('Email', $duplicate->id);
$this->processDuplicate($duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList);
return $duplicate;
}
}
}
if ($duplicate) {
$duplicate->set([
'from' => $email->get('from'),
'to' => $email->get('to'),
'cc' => $email->get('cc'),
'bcc' => $email->get('bcc'),
'replyTo' => $email->get('replyTo'),
'name' => $email->get('name'),
'dateSent' => $email->get('dateSent'),
'body' => $email->get('body'),
'bodyPlain' => $email->get('bodyPlain'),
'parentType' => $email->get('parentType'),
'parentId' => $email->get('parentId'),
'isHtml' => $email->get('isHtml'),
'messageId' => $email->get('messageId'),
'fromString' => $email->get('fromString'),
'replyToString' => $email->get('replyToString'),
]);
$this->getEntityManager()->getRepository('Email')->fillAccount($duplicate);
if ($duplicate = $this->findDuplicate($email)) {
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
$duplicate = $this->getEntityManager()->getEntity('Email', $duplicate->id);
$this->processDuplicate($duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList);
return $duplicate;
}
@@ -292,13 +331,14 @@ class Importer
if (!$email->get('messageId')) {
$email->setDummyMessageId();
}
$email->set('status', 'Being Imported');
$this->getEntityManager()->saveEntity($email, [
'skipAll' => true,
'keepNew' => true
]);
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
$this->unlockTables();
if ($parentFound) {
$parentType = $email->get('parentType');
@@ -317,19 +357,34 @@ class Importer
}
}
$this->getEntityManager()->saveEntity($email);
$email->set('status', 'Archived');
$this->getEntityManager()->saveEntity($email, [
'isBeingImported' => true
]);
foreach ($inlineAttachmentList as $attachment) {
$attachment->set(array(
$attachment->set([
'relatedId' => $email->id,
'relatedType' => 'Email'
));
'relatedType' => 'Email',
'field' => 'body'
]);
$this->getEntityManager()->saveEntity($attachment);
}
return $email;
}
protected function lockEmailTable()
{
$this->getEntityManager()->getPdo()->query('LOCK TABLES `email` WRITE');
}
protected function unlockTables()
{
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
}
protected function findParent(Entity $email, $emailAddress)
{
$contact = $this->getEntityManager()->getRepository('Contact')->where(array(
@@ -370,9 +425,9 @@ class Importer
protected function findDuplicate(Entity $email)
{
if ($email->get('messageId')) {
$duplicate = $this->getEntityManager()->getRepository('Email')->select(['id'])->where(array(
$duplicate = $this->getEntityManager()->getRepository('Email')->select(['id', 'status'])->where([
'messageId' => $email->get('messageId')
))->findOne(['skipAdditionalSelectParams' => true]);
])->findOne(['skipAdditionalSelectParams' => true]);
if ($duplicate) {
return $duplicate;
}
@@ -381,30 +436,94 @@ class Importer
protected function processDuplicate(Entity $duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList)
{
if ($duplicate->get('status') == 'Archived') {
$this->getEntityManager()->getRepository('Email')->loadFromField($duplicate);
$this->getEntityManager()->getRepository('Email')->loadToField($duplicate);
}
$duplicate->loadLinkMultipleField('users');
$fetchedUserIdList = $duplicate->getLinkMultipleIdList('users');
$duplicate->setLinkMultipleIdList('users', []);
$processNoteAcl = false;
if ($assignedUserId) {
$duplicate->addLinkMultipleId('users', $assignedUserId);
if (!in_array($assignedUserId, $fetchedUserIdList)) {
$processNoteAcl = true;
$duplicate->addLinkMultipleId('users', $assignedUserId);
}
$duplicate->addLinkMultipleId('assignedUsers', $assignedUserId);
}
if (!empty($userIdList)) {
foreach ($userIdList as $uId) {
$duplicate->addLinkMultipleId('users', $uId);
if (!in_array($uId, $fetchedUserIdList)) {
$processNoteAcl = true;
$duplicate->addLinkMultipleId('users', $uId);
}
}
}
if ($folderData) {
foreach ($folderData as $uId => $folderId) {
$duplicate->setLinkMultipleColumn('users', 'folderId', $uId, $folderId);
if (!in_array($uId, $fetchedUserIdList)) {
$duplicate->setLinkMultipleColumn('users', 'folderId', $uId, $folderId);
} else {
$this->getEntityManager()->getRepository('Email')->updateRelation($duplicate, 'users', $uId, [
'folderId' => $folderId
]);
}
}
}
$duplicate->set('isBeingImported', true);
$this->getEntityManager()->saveEntity($duplicate);
$this->getEntityManager()->getRepository('Email')->applyUsersFilters($duplicate);
$this->getEntityManager()->getRepository('Email')->processLinkMultipleFieldSave($duplicate, 'users', [
'skipLinkMultipleRemove' => true,
'skipLinkMultipleUpdate' => true
]);
$this->getEntityManager()->getRepository('Email')->processLinkMultipleFieldSave($duplicate, 'assignedUsers', [
'skipLinkMultipleRemove' => true,
'skipLinkMultipleUpdate' => true
]);
if ($notificator = $this->getNotificator()) {
$notificator->process($duplicate, [
'isBeingImported' => true
]);
}
$fetchedTeamIdList = $duplicate->getLinkMultipleIdList('teams');
if (!empty($teamsIdList)) {
foreach ($teamsIdList as $teamId) {
$this->getEntityManager()->getRepository('Email')->relate($duplicate, 'teams', $teamId);
if (!in_array($teamId, $fetchedTeamIdList)) {
$processNoteAcl = true;
$this->getEntityManager()->getRepository('Email')->relate($duplicate, 'teams', $teamId);
}
}
}
if ($duplicate->get('parentType') && $processNoteAcl) {
$dt = new \DateTime();
$dt->modify('+5 seconds');
$executeAt = $dt->format('Y-m-d H:i:s');
$job = $this->getEntityManager()->getEntity('Job');
$job->set([
'serviceName' => 'Note',
'methodName' => 'processNoteAclJob',
'data' => [
'targetType' => 'Email',
'targetId' => $duplicate->id
],
'executeAt' => $executeAt,
'queue' => 'q1'
]);
$this->getEntityManager()->saveEntity($job);
}
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php
/************************************************************************
* This file is part of EspoCRM.
*
@@ -47,7 +47,7 @@ use Zend\Mime\Mime;
class XQueueItemId implements Header\HeaderInterface
{
protected $fieldName = 'X-QueueItemId';
protected $fieldName = 'X-Queue-Item-Id';
protected $id = null;
@@ -57,7 +57,7 @@ class XQueueItemId implements Header\HeaderInterface
$value = Header\HeaderWrap::mimeDecodeValue($value);
if (strtolower($name) !== 'x-queue-item-id') {
throw new Header\Exception\InvalidArgumentException('Invalid header line for Message-ID string');
throw new Header\Exception\InvalidArgumentException('Invalid header line for x-queue-item-id string');
}
$header = new static();

View File

@@ -176,7 +176,10 @@ class Sender
} else {
$fromName = $config->get('outboundEmailFromName');
}
$message->addFrom(trim($email->get('from')), $fromName);
$fromAddress = trim($email->get('from'));
} else {
if (!empty($params['fromAddress'])) {
$fromAddress = $params['fromAddress'];
@@ -194,12 +197,16 @@ class Sender
}
$message->addFrom($fromAddress, $fromName);
}
if (!$email->get('from')) {
$email->set('from', $fromAddress);
}
$fromString = '<' . $fromAddress . '>';
if ($fromName) {
$fromString = $fromName . ' ' . $fromString;
}
$email->set('fromString', $fromString);
$sender = new \Zend\Mail\Header\Sender();
$sender->setAddress($email->get('from'));
$message->getHeaders()->addHeader($sender);

View File

@@ -0,0 +1,58 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://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;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Utils\Util;
use \Espo\Core\InjectableFactory;
class NotificatorFactory extends InjectableFactory
{
public function create($entityType)
{
$normalizedName = Util::normilizeClassName($entityType);
$className = '\\Espo\\Custom\\Notificators\\' . $normalizedName;
if (!class_exists($className)) {
$moduleName = $this->getMetadata()->getScopeModuleName($entityType);
if ($moduleName) {
$className = '\\Espo\\Modules\\' . $moduleName . '\\Notificators\\' . $normalizedName;
} else {
$className = '\\Espo\\Notificators\\' . $normalizedName;
}
if (!class_exists($className)) {
$className = '\\Espo\\Core\\Notificators\\Base';
}
}
return $this->createByClassName($className);
}
}

View File

@@ -90,7 +90,7 @@ class Base implements Injectable
return $this->injections['user'];
}
public function process(Entity $entity)
public function process(Entity $entity, array $options = [])
{
if ($entity->hasLinkMultipleField('assignedUsers')) {
$userIdList = $entity->getLinkMultipleIdList('assignedUsers');

View File

@@ -33,12 +33,22 @@ class Entity extends \Espo\ORM\Entity
{
public function hasLinkMultipleField($field)
{
return $this->hasAttribute($field . 'Ids');
return
$this->hasRelation($field) &&
$this->getAttributeParam($field . 'Ids', 'isLinkMultipleIdList');
}
public function hasLinkField($field)
{
return $this->hasAttribute($field . 'Id');
return $this->hasAttribute($field . 'Id') && $this->hasRelation($field);
}
public function hasLinkParentField($field)
{
return
$this->hasAttributeType($field . 'Type') == 'foreignType' &&
$this->hasAttribute($field . 'Id') &&
$this->hasRelation($field);
}
public function loadParentNameField($field)
@@ -64,15 +74,28 @@ class Entity extends \Espo\ORM\Entity
}
}
public function loadLinkMultipleField($field, $columns = null)
public function getLinkMultipleCollection($field)
{
if (!$this->hasRelation($field) || !$this->hasAttribute($field . 'Ids')) return;
if (!$this->hasLinkMultipleField($field)) return;
$defs = array();
if (!empty($columns)) {
$defs['additionalColumns'] = $columns;
$defs = $this->getRelationSelectParams($field);
$columnAttribute = $field . 'Columns';
if ($this->hasAttribute($columnAttribute) && $this->getAttributeParam($columnAttribute, 'columns')) {
$defs['additionalColumns'] = $this->getAttributeParam($columnAttribute, 'columns');
}
$collection = $this->get($field, $defs);
return $collection;
}
protected function getRelationSelectParams($link)
{
$field = $link;
$defs = [];
$idsAttribute = $field . 'Ids';
$foreignEntityType = $this->getRelationParam($field, 'entity');
@@ -103,6 +126,19 @@ class Entity extends \Espo\ORM\Entity
}
}
return $defs;
}
public function loadLinkMultipleField($field, $columns = null)
{
if (!$this->hasRelation($field) || !$this->hasAttribute($field . 'Ids')) return;
$defs = $this->getRelationSelectParams($field);
if (!empty($columns)) {
$defs['additionalColumns'] = $columns;
}
$defs['select'] = ['id', 'name'];
$hasType = false;
@@ -136,6 +172,8 @@ class Entity extends \Espo\ORM\Entity
}
}
$idsAttribute = $field . 'Ids';
$this->set($idsAttribute, $ids);
if (!$this->isNew() && !$this->hasFetched($idsAttribute)) {
$this->setFetched($idsAttribute, $ids);
@@ -301,4 +339,3 @@ class Entity extends \Espo\ORM\Entity
return false;
}
}

View File

@@ -256,7 +256,7 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
}
}
protected function afterSave(Entity $entity, array $options = array())
protected function afterSave(Entity $entity, array $options = [])
{
if (!empty($this->restoreData)) {
$entity->set($this->restoreData);
@@ -267,7 +267,7 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
if (!$this->processFieldsAfterSaveDisabled) {
$this->processEmailAddressSave($entity);
$this->processPhoneNumberSave($entity);
$this->processSpecifiedRelationsSave($entity);
$this->processSpecifiedRelationsSave($entity, $options);
$this->processFileFieldsSave($entity);
$this->processArrayFieldsSave($entity);
$this->processWysiwygFieldsSave($entity);
@@ -454,112 +454,146 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
}
}
protected function processSpecifiedRelationsSave(Entity $entity)
public function processLinkMultipleFieldSave(Entity $entity, $link, array $options = [])
{
$name = $link;
$idListAttribute = $link . 'Ids';
$columnsAttribute = $link . 'Columns';
if ($this->getMetadata()->get("entityDefs." . $entity->getEntityType() . ".fields.{$name}.noSave")) {
return;
}
$skipCreate = false;
$skipRemove = false;
$skipUpdate = false;
if (!empty($options['skipLinkMultipleCreate'])) $skipCreate = true;
if (!empty($options['skipLinkMultipleRemove'])) $skipRemove = true;
if (!empty($options['skipLinkMultipleUpdate'])) $skipUpdate = true;
if ($entity->isNew()) {
$skipRemove = true;
$skipUpdate = true;
}
if ($entity->has($idListAttribute)) {
$specifiedIdList = $entity->get($idListAttribute);
} else if ($entity->has($columnsAttribute)) {
$skipRemove = true;
$specifiedIdList = [];
foreach ($entity->get($columnsAttribute) as $id => $d) {
$specifiedIdList[] = $id;
}
} else {
return;
}
if (!is_array($specifiedIdList)) return;
$toRemoveIdList = [];
$existingIdList = [];
$toUpdateIdList = [];
$toCreateIdList = [];
$existingColumnsData = (object)[];
$defs = [];
$columns = $this->getMetadata()->get("entityDefs." . $entity->getEntityType() . ".fields.{$name}.columns");
if (!empty($columns)) {
$columnData = $entity->get($columnsAttribute);
$defs['additionalColumns'] = $columns;
}
if (!$skipRemove && !$skipUpdate) {
$foreignEntityList = $entity->get($name, $defs);
if ($foreignEntityList) {
foreach ($foreignEntityList as $foreignEntity) {
$existingIdList[] = $foreignEntity->id;
if (!empty($columns)) {
$data = (object)[];
foreach ($columns as $columnName => $columnField) {
$foreignId = $foreignEntity->id;
$data->$columnName = $foreignEntity->get($columnField);
}
$existingColumnsData->$foreignId = $data;
if (!$entity->isNew()) {
$entity->setFetched($columnsAttribute, $existingColumnsData);
}
}
}
}
}
if (!$entity->isNew()) {
if ($entity->has($idListAttribute) && !$entity->hasFetched($idListAttribute)) {
$entity->setFetched($idListAttribute, $existingIdList);
}
if ($entity->has($columnsAttribute) && !empty($columns)) {
$entity->setFetched($columnsAttribute, $existingColumnsData);
}
}
foreach ($existingIdList as $id) {
if (!in_array($id, $specifiedIdList)) {
if (!$skipRemove) {
$toRemoveIdList[] = $id;
}
} else {
if (!$skipUpdate && !empty($columns)) {
foreach ($columns as $columnName => $columnField) {
if (isset($columnData->$id) && is_object($columnData->$id)) {
if (
property_exists($columnData->$id, $columnName)
&&
(
!property_exists($existingColumnsData->$id, $columnName)
||
$columnData->$id->$columnName !== $existingColumnsData->$id->$columnName
)
) {
$toUpdateIdList[] = $id;
}
}
}
}
}
}
if (!$skipCreate) {
foreach ($specifiedIdList as $id) {
if (!in_array($id, $existingIdList)) {
$toCreateIdList[] = $id;
}
}
}
foreach ($toCreateIdList as $id) {
$data = null;
if (!empty($columns) && isset($columnData->$id)) {
$data = $columnData->$id;
}
$this->relate($entity, $name, $id, $data);
}
foreach ($toRemoveIdList as $id) {
$this->unrelate($entity, $name, $id);
}
foreach ($toUpdateIdList as $id) {
$data = $columnData->$id;
$this->updateRelation($entity, $name, $id, $data);
}
}
protected function processSpecifiedRelationsSave(Entity $entity, array $options = [])
{
$relationTypeList = [$entity::HAS_MANY, $entity::MANY_MANY, $entity::HAS_CHILDREN];
foreach ($entity->getRelations() as $name => $defs) {
if (in_array($defs['type'], $relationTypeList)) {
$fieldName = $name . 'Ids';
$columnsFieldsName = $name . 'Columns';
if ($entity->has($fieldName) || $entity->has($columnsFieldsName)) {
if ($this->getMetadata()->get("entityDefs." . $entity->getEntityType() . ".fields.{$name}.noSave")) {
continue;
}
if ($entity->has($fieldName)) {
$specifiedIds = $entity->get($fieldName);
} else {
$specifiedIds = array();
foreach ($entity->get($columnsFieldsName) as $id => $d) {
$specifiedIds[] = $id;
}
}
if (is_array($specifiedIds)) {
$toRemoveIds = array();
$existingIds = array();
$toUpdateIds = array();
$existingColumnsData = new \stdClass();
$defs = array();
$columns = $this->getMetadata()->get("entityDefs." . $entity->getEntityType() . ".fields.{$name}.columns");
if (!empty($columns)) {
$columnData = $entity->get($columnsFieldsName);
$defs['additionalColumns'] = $columns;
}
$foreignCollection = $entity->get($name, $defs);
if ($foreignCollection) {
foreach ($foreignCollection as $foreignEntity) {
$existingIds[] = $foreignEntity->id;
if (!empty($columns)) {
$data = new \stdClass();
foreach ($columns as $columnName => $columnField) {
$foreignId = $foreignEntity->id;
$data->$columnName = $foreignEntity->get($columnField);
}
$existingColumnsData->$foreignId = $data;
if (!$entity->isNew()) {
$entity->setFetched($columnsFieldsName, $existingColumnsData);
}
}
}
}
if (!$entity->isNew()) {
if ($entity->has($fieldName)) {
$entity->setFetched($fieldName, $existingIds);
}
if ($entity->has($columnsFieldsName) && !empty($columns)) {
$entity->setFetched($columnsFieldsName, $existingColumnsData);
}
}
foreach ($existingIds as $id) {
if (!in_array($id, $specifiedIds)) {
$toRemoveIds[] = $id;
} else {
if (!empty($columns)) {
foreach ($columns as $columnName => $columnField) {
if (isset($columnData->$id) && is_object($columnData->$id)) {
if (
property_exists($columnData->$id, $columnName)
&&
(
!property_exists($existingColumnsData->$id, $columnName)
||
$columnData->$id->$columnName !== $existingColumnsData->$id->$columnName
)
) {
$toUpdateIds[] = $id;
}
}
}
}
}
}
foreach ($specifiedIds as $id) {
if (!in_array($id, $existingIds)) {
$data = null;
if (!empty($columns) && isset($columnData->$id)) {
$data = $columnData->$id;
}
$this->relate($entity, $name, $id, $data);
}
}
foreach ($toRemoveIds as $id) {
$this->unrelate($entity, $name, $id);
}
if (!empty($columns)) {
foreach ($toUpdateIds as $id) {
$data = $columnData->$id;
$this->updateRelation($entity, $name, $id, $data);
}
}
}
$idListAttribute = $name . 'Ids';
$columnsAttribute = $name . 'Columns';
if ($entity->has($idListAttribute) || $entity->has($columnsAttribute)) {
$this->processLinkMultipleFieldSave($entity, $name, $options);
}
} else if ($defs['type'] === $entity::HAS_ONE) {
if (empty($defs['entity']) || empty($defs['foreignKey'])) continue;
@@ -570,39 +604,37 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
$foreignEntityType = $defs['entity'];
$foreignKey = $defs['foreignKey'];
$idFieldName = $name . 'Id';
$nameFieldName = $name . 'Name';
$idAttribute = $name . 'Id';
if (!$entity->has($idFieldName)) continue;
if (!$entity->has($idAttribute)) continue;
$where = array();
$where = [];
$where[$foreignKey] = $entity->id;
$previousForeignEntity = $this->getEntityManager()->getRepository($foreignEntityType)->where($where)->findOne();
if ($previousForeignEntity) {
if (!$entity->isNew()) {
$entity->setFetched($idFieldName, $previousForeignEntity->id);
$entity->setFetched($idAttribute, $previousForeignEntity->id);
}
if ($previousForeignEntity->id !== $entity->get($idFieldName)) {
if ($previousForeignEntity->id !== $entity->get($idAttribute)) {
$previousForeignEntity->set($foreignKey, null);
$this->getEntityManager()->saveEntity($previousForeignEntity);
}
} else {
if (!$entity->isNew()) {
$entity->setFetched($idFieldName, null);
$entity->setFetched($idAttribute, null);
}
}
if ($entity->get($idFieldName)) {
$newForeignEntity = $this->getEntityManager()->getEntity($foreignEntityType, $entity->get($idFieldName));
if ($entity->get($idAttribute)) {
$newForeignEntity = $this->getEntityManager()->getEntity($foreignEntityType, $entity->get($idAttribute));
if ($newForeignEntity) {
$newForeignEntity->set($foreignKey, $entity->id);
$this->getEntityManager()->saveEntity($newForeignEntity);
} else {
$entity->set($idFieldName, null);
$entity->set($idAttribute, null);
}
}
}
}
}
}

View File

@@ -264,8 +264,7 @@ class AclManager extends \Espo\Core\AclManager
protected function checkUserIsNotPortal($user)
{
return !$user->get('isPortalUser');
return !$user->isPortal();
}
}

View File

@@ -136,8 +136,11 @@ class Base
protected function order($sortBy, $desc = false, &$result)
{
if (!empty($sortBy)) {
if (is_string($desc)) {
$desc = $desc === strtolower('desc');
}
if (!empty($sortBy)) {
$result['orderBy'] = $sortBy;
$type = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'fields', $sortBy, 'type']);
if (in_array($type, ['link', 'file', 'image'])) {
@@ -150,7 +153,7 @@ class Base
} else {
$orderPart = 'DESC';
}
$result['orderBy'] = [[$sortBy . 'Country', $orderPart], [$sortBy . 'City', $orderPart], [$sortBy . '_eet', $orderPart]];
$result['orderBy'] = [[$sortBy . 'Country', $orderPart], [$sortBy . 'City', $orderPart], [$sortBy . 'Street', $orderPart]];
return;
} else if ($type === 'enum') {
$list = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'fields', $sortBy, 'options']);
@@ -222,6 +225,8 @@ class Base
$whereClause = $this->convertWhere($where, false, $result);
$result['whereClause'] = array_merge($result['whereClause'], $whereClause);
$this->applyLeftJoinsFromWhere($where, $result);
}
public function convertWhere(array $where, $ignoreAdditionaFilterTypes = false, &$result = null)
@@ -423,7 +428,7 @@ class Base
protected function prepareResult(&$result)
{
if (empty($result)) {
$result = array();
$result = [];
}
if (empty($result['joins'])) {
$result['joins'] = [];
@@ -699,14 +704,26 @@ class Base
public function getSelectParams(array $params, $withAcl = false, $checkWherePermission = false)
{
$result = array();
$result = [];
$this->prepareResult($result);
if (!empty($params['sortBy'])) {
if (!array_key_exists('asc', $params)) {
$params['asc'] = true;
if (!empty($params['orderBy'])) {
$isDesc = false;
if (isset($params['order'])) {
$isDesc = $params['order'] === 'desc';
}
$this->order($params['sortBy'], !$params['asc'], $result);
$this->order($params['orderBy'], $isDesc, $result);
} else if (!empty($params['sortBy'])) {
if (isset($params['order'])) {
$isDesc = $params['order'] === 'desc';
} else if (isset($params['asc'])) {
$isDesc = $params['asc'] !== true;
}
$this->order($params['sortBy'], $isDesc, $result);
} else if (!empty($params['order'])) {
$orderBy = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'collection', 'orderBy']);
$isDesc = $params['order'] === 'desc';
$this->order($orderBy, $isDesc, $result);
}
if (!isset($params['offset'])) {
@@ -839,10 +856,12 @@ class Base
case 'today':
$where['type'] = 'between';
$dt->setTime(0, 0, 0);
$dtTo = clone $dt;
$dtTo->modify('+1 day -1 second');
$dt->setTimezone(new \DateTimeZone('UTC'));
$dtTo->setTimezone(new \DateTimeZone('UTC'));
$from = $dt->format($format);
$dt->modify('+1 day -1 second');
$to = $dt->format($format);
$to = $dtTo->format($format);
$where['value'] = [$from, $to];
break;
case 'past':
@@ -927,12 +946,13 @@ class Base
break;
case 'on':
$where['type'] = 'between';
$dt = new \DateTime($value, new \DateTimeZone($timeZone));
$dtTo = clone $dt;
$dtTo->modify('+1 day -1 second');
$dt->setTimezone(new \DateTimeZone('UTC'));
$dtTo->setTimezone(new \DateTimeZone('UTC'));
$from = $dt->format($format);
$dt->modify('+1 day -1 second');
$to = $dt->format($format);
$to = $dtTo->format($format);
$where['value'] = [$from, $to];
break;
case 'before':
@@ -944,6 +964,7 @@ class Base
case 'after':
$where['type'] = 'after';
$dt = new \DateTime($value, new \DateTimeZone($timeZone));
$dt->modify('+1 day -1 second');
$dt->setTimezone(new \DateTimeZone('UTC'));
$where['value'] = $dt->format($format);
break;
@@ -1228,6 +1249,49 @@ class Base
];
break;
case 'currentFiscalYear':
case 'lastFiscalYear':
$dtToday = new \DateTime();
$dt = new \DateTime();
$fiscalYearShift = $this->getConfig()->get('fiscalYearShift', 0);
$dt->modify('first day of January this year')->modify('+' . $fiscalYearShift . ' months');
if (intval($dtToday->format('m')) < $fiscalYearShift + 1) {
$dt->modify('-1 year');
}
if ($type === 'lastFiscalYear') {
$dt->modify('-1 year');
}
$part['AND'] = [
$attribute . '>=' => $dt->format('Y-m-d'),
$attribute . '<' => $dt->add(new \DateInterval('P1Y'))->format('Y-m-d')
];
break;
case 'currentFiscalQuarter':
case 'lastFiscalQuarter':
$dtToday = new \DateTime();
$dt = new \DateTime();
$fiscalYearShift = $this->getConfig()->get('fiscalYearShift', 0);
$dt->modify('first day of January this year')->modify('+' . $fiscalYearShift . ' months');
$month = intval($dtToday->format('m'));
$quarterShift = floor(($month - $fiscalYearShift - 1) / 3);
if ($quarterShift) {
if ($quarterShift >= 0) {
$dt->add(new \DateInterval('P'.($quarterShift * 3).'M'));
} else {
$quarterShift *= -1;
$dt->sub(new \DateInterval('P'.($quarterShift * 3).'M'));
}
}
if ($type === 'lastFiscalQuarter') {
$dt->modify('-3 months');
}
$part['AND'] = [
$attribute . '>=' => $dt->format('Y-m-d'),
$attribute . '<' => $dt->add(new \DateInterval('P3M'))->format('Y-m-d')
];
break;
case 'between':
if (is_array($value)) {
$part['AND'] = [
@@ -1908,4 +1972,119 @@ class Base
{
$this->filterFollowed($result);
}
public function mergeSelectParams($selectParams1, $selectParams2)
{
if (!$selectParams2) {
return $selectParams1;
}
if (!isset($selectParams1['whereClause'])) {
$selectParams1['whereClause'] = [];
}
if (!empty($selectParams2['whereClause'])) {
$selectParams1['whereClause'][] = $selectParams2['whereClause'];
}
if (!isset($selectParams1['havingClause'])) {
$selectParams1['havingClause'] = [];
}
if (!empty($selectParams2['havingClause'])) {
$selectParams1['havingClause'][] = $selectParams2['havingClause'];
}
if (!empty($selectParams2['leftJoins'])) {
foreach ($selectParams2['leftJoins'] as $item) {
$this->addLeftJoin($item, $selectParams1);
}
}
if (!empty($selectParams2['joins'])) {
foreach ($selectParams2['joins'] as $item) {
$this->addJoin($item, $selectParams1);
}
}
if (isset($selectParams2['select'])) {
$selectParams1['select'] = $selectParams2['select'];
}
if (isset($selectParams2['customJoin'])) {
if (!isset($selectParams1['customJoin'])) {
$selectParams1['customJoin'] = '';
}
$selectParams1['customJoin'] .= ' ' . $selectParams2['customJoin'];
}
if (isset($selectParams2['customWhere'])) {
if (!isset($selectParams1['customWhere'])) {
$selectParams1['customWhere'] = '';
}
$selectParams1['customWhere'] .= ' ' . $selectParams2['customWhere'];
}
if (isset($selectParams2['additionalSelectColumns'])) {
if (!isset($selectParams1['additionalSelectColumns'])) {
$selectParams1['additionalSelectColumns'] = [];
}
foreach ($selectParams2['additionalSelectColumns'] as $key => $item) {
$selectParams1['additionalSelectColumns'][$key] = $item;
}
}
if (isset($selectParams2['joinConditions'])) {
if (!isset($selectParams1['joinConditions'])) {
$selectParams1['joinConditions'] = [];
}
foreach ($selectParams2['joinConditions'] as $key => $item) {
$selectParams1['joinConditions'][$key] = $item;
}
}
if (isset($selectParams2['orderBy'])) {
$selectParams1['orderBy'] = $selectParams2['orderBy'];
}
if (isset($selectParams2['order'])) {
$selectParams1['order'] = $selectParams2['order'];
}
if (!empty($selectParams2['distinct'])) {
$selectParams1['distinct'] = true;
}
return $selectParams1;
}
protected function applyLeftJoinsFromWhere($where, &$result)
{
if (!is_array($where)) return;
foreach ($where as $item) {
$this->applyLeftJoinsFromWhereItem($item, $result);
}
}
protected function applyLeftJoinsFromWhereItem($item, &$result)
{
if (!empty($item['type'])) {
if (in_array($item['type'], ['or', 'and', 'not', 'having'])) {
if (!array_key_exists('value', $item) || !is_array($item['value'])) return;
foreach ($item['value'] as $listItem) {
$this->applyLeftJoinsFromWhereItem($listItem, $result);
}
return;
}
}
$attibute = null;
if (!empty($item['attribute'])) $attibute = $item['attribute'];
if (!$attibute) return;
$attributeType = $this->getSeed()->getAttributeType($attibute);
if ($attributeType === 'foreign') {
$relation = $this->getSeed()->getAttributeParam($attibute, 'relation');
if ($relation) {
$this->addLeftJoin($relation, $result);
}
}
}
}

View File

@@ -108,9 +108,11 @@ class ServiceFactory
foreach ($dependencies as $name) {
$service->inject($name, $this->container->get($name));
}
if (method_exists($service, 'prepare')) {
$service->prepare();
}
return $service;
}
throw new Error("Class '$className' does not exist.");
}
}

View File

@@ -37,6 +37,7 @@ abstract class Base implements Injectable
'config',
'entityManager',
'user',
'serviceFactory'
);
protected $injections = array();
@@ -55,6 +56,10 @@ abstract class Base implements Injectable
{
}
public function prepare()
{
}
protected function getInjection($name)
{
return $this->injections[$name];
@@ -91,5 +96,9 @@ abstract class Base implements Injectable
{
return $this->getInjection('user');
}
}
protected function getServiceFactory()
{
return $this->getInjection('serviceFactory');
}
}

View File

@@ -57,8 +57,8 @@
}
},
"collection": {
"sortBy": "createdAt",
"asc": false
"orderBy": "createdAt",
"order": "desc"
},
"indexes": {
"name": {

View File

@@ -75,8 +75,8 @@
}
},
"collection": {
"sortBy": "createdAt",
"asc": false
"orderBy": "createdAt",
"order": "desc"
},
"indexes": {
"name": {

View File

@@ -72,8 +72,8 @@
}
},
"collection": {
"sortBy": "parent",
"asc": true
"orderBy": "parent",
"order": "asc"
},
"indexes": {
"name": {

View File

@@ -136,8 +136,8 @@
}
},
"collection": {
"sortBy": "createdAt",
"asc": false
"orderBy": "createdAt",
"order": "desc"
},
"indexes": {
"name": {

View File

@@ -9,7 +9,6 @@
"type": "enum",
"options": ["Planned", "Held", "Not Held"],
"default": "Planned",
"view": "views/fields/enum-styled",
"style": {
"Held": "success"
},
@@ -101,8 +100,8 @@
}
},
"collection": {
"sortBy": "dateStart",
"asc": false
"orderBy": "dateStart",
"order": "desc"
},
"indexes": {
"dateStartStatus": {

View File

@@ -126,8 +126,8 @@
}
},
"collection": {
"sortBy": "createdAt",
"asc": false
"orderBy": "createdAt",
"order": "desc"
},
"indexes": {
"firstName": {

View File

@@ -0,0 +1,5 @@
{
"labels": {
"Create {entityType}": "ایجاد {entityTypeTranslated}"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "جلسات",
"calls": "تماس ها",
"tasks": "وظایف"
},
"labels": {
"Create {entityType}": "ایجاد {entityTypeTranslated}"
}
}

View File

@@ -0,0 +1,15 @@
{
"fields": {
"billingAddress": "آدرس صورتحساب",
"shippingAddress": "آدرس حمل و نقل",
"website": "وب سایت"
},
"links": {
"meetings": "جلسات",
"calls": "تماس ها",
"tasks": "وظایف"
},
"labels": {
"Create {entityType}": "ایجاد {entityTypeTranslated}"
}
}

View File

@@ -0,0 +1,34 @@
{
"fields": {
"parent": "پدر",
"dateStart": "تاریخ شروع",
"dateEnd": "تاریخ پایان",
"duration": "مدت زمان",
"status": "وضعیت",
"reminders": "یادآوری ها"
},
"links": {
"parent": "پدر"
},
"options": {
"status": {
"Planned": "برنامه ریزی شده",
"Held": "برگزار شد",
"Not Held": "برگزار نشد"
}
},
"labels": {
"Create {entityType}": "ایجاد {entityTypeTranslated}",
"Set Held": "تنظیم برگذاری",
"Set Not Held": "تنظیم عدم برگذاری"
},
"massActions": {
"setHeld": "تنظیم برگذاری",
"setNotHeld": "تنظیم عدم برگذاری"
},
"presetFilters": {
"planned": "برنامه ریزی شده",
"held": "برگزار شد",
"todays": "امروزه"
}
}

View File

@@ -0,0 +1,13 @@
{
"fields": {
"address": "آدرس"
},
"links": {
"meetings": "جلسات",
"calls": "تماس ها",
"tasks": "وظایف"
},
"labels": {
"Create {entityType}": "ایجاد {entityTypeTranslated}"
}
}

View File

@@ -0,0 +1,5 @@
{
"labels": {
"Create {entityType}": "Izveidot {entityTypeTranslated}"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Tikšanās",
"calls": "Zvani",
"tasks": "Uzdevumi"
},
"labels": {
"Create {entityType}": "Izveidot {entityTypeTranslated}"
}
}

View File

@@ -0,0 +1,15 @@
{
"fields": {
"billingAddress": "Norēķinu adrese",
"shippingAddress": "Piegādes adrese",
"website": "Vietne"
},
"links": {
"meetings": "Tikšanās",
"calls": "Zvani",
"tasks": "Uzdevumi"
},
"labels": {
"Create {entityType}": "Izveidot {entityTypeTranslated}"
}
}

View File

@@ -0,0 +1,36 @@
{
"fields": {
"parent": "Primārais elements",
"dateStart": "Sākuma datums",
"dateEnd": "Beigu datums",
"duration": "Ilgums",
"status": "Statuss",
"reminders": "Atgādinājumi"
},
"links": {
"parent": "Primārais elements"
},
"options": {
"status": {
"Planned": "Plānots",
"Held": "Noticis",
"Not Held": "Nenoticis"
}
},
"labels": {
"Create {entityType}": "Izveidot {entityTypeTranslated}",
"Schedule {entityType}": "Ieplānot {entityTypeTranslated}",
"Log {entityType}": "Reģistrēt {entityTypeTranslated}",
"Set Held": "Iestatīt kā notikušu",
"Set Not Held": "Iestatīt kā nenotikušu"
},
"massActions": {
"setHeld": "Iestatīt kā notikušu",
"setNotHeld": "Iestatīt kā nenotikušu"
},
"presetFilters": {
"planned": "Ieplānots",
"held": "Noticis",
"todays": "Šodienas"
}
}

View File

@@ -0,0 +1,13 @@
{
"fields": {
"address": "Adrese"
},
"links": {
"meetings": "Tikšanās",
"calls": "Zvani",
"tasks": "Uzdevumi"
},
"labels": {
"Create {entityType}": "Izveidot {entityTypeTranslated}"
}
}

View File

@@ -1,8 +1,8 @@
{
"links": {
"meetings": "Meetings",
"calls": "Calls",
"tasks": "Tasks"
"meetings": "Встречи",
"calls": "Звонки",
"tasks": "Задачи"
},
"labels": {
"Create {entityType}": "Создать {entityTypeTranslated}"

View File

@@ -2,12 +2,12 @@
"fields": {
"billingAddress": "Платежный адрес",
"shippingAddress": "Адрес доставки",
"website": "Website"
"website": "Сайт"
},
"links": {
"meetings": "Meetings",
"calls": "Calls",
"tasks": "Tasks"
"meetings": "Встречи",
"calls": "Звонки",
"tasks": "Задачи"
},
"labels": {
"Create {entityType}": "Создать {entityTypeTranslated}"

View File

@@ -1,36 +1,35 @@
{
"fields": {
"parent": "Parent",
"dateStart": "Date Start",
"dateEnd": "Date End",
"duration": "Duration",
"status": "Status",
"reminders": "Reminders"
"parent": "Источник",
"dateStart": "Дата начала",
"dateEnd": "Дата окончания",
"duration": "Длительность",
"status": "Статус",
"reminders": "Напоминания"
},
"links": {
"parent": "Parent"
"parent": "Источник"
},
"options": {
"status": {
"Planned": "Planned",
"Held": "Held",
"Not Held": "Not Held"
"Planned": "Запланировано",
"Held": "Состоялось",
"Not Held": "Не состоялся"
}
},
"labels": {
"Create {entityType}": "Создать {entityTypeTranslated}",
"Schedule {entityType}": "Schedule {entityTypeTranslated}",
"Log {entityType}": "Log {entityTypeTranslated}",
"Set Held": "Set Held",
"Set Not Held": "Set Not Held"
"Schedule {entityType}": "Запланировать {entityTypeTranslated}",
"Set Held": "Состоялось",
"Set Not Held": "Не состоялось"
},
"massActions": {
"setHeld": "Set Held",
"setNotHeld": "Set Not Held"
"setHeld": "Состоялось",
"setNotHeld": "Не состоялось"
},
"presetFilters": {
"planned": "Запланировано",
"held": "Held",
"held": "Состоялся",
"todays": "Сегодняшние"
}
}

View File

@@ -3,9 +3,9 @@
"address": "Адрес"
},
"links": {
"meetings": "Meetings",
"calls": "Calls",
"tasks": "Tasks"
"meetings": "Встречи",
"calls": "Звонки",
"tasks": "Задачи"
},
"labels": {
"Create {entityType}": "Создать {entityTypeTranslated}"

View File

@@ -164,7 +164,7 @@ abstract class Base
throw new Error('Another installation process is currently running.');
}
$this->processId = uniqid();
$this->processId = Util::generateId();
return $this->processId;
}
@@ -583,6 +583,17 @@ abstract class Base
return true;
}
protected function getManifestParam($name, $default = null)
{
$manifest = $this->getManifest();
if (array_key_exists($name, $manifest)) {
return $manifest[$name];
}
return $default;
}
/**
* Unzip a package archieve
*
@@ -726,4 +737,4 @@ abstract class Base
return $array;
}
}
}

View File

@@ -109,14 +109,18 @@ class Install extends \Espo\Core\Upgrades\Actions\Base
$this->afterRunAction();
$this->clearCache();
$this->finalize();
/* delete unziped files */
$this->deletePackageFiles();
$this->finalize();
if ($this->getManifestParam('skipBackup')) {
$this->getFileManager()->removeInDir($this->getPath('backupPath'), true);
}
$GLOBALS['log']->debug('Installation process ['.$processId.']: end run.');
$this->clearCache();
}
protected function restoreFiles()

View File

@@ -59,16 +59,15 @@ class Uninstall extends \Espo\Core\Upgrades\Actions\Base
$backupPath = $this->getPath('backupPath');
if (file_exists($backupPath)) {
/* copy core files */
if (!$this->copyFiles()) {
$this->throwErrorAndRemovePackage('Cannot copy files.');
}
}
/* remove extension files, saved in fileList */
if (!$this->deleteFiles('delete', true)) {
$this->throwErrorAndRemovePackage('Permission denied to delete files.');
}
/* remove extension files, saved in fileList */
if (!$this->deleteFiles('delete', true)) {
$this->throwErrorAndRemovePackage('Permission denied to delete files.');
}
if (!isset($data['skipSystemRebuild']) || !$data['skipSystemRebuild']) {
@@ -84,14 +83,14 @@ class Uninstall extends \Espo\Core\Upgrades\Actions\Base
$this->afterRunAction();
$this->clearCache();
/* delete backup files */
$this->deletePackageFiles();
$this->finalize();
$GLOBALS['log']->debug('Uninstallation process ['.$processId.']: end run.');
$this->clearCache();
}
protected function restoreFiles()

View File

@@ -48,32 +48,49 @@ class Auth extends \Slim\Middleware
function call()
{
$req = $this->app->request();
$request = $this->app->request();
$uri = $req->getResourceUri();
$httpMethod = $req->getMethod();
$uri = $request->getResourceUri();
$httpMethod = $request->getMethod();
$authUsername = $req->headers('PHP_AUTH_USER');
$authPassword = $req->headers('PHP_AUTH_PW');
$username = $request->headers('PHP_AUTH_USER');
$password = $request->headers('PHP_AUTH_PW');
$espoAuth = $req->headers('HTTP_ESPO_AUTHORIZATION');
if (isset($espoAuth)) {
list($authUsername, $authPassword) = explode(':', base64_decode($espoAuth), 2);
}
$authenticationMethod = null;
if (!isset($authUsername)) {
if (!empty($_COOKIE['auth-username']) && !empty($_COOKIE['auth-token'])) {
$authUsername = $_COOKIE['auth-username'];
$authPassword = $_COOKIE['auth-token'];
$espoAuthorizationHeader = $request->headers('Http-Espo-Authorization');
if (isset($espoAuthorizationHeader)) {
list($username, $password) = explode(':', base64_decode($espoAuthorizationHeader), 2);
} else {
$hmacAuthorizationHeader = $request->headers('X-Hmac-Authorization');
if ($hmacAuthorizationHeader) {
$authenticationMethod = 'Hmac';
list($username, $password) = explode(':', base64_decode($hmacAuthorizationHeader), 2);
} else {
$apiKeyHeader = $request->headers('X-Api-Key');
if ($apiKeyHeader) {
$authenticationMethod = 'ApiKey';
$username = $apiKeyHeader;
$password = null;
}
}
}
$espoCgiAuth = $req->headers('HTTP_ESPO_CGI_AUTH');
if (empty($espoCgiAuth)) {
$espoCgiAuth = $req->headers('REDIRECT_HTTP_ESPO_CGI_AUTH');
if (!isset($username)) {
if (!empty($_COOKIE['auth-username']) && !empty($_COOKIE['auth-token'])) {
$username = $_COOKIE['auth-username'];
$password = $_COOKIE['auth-token'];
}
}
if (!isset($authUsername) && !isset($authPassword) && !empty($espoCgiAuth)) {
list($authUsername, $authPassword) = explode(':' , base64_decode(substr($espoCgiAuth, 6)));
if (!isset($username) && !isset($password)) {
$espoCgiAuth = $request->headers('Http-Espo-Cgi-Auth');
if (empty($espoCgiAuth)) {
$espoCgiAuth = $request->headers('Redirect-Http-Espo-Cgi-Auth');
}
if (!empty($espoCgiAuth)) {
list($username, $password) = explode(':' , base64_decode(substr($espoCgiAuth, 6)));
}
}
if (is_null($this->authRequired)) {
@@ -83,9 +100,9 @@ class Auth extends \Slim\Middleware
$routeConditions = $routes[0]->getConditions();
if (isset($routeConditions['auth']) && $routeConditions['auth'] === false) {
if ($authUsername && $authPassword) {
if ($username && $password) {
try {
$isAuthenticated = $this->auth->login($authUsername, $authPassword);
$isAuthenticated = $this->auth->login($username, $password);
} catch (\Exception $e) {
$this->processException($e);
return;
@@ -109,9 +126,9 @@ class Auth extends \Slim\Middleware
}
}
if ($authUsername && $authPassword) {
if ($username) {
try {
$isAuthenticated = $this->auth->login($authUsername, $authPassword);
$isAuthenticated = $this->auth->login($username, $password, $authenticationMethod);
} catch (\Exception $e) {
$this->processException($e);
return;
@@ -154,9 +171,8 @@ class Auth extends \Slim\Middleware
{
$request = $this->app->request();
$httpXRequestedWith = $request->headers('HTTP_X_REQUESTED_WITH');
if (isset($httpXRequestedWith) && strtolower($httpXRequestedWith) == 'xmlhttprequest') {
$httpXRequestedWith = $request->headers('Http-X-Requested-With');
if ($httpXRequestedWith && strtolower($httpXRequestedWith) == 'xmlhttprequest') {
return true;
}

View File

@@ -0,0 +1,83 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://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\Utils;
class ApiKey
{
private $config;
public function __construct(\Espo\Core\Utils\Config $config)
{
$this->config = $config;
}
protected function getConfig()
{
return $this->config;
}
public static function hash($secretKey, $string = '')
{
return hash_hmac('sha256', $string, $secretKey, true);
}
public function getSecretKeyForUserId($id)
{
$apiSecretKeys = $this->getConfig()->get('apiSecretKeys');
if (!$apiSecretKeys) return;
if (!is_object($apiSecretKeys)) return;
if (!isset($apiSecretKeys->$id)) return;
return $apiSecretKeys->$id;
}
public function storeSecretKeyForUserId($id, $secretKey)
{
$apiSecretKeys = $this->getConfig()->get('apiSecretKeys');
if (!is_object($apiSecretKeys)) {
$apiSecretKeys = (object)[];
}
$apiSecretKeys->$id = $secretKey;
$this->getConfig()->set('apiSecretKeys', $apiSecretKeys);
$this->getConfig()->save();
}
public function removeSecretKeyForUserId($id)
{
$apiSecretKeys = $this->getConfig()->get('apiSecretKeys');
if (!is_object($apiSecretKeys)) {
$apiSecretKeys = (object)[];
}
unset($apiSecretKeys->$id);
$this->getConfig()->set('apiSecretKeys', $apiSecretKeys);
$this->getConfig()->save();
}
}

View File

@@ -38,8 +38,6 @@ class Auth
{
protected $container;
protected $authentication;
protected $allowAnyAccess;
const ACCESS_CRM_ONLY = 0;
@@ -60,10 +58,6 @@ class Auth
$this->allowAnyAccess = $allowAnyAccess;
$authenticationMethod = $this->getConfig()->get('authenticationMethod', 'Espo');
$authenticationClassName = "\\Espo\\Core\\Utils\\Authentication\\" . $authenticationMethod;
$this->authentication = new $authenticationClassName($this->getConfig(), $this->getEntityManager(), $this);
$this->request = $container->get('slim')->request();
}
@@ -72,6 +66,25 @@ class Auth
return $this->container;
}
protected function getDefaultAuthenticationMethod()
{
return $this->getConfig()->get('authenticationMethod', 'Espo');
}
protected function getAuthentication($authenticationMethod)
{
$authenticationMethod = preg_replace('/[^a-zA-Z0-9]+/', '', $authenticationMethod);
$authenticationClassName = "\\Espo\\Custom\\Core\\Utils\\Authentication\\" . $authenticationMethod;
if (!class_exists($authenticationClassName)) {
$authenticationClassName = "\\Espo\\Core\\Utils\\Authentication\\" . $authenticationMethod;
}
$authentication = new $authenticationClassName($this->getConfig(), $this->getEntityManager(), $this);
return $authentication;
}
protected function setPortal(Portal $portal)
{
$this->portal = $portal;
@@ -119,23 +132,29 @@ class Auth
$this->getContainer()->setUser($user);
}
public function login($username, $password)
public function login($username, $password = null, $authenticationMethod = null)
{
$isByTokenOnly = false;
if ($this->request->headers->get('HTTP_ESPO_AUTHORIZATION_BY_TOKEN') === 'true') {
$isByTokenOnly = true;
if (!$authenticationMethod) {
if ($this->request->headers->get('Http-Espo-Authorization-By-Token') === 'true') {
$isByTokenOnly = true;
}
}
if (!$isByTokenOnly) {
$this->checkFailedAttemptsLimit($username);
}
$authToken = $this->getEntityManager()->getRepository('AuthToken')->where([
'token' => $password
])->findOne();
$authToken = null;
$authTokenIsFound = false;
if (!$authenticationMethod) {
$authToken = $this->getEntityManager()->getRepository('AuthToken')->where([
'token' => $password
])->findOne();
}
if ($authToken) {
$authTokenIsFound = true;
}
@@ -168,12 +187,22 @@ class Auth
return;
}
$user = $this->authentication->login($username, $password, $authToken, $this->isPortal());
if (!$authenticationMethod) {
$authenticationMethod = $this->getDefaultAuthenticationMethod();
}
$authentication = $this->getAuthentication($authenticationMethod);
$params = [
'isPortal' => $this->isPortal()
];
$user = $authentication->login($username, $password, $authToken, $params, $this->request);
$authLogRecord = null;
if (!$authTokenIsFound) {
$authLogRecord = $this->createAuthLogRecord($username, $user);
$authLogRecord = $this->createAuthLogRecord($username, $user, $authenticationMethod);
}
if (!$user) {
@@ -190,20 +219,20 @@ class Auth
return;
}
if (!$user->isAdmin() && !$this->isPortal() && $user->get('isPortalUser')) {
if (!$user->isAdmin() && !$this->isPortal() && $user->isPortal()) {
$GLOBALS['log']->info("AUTH: Trying to login to crm as a portal user '".$user->get('userName')."'.");
$this->logDenied($authLogRecord, 'IS_PORTAL_USER');
return;
}
if (!$user->isAdmin() && $this->isPortal() && !$user->get('isPortalUser')) {
if ($this->isPortal() && !$user->isPortal()) {
$GLOBALS['log']->info("AUTH: Trying to login to portal as user '".$user->get('userName')."' which is not portal user.");
$this->logDenied($authLogRecord, 'IS_NOT_PORTAL_USER');
return;
}
if ($this->isPortal()) {
if (!$user->isAdmin() && !$this->getEntityManager()->getRepository('Portal')->isRelated($this->getPortal(), 'users', $user)) {
if (!$this->getEntityManager()->getRepository('Portal')->isRelated($this->getPortal(), 'users', $user)) {
$GLOBALS['log']->info("AUTH: Trying to login to portal as user '".$user->get('userName')."' which is portal user but does not belongs to portal.");
$this->logDenied($authLogRecord, 'USER_IS_NOT_IN_PORTAL');
return;
@@ -218,7 +247,7 @@ class Auth
$this->getEntityManager()->setUser($user);
$this->getContainer()->setUser($user);
if ($this->request->headers->get('HTTP_ESPO_AUTHORIZATION')) {
if ($this->request->headers->get('Http-Espo-Authorization')) {
if (!$authToken) {
$authToken = $this->getEntityManager()->getEntity('AuthToken');
$token = $this->generateToken();
@@ -313,7 +342,7 @@ class Auth
}
}
protected function createAuthLogRecord($username, $user)
protected function createAuthLogRecord($username, $user, $authenticationMethod = null)
{
if ($username === '**logout') return;
@@ -324,7 +353,8 @@ class Auth
'ipAddress' => $_SERVER['REMOTE_ADDR'],
'requestTime' => $_SERVER['REQUEST_TIME_FLOAT'],
'requestMethod' => $this->request->getMethod(),
'requestUrl' => $this->request->getUrl() . $this->request->getPath()
'requestUrl' => $this->request->getUrl() . $this->request->getPath(),
'authenticationMethod' => $authenticationMethod
]);
if ($this->isPortal()) {

View File

@@ -0,0 +1,50 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://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\Utils\Authentication;
use \Espo\Core\Exceptions\Error;
class ApiKey extends Base
{
public function login($username, $password, $authToken = null, $params = [], $request)
{
$apiKey = $username;
$user = $this->getEntityManager()->getRepository('User')->findOne([
'whereClause' => [
'type' => 'api',
'apiKey' => $apiKey,
'authMethod' => 'ApiKey'
]
]);
return $user;
}
}

View File

@@ -33,20 +33,23 @@ use \Espo\Core\Exceptions\Error;
class Espo extends Base
{
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null, $isPortal = null)
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null, $params = [], $request)
{
if (!$password) return;
if ($authToken) {
$hash = $authToken->get('hash');
} else {
$hash = $this->getPasswordHash()->hash($password);
}
$user = $this->getEntityManager()->getRepository('User')->findOne(array(
'whereClause' => array(
$user = $this->getEntityManager()->getRepository('User')->findOne([
'whereClause' => [
'userName' => $username,
'password' => $hash
)
));
'password' => $hash,
'type!=' => ['api', 'system']
]
]);
return $user;
}

View File

@@ -0,0 +1,67 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://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\Utils\Authentication;
use \Espo\Core\Exceptions\Error;
class Hmac extends Base
{
public function login($username, $password, $authToken = null, $params = [], $request)
{
$apiKey = $username;
$hash = $password;
$user = $this->getEntityManager()->getRepository('User')->findOne([
'whereClause' => [
'type' => 'api',
'apiKey' => $apiKey,
'authMethod' => 'Hmac'
]
]);
if (!$user) return;
if ($user) {
$apiKeyUtil = new \Espo\Core\Utils\ApiKey($this->getConfig());
$secretKey = $apiKeyUtil->getSecretKeyForUserId($user->id);
if (!$secretKey) return;
$string = $request->getMethod() . ' ' . $request->getResourceUri();
if ($hash === \Espo\Core\Utils\ApiKey::hash($secretKey, $string)) {
return $user;
}
return;
}
return $user;
}
}

View File

@@ -110,8 +110,12 @@ class LDAP extends Espo
*
* @return \Espo\Entities\User | null
*/
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null, $isPortal = null)
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null, $params = [], $request)
{
if (!$password) return;
$isPortal = !empty($params['isPortal']);
if ($authToken) {
return $this->loginByToken($username, $authToken);
}
@@ -119,7 +123,7 @@ class LDAP extends Espo
if ($isPortal) {
$useLdapAuthForPortalUser = $this->getUtils()->getOption('portalUserLdapAuth');
if (!$useLdapAuthForPortalUser) {
return parent::login($username, $password, $authToken, $isPortal);
return parent::login($username, $password, $authToken, $params, $request);
}
}
@@ -168,14 +172,14 @@ class LDAP extends Espo
}
}
$user = $this->getEntityManager()->getRepository('User')->findOne(array(
'whereClause' => array(
$user = $this->getEntityManager()->getRepository('User')->findOne([
'whereClause' => [
'userName' => $username,
),
));
'type!=' => ['api', 'system']
]
]);
$isCreateUser = $this->getUtils()->getOption('createEspoUser');
if (!isset($user) && $isCreateUser) {
if (!isset($user) && $this->getUtils()->getOption('createEspoUser')) {
$userData = $ldapClient->getEntry($userDn);
$user = $this->createUser($userData, $isPortal);
}
@@ -226,13 +230,13 @@ class LDAP extends Espo
{
$hash = $this->getPasswordHash()->hash($password);
$user = $this->getEntityManager()->getRepository('User')->findOne(array(
'whereClause' => array(
$user = $this->getEntityManager()->getRepository('User')->findOne([
'whereClause' => [
'userName' => $username,
'password' => $hash,
'isAdmin' => 1
)
));
'type' => ['admin', 'super-admin']
]
]);
return $user;
}
@@ -266,7 +270,7 @@ class LDAP extends Espo
//set user fields
if ($isPortal) {
$userFields = $this->loadFields('portalUser');
$userFields['isPortalUser'] = true;
$userFields['type'] = 'portal';
} else {
$userFields = $this->loadFields('user');
}

View File

@@ -31,12 +31,6 @@ namespace Espo\Core\Utils;
class Config
{
/**
* Path of default config file
*
* @access private
* @var string
*/
private $defaultConfigPath = 'application/Espo/Core/defaults/config.php';
private $systemConfigPath = 'application/Espo/Core/defaults/systemConfig.php';
@@ -45,13 +39,7 @@ class Config
private $cacheTimestamp = 'cacheTimestamp';
/**
* Array of admin items
*
* @access protected
* @var array
*/
protected $adminItems = array();
protected $adminItems = [];
protected $associativeArrayAttributeList = [
'currencyRates',
@@ -61,21 +49,16 @@ class Config
];
/**
* Contains content of config
*
* @access private
* @var array
*/
private $data;
private $changedData = array();
private $removeData = array();
private $changedData = [];
private $removeData = [];
private $fileManager;
public function __construct(\Espo\Core\Utils\File\Manager $fileManager) //TODO
public function __construct(\Espo\Core\Utils\File\Manager $fileManager)
{
$this->fileManager = $fileManager;
}
@@ -228,11 +211,6 @@ class Config
return $this->getFileManager()->getPhpContents($this->defaultConfigPath);
}
/**
* Return an Object of all configs
* @param boolean $reload
* @return array()
*/
protected function loadConfig($reload = false)
{
if (!$reload && isset($this->data) && !empty($this->data)) {
@@ -249,50 +227,25 @@ class Config
return $this->data;
}
public function getAllData()
{
return (object) $this->loadConfig();
}
/**
* Get config acording to restrictions for a user
*
* @param $isAdmin
* @return array
*/
public function getData($isAdmin = null)
{
$data = $this->loadConfig();
$restrictedConfig = $data;
foreach($this->getRestrictItems($isAdmin) as $name) {
if (isset($restrictedConfig[$name])) {
unset($restrictedConfig[$name]);
}
}
return $restrictedConfig;
return $data;
}
/**
* Set JSON data acording to restrictions for a user
*
* @param $isAdmin
* @return bool
*/
public function setData($data, $isAdmin = null)
public function setData($data)
{
$restrictItems = $this->getRestrictItems($isAdmin);
if (is_object($data)) {
$data = get_object_vars($data);
}
$values = array();
foreach ($data as $key => $item) {
if (!in_array($key, $restrictItems)) {
$values[$key] = $item;
}
}
return $this->set($values);
return $this->set($data);
}
/**
@@ -303,9 +256,9 @@ class Config
*/
public function updateCacheTimestamp($onlyValue = false)
{
$timestamp = array(
$this->cacheTimestamp => time(),
);
$timestamp = [
$this->cacheTimestamp => time()
];
if ($onlyValue) {
return $timestamp;
@@ -314,45 +267,24 @@ class Config
return $this->set($timestamp);
}
/**
* Get admin items
*
* @return object
*/
protected function getRestrictItems($onlySystemItems = null)
public function getAdminOnlyItemList()
{
$data = $this->loadConfig();
if ($onlySystemItems) {
return $data['systemItems'];
}
if (empty($this->adminItems)) {
$this->adminItems = array_merge($data['systemItems'], $data['adminItems']);
}
if ($onlySystemItems === false) {
return $this->adminItems;
}
return array_merge($this->adminItems, $data['userItems']);
return $this->get('adminItems', []);
}
/**
* Check if an item is allowed to get and save
*
* @param $name
* @param $isAdmin
* @return bool
*/
protected function isAllowed($name, $isAdmin = false)
public function getSuperAdminOnlyItemList()
{
if (in_array($name, $this->getRestrictItems($isAdmin))) {
return false;
}
return $this->get('superAdminItems', []);
}
return true;
public function getSystemOnlyItemList()
{
return $this->get('systemItems', []);
}
public function getUserOnlyItemList()
{
return $this->get('userItems', []);
}
public function getSiteUrl()

View File

@@ -0,0 +1,72 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://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\Utils;
class ControllerUtil
{
public static function fetchListParamsFromRequest(&$params, $request, $data)
{
$params['where'] = $request->get('where');
$params['maxSize'] = $request->get('maxSize');
$params['offset'] = $request->get('offset');
if ($request->get('orderBy')) {
$params['orderBy'] = $request->get('orderBy');
} else if ($request->get('sortBy')) {
$params['orderBy'] = $request->get('sortBy');
}
if ($request->get('order')) {
$params['order'] = $request->get('order');
} else if ($request->get('asc')) {
$params['order'] = $request->get('asc') === 'true' ? 'asc' : 'desc';
}
if ($request->get('q')) {
$params['q'] = trim($request->get('q'));
}
if ($request->get('textFilter')) {
$params['textFilter'] = $request->get('textFilter');
}
if ($request->get('primaryFilter')) {
$params['primaryFilter'] = $request->get('primaryFilter');
}
if ($request->get('boolFilterList')) {
$params['boolFilterList'] = $request->get('boolFilterList');
}
if ($request->get('filterList')) {
$params['filterList'] = $request->get('filterList');
}
if ($request->get('select')) {
$params['select'] = explode(',', $request->get('select'));
}
}
}

View File

@@ -43,6 +43,10 @@ class Job
private $cronScheduledJob;
const NOT_EXISTING_PROCESS_PERIOD = 300;
const READY_NOT_STARTED_PERIOD = 60;
public function __construct(Config $config, EntityManager $entityManager)
{
$this->config = $config;
@@ -74,10 +78,8 @@ class Job
])->findOne();
}
public function getPendingJobList()
public function getPendingJobList($queue = null, $limit = 0)
{
$limit = intval($this->getConfig()->get('jobMaxPortion', 0));
$selectParams = [
'select' => [
'id',
@@ -87,15 +89,15 @@ class Job
'targetId',
'targetType',
'methodName',
'method', // TODO remove deprecated
'serviceName',
'data'
],
'whereClause' => [
'status' => CronManager::PENDING,
'executeTime<=' => date('Y-m-d H:i:s')
'executeTime<=' => date('Y-m-d H:i:s'),
'queue' => $queue
],
'orderBy' => 'executeTime'
'orderBy' => 'number'
];
if ($limit) {
$selectParams['offset'] = 0;
@@ -109,7 +111,7 @@ class Job
{
$where = [
'scheduledJobId' => $scheduledJobId,
'status' => CronManager::RUNNING
'status' => [CronManager::RUNNING, CronManager::READY]
];
if ($targetId && $targetType) {
$where['targetId'] = $targetId;
@@ -127,7 +129,7 @@ class Job
$query = "
SELECT scheduled_job_id FROM job
WHERE
`status` = 'Running' AND
(`status` = 'Running' OR `status` = 'Ready') AND
scheduled_job_id IS NOT NULL AND
target_id IS NULL AND
deleted = 0
@@ -145,14 +147,13 @@ class Job
}
/**
* Get Jobs by ScheduledJobId and date
*
* @param string $scheduledJobId
* @param string $time
*
* @return array
*/
public function getJobByScheduledJob($scheduledJobId, $time)
public function getJobByScheduledJobIdOnMinute($scheduledJobId, $time)
{
$dateObj = new \DateTime($time);
$timeWithoutSeconds = $dateObj->format('Y-m-d H:i:');
@@ -176,69 +177,141 @@ class Job
return $scheduledJob;
}
/**
* Mark pending jobs (all jobs that exceeded jobPeriod)
*
* @return void
*/
public function markFailedJobs()
public function getPendingCountByScheduledJobId($scheduledJobId)
{
$this->markFailedJobsByPeriod('jobPeriodForActiveProcess');
$this->markFailedJobsByPeriod('jobPeriod');
$countPending = $this->getEntityManager()->getRepository('Job')->where([
'scheduledJobId' => $scheduledJobId,
'status' => CronManager::PENDING
])->count();
return $countPending;
}
protected function markFailedJobsByPeriod($period)
public function markJobsFailed()
{
$time = time() - $this->getConfig()->get($period);
$this->markJobsFailedByNotExistingProcesses();
$this->markJobsFailedReadyNotStarted();
$this->markJobsFailedByPeriod(true);
$this->markJobsFailedByPeriod();
}
$pdo = $this->getEntityManager()->getPDO();
protected function markJobsFailedByNotExistingProcesses()
{
$timeThreshold = time() - $this->getConfig()->get('jobPeriodForNotExistingProcess', self::NOT_EXISTING_PROCESS_PERIOD);
$dateTimeThreshold = date('Y-m-d H:i:s', $timeThreshold);
$select = "
SELECT id, scheduled_job_id, execute_time, target_id, target_type, pid FROM `job`
WHERE
`status` = '" . CronManager::RUNNING ."' AND execute_time < '".date('Y-m-d H:i:s', $time)."'
";
$sth = $pdo->prepare($select);
$sth->execute();
$runningJobList = $this->getEntityManager()->getRepository('Job')->select([
'id',
'scheduledJobId',
'executeTime',
'targetId',
'targetType',
'pid',
'startedAt'
])->where([
'status' => CronManager::RUNNING,
'startedAt<' => $dateTimeThreshold
])->find();
$jobData = array();
switch ($period) {
case 'jobPeriod':
while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
if (empty($row['pid']) || !System::isProcessActive($row['pid'])) {
$jobData[$row['id']] = $row;
}
}
break;
case 'jobPeriodForActiveProcess':
while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
$jobData[$row['id']] = $row;
}
break;
$failedJobList = [];
foreach ($runningJobList as $job) {
if ($job->get('pid') && !System::isProcessActive($job->get('pid'))) {
$failedJobList[] = $job;
}
}
if (!empty($jobData)) {
$jobQuotedIdList = [];
foreach ($jobData as $jobId => $job) {
$jobQuotedIdList[] = $pdo->quote($jobId);
}
$this->markJobListFailed($failedJobList);
}
$update = "
UPDATE job
SET `status` = '" . CronManager::FAILED . "', attempts = 0
WHERE id IN (".implode(", ", $jobQuotedIdList).")
";
protected function markJobsFailedReadyNotStarted()
{
$timeThreshold = time() - $this->getConfig()->get('jobPeriodForReadyNotStarted', SELF::READY_NOT_STARTED_PERIOD);
$dateTimeThreshold = date('Y-m-d H:i:s', $timeThreshold);
$sth = $pdo->prepare($update);
$sth->execute();
$failedJobList = $this->getEntityManager()->getRepository('Job')->select([
'id',
'scheduledJobId',
'executeTime',
'targetId',
'targetType',
'pid',
'startedAt'
])->where([
'status' => CronManager::READY,
'startedAt<' => $dateTimeThreshold
])->find();
$cronScheduledJob = $this->getCronScheduledJob();
foreach ($jobData as $jobId => $job) {
if (!empty($job['scheduled_job_id'])) {
$cronScheduledJob->addLogRecord($job['scheduled_job_id'], CronManager::FAILED, $job['execute_time'], $job['target_id'], $job['target_type']);
$this->markJobListFailed($failedJobList);
}
protected function markJobsFailedByPeriod($isForActiveProcesses = false)
{
$period = 'jobPeriod';
if ($isForActiveProcesses) {
$period = 'jobPeriodForActiveProcess';
}
$timeThreshold = time() - $this->getConfig()->get($period, 7800);
$dateTimeThreshold = date('Y-m-d H:i:s', $timeThreshold);
$runningJobList = $this->getEntityManager()->getRepository('Job')->select([
'id',
'scheduledJobId',
'executeTime',
'targetId',
'targetType',
'pid',
'startedAt'
])->where([
'status' => CronManager::RUNNING,
'executeTime<' => $dateTimeThreshold
])->find();
$failedJobList = [];
foreach ($runningJobList as $job) {
if (!$isForActiveProcesses) {
if (!$job->get('pid') || !System::isProcessActive($job->get('pid'))) {
$failedJobList[] = $job;
}
} else {
$failedJobList[] = $job;
}
}
$this->markJobListFailed($failedJobList);
}
protected function markJobListFailed($jobList)
{
if (!count($jobList)) return;
$jobIdList = [];
foreach ($jobList as $job) {
$jobIdList[] = $job->id;
}
$quotedIdList = [];
foreach ($jobIdList as $id) {
$quotedIdList[] = $this->getEntityManager()->getPDO()->quote($id);
}
$sql = "
UPDATE job
SET `status` = '" . CronManager::FAILED . "', attempts = 0
WHERE id IN (".implode(", ", $quotedIdList).")
";
$this->getEntityManager()->getPDO()->query($sql);
foreach ($jobList as $job) {
if ($job->get('scheduledJobId')) {
$this->getCronScheduledJob()->addLogRecord(
$job->get('scheduledJobId'),
CronManager::FAILED,
$job->get('startedAt'),
$job->get('targetId'),
$job->get('targetType')
);
}
}
}
@@ -273,7 +346,6 @@ class Job
foreach ($duplicateJobList as $row) {
if (!empty($row['scheduled_job_id'])) {
/* no possibility to use limit in update or subqueries */
$query = "
SELECT id FROM `job`
WHERE
@@ -349,9 +421,4 @@ class Job
}
}
}
public function getPid()
{
return System::getPid();
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://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\Utils\Cron;
class JobTask extends \Spatie\Async\Task
{
private $jobId;
public function __construct($jobId)
{
$this->jobId = $jobId;
}
public function configure()
{
}
public function run()
{
$app = new \Espo\Core\Application();
$app->runJob($this->jobId);
}
}

View File

@@ -97,7 +97,6 @@ class Crypt
public function generateKey()
{
return md5(uniqid());
return \Espo\Core\Utils\Util::generateKey();
}
}

View File

@@ -25,16 +25,19 @@
*
* 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\Utils\Database\DBAL\Driver\Mysqli;
class Driver extends \Doctrine\DBAL\Driver\Mysqli\Driver
class Driver extends \Doctrine\DBAL\Driver\Mysqli\Driver
{
public function getDatabasePlatform()
{
return new \Espo\Core\Utils\Database\DBAL\Platforms\MySqlPlatform();
}
}
public function getSchemaManager(\Doctrine\DBAL\Connection $conn)
{
return new \Espo\Core\Utils\Database\DBAL\Schema\MySqlSchemaManager($conn);
}
}

View File

@@ -29,7 +29,7 @@
namespace Espo\Core\Utils\Database\DBAL\FieldTypes;
class JsonArrayType extends \Doctrine\DBAL\Types\JsonArrayType
class JsonArrayType extends \Doctrine\DBAL\Types\TextType
{
const JSON_ARRAY = 'jsonArray';

View File

@@ -29,7 +29,7 @@
namespace Espo\Core\Utils\Database\DBAL\FieldTypes;
class JsonObjectType extends \Doctrine\DBAL\Types\ObjectType
class JsonObjectType extends \Doctrine\DBAL\Types\TextType
{
const JSON_OBJECT = 'jsonObject';

View File

@@ -38,6 +38,10 @@ use Doctrine\DBAL\Schema\Column;
class MySqlPlatform extends \Doctrine\DBAL\Platforms\MySqlPlatform
{
/* Espo */
const LENGTH_LIMIT_LONGTEXT = 4294967295;
/* Espo: end */
public function getAlterTableSQL(TableDiff $diff)
{
$columnSql = array();
@@ -453,6 +457,29 @@ class MySqlPlatform extends \Doctrine\DBAL\Platforms\MySqlPlatform
return 'MEDIUMTEXT';
}
/* Espo: fix a problem of changing text field type */
public function getClobTypeLength($type)
{
switch ($type) {
case 'tinytext':
return static::LENGTH_LIMIT_TINYTEXT;
break;
case 'text':
return static::LENGTH_LIMIT_TEXT;
break;
case 'mediumtext':
return static::LENGTH_LIMIT_MEDIUMTEXT;
break;
case 'longtext':
return static::LENGTH_LIMIT_LONGTEXT;
break;
}
}
/* Espo: end */
public function getColumnDeclarationListSQL(array $fields)
{
$queryFields = array();

View File

@@ -28,12 +28,11 @@
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Schema;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Column;
class Comparator extends \Doctrine\DBAL\Schema\Comparator
{
public function diffColumn(Column $column1, Column $column2)
{
$changedProperties = array();
@@ -80,6 +79,15 @@ class Comparator extends \Doctrine\DBAL\Schema\Comparator
}
}
if ($column1->getType() instanceof \Doctrine\DBAL\Types\TextType) {
$length1 = $column1->getLength() ?: 16777215/* mediumtext length*/;
$length2 = $column2->getLength() ?: 16777215;
if ($length2 > $length1) {
$changedProperties[] = 'length';
}
}
if ($column1->getType() instanceof \Doctrine\DBAL\Types\DecimalType) {
if (($column1->getPrecision()?:10) != ($column2->getPrecision()?:10)) {
$changedProperties[] = 'precision';
@@ -113,7 +121,19 @@ class Comparator extends \Doctrine\DBAL\Schema\Comparator
$changedProperties = array_merge($changedProperties, $diffKeys);
/** Espo: do not change a field length while changing other parameters */
if (!empty($changedProperties) && !in_array('length', $changedProperties) && $column1->getType() instanceof \Doctrine\DBAL\Types\StringType) {
$length1 = $column1->getLength() ?: 255;
$length2 = $column2->getLength() ?: 255;
if ($length1 > $length2) {
$changedProperties[] = 'length';
$column2->setLength($length1);
}
}
/** Espo: end */
return $changedProperties;
}
}
}

View File

@@ -34,6 +34,10 @@ use Doctrine\DBAL\Event\SchemaIndexDefinitionEventArgs;
class MySqlSchemaManager extends \Doctrine\DBAL\Schema\MySqlSchemaManager
{
/* Espo: default value for MariaDB 10.2.7+ */
protected $mariaDb1027;
/* Espo: end */
public function createSchema()
{
$sequences = array();
@@ -141,4 +145,140 @@ class MySqlSchemaManager extends \Doctrine\DBAL\Schema\MySqlSchemaManager
return $indexes;
}
}
protected function _getPortableTableColumnDefinition($tableColumn)
{
$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
$dbType = strtolower($tableColumn['type']);
$dbType = strtok($dbType, '(), ');
if (isset($tableColumn['length'])) {
$length = $tableColumn['length'];
} else {
$length = strtok('(), ');
}
$fixed = null;
if ( ! isset($tableColumn['name'])) {
$tableColumn['name'] = '';
}
$scale = null;
$precision = null;
$type = $this->_platform->getDoctrineTypeMapping($dbType);
// In cases where not connected to a database DESCRIBE $table does not return 'Comment'
if (isset($tableColumn['comment'])) {
$type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
$tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
}
switch ($dbType) {
case 'char':
$fixed = true;
break;
case 'float':
case 'double':
case 'real':
case 'numeric':
case 'decimal':
if(preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['type'], $match)) {
$precision = $match[1];
$scale = $match[2];
$length = null;
}
break;
case 'tinyint':
case 'smallint':
case 'mediumint':
case 'int':
case 'integer':
case 'bigint':
case 'tinyblob':
case 'mediumblob':
case 'longblob':
case 'blob':
case 'year':
$length = null;
break;
/* Espo: fix a problem of changing text field type */
case 'tinytext':
case 'text':
case 'mediumtext':
case 'longtext':
$length = $this->_platform->getClobTypeLength($dbType);
break;
/* Espo: end */
}
$length = ((int) $length == 0) ? null : (int) $length;
/* Espo: default value for MariaDB 10.2.7+ */
$columnDefault = isset($tableColumn['default']) ? $tableColumn['default'] : null;
if ($this->isMariaDb1027()) {
$columnDefault = $this->getMariaDb1027ColumnDefault($this->_platform, $columnDefault);
}
/* Espo: end */
$options = array(
'length' => $length,
'unsigned' => (bool) (strpos($tableColumn['type'], 'unsigned') !== false),
'fixed' => (bool) $fixed,
'default' => /* Espo: default value for MariaDB 10.2.7+ */ $columnDefault /* Espo: end */,
'notnull' => (bool) ($tableColumn['null'] != 'YES'),
'scale' => null,
'precision' => null,
'autoincrement' => (bool) (strpos($tableColumn['extra'], 'auto_increment') !== false),
'comment' => (isset($tableColumn['comment'])) ? $tableColumn['comment'] : null
);
if ($scale !== null && $precision !== null) {
$options['scale'] = $scale;
$options['precision'] = $precision;
}
return new Column($tableColumn['field'], \Doctrine\DBAL\Types\Type::getType($type), $options);
}
/* Espo: default value for MariaDB 10.2.7+ */
protected function isMariaDb1027()
{
if (!isset($this->mariaDb1027)) {
$version = $this->_conn->fetchColumn("select version()");
$this->mariaDb1027 = false;
if (preg_match('/mariadb/i', $version) && version_compare($version, '10.2.7') >= 0) {
$this->mariaDb1027 = true;
}
}
return $this->mariaDb1027;
}
private function getMariaDb1027ColumnDefault($platform, $columnDefault)
{
if ($columnDefault === 'NULL' || $columnDefault === null) {
return null;
}
if ($columnDefault[0] === "'") {
return stripslashes(
str_replace("''", "'",
preg_replace('/^\'(.*)\'$/', '$1', $columnDefault)
)
);
}
switch ($columnDefault) {
case 'current_timestamp()':
return $platform->getCurrentTimestampSQL();
case 'curdate()':
return $platform->getCurrentDateSQL();
case 'curtime()':
return $platform->getCurrentTimeSQL();
}
return $columnDefault;
}
/* Espo: end */
}

View File

@@ -56,26 +56,79 @@ class Helper
public function getDbalConnection()
{
if (!isset($this->connection)) {
if (!$this->getConfig()) {
return null;
}
$connectionParams = $this->getConfig()->get('database');
if (empty($connectionParams['dbname']) || empty($connectionParams['user'])) {
return null;
}
$connectionParams['driverClass'] = $this->drivers[ $connectionParams['driver'] ];
unset($connectionParams['driver']);
$dbalConfig = new \Doctrine\DBAL\Configuration();
$this->connection = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $dbalConfig);
$this->connection = $this->createDbalConnection();
}
return $this->connection;
}
public function setDbalConnection($dbalConnection)
{
$this->connection = $dbalConnection;
}
public function createDbalConnection(array $params = null)
{
if (!isset($params)) {
$config = $this->getConfig();
if ($config) {
$params = $config->get('database');
}
}
$params['driver'] = isset($params['driver']) ? $params['driver'] : 'pdo_mysql';
if (empty($params['dbname']) || empty($params['user'])) {
return null;
}
$params['driverClass'] = $this->drivers[ $params['driver'] ];
unset($params['driver']);
$dbalConfig = new \Doctrine\DBAL\Configuration();
return \Doctrine\DBAL\DriverManager::getConnection($params, $dbalConfig);
}
/**
* Create PDO connection
* @param array $params
* @return \Pdo| \PDOException
*/
public function createPdoConnection(array $params = null)
{
if (!isset($params)) {
$params = $this->getConfig()->get('database');
}
$platform = !empty($params['platform']) ? strtolower($params['platform']) : 'mysql';
$port = empty($params['port']) ? '' : ';port=' . $params['port'];
$dbname = empty($params['dbname']) ? '' : ';dbname=' . $params['dbname'];
$options = array();
if (isset($params['sslCA'])) {
$options[PDO::MYSQL_ATTR_SSL_CA] = $params['sslCA'];
}
if (isset($params['sslCert'])) {
$options[PDO::MYSQL_ATTR_SSL_CERT] = $params['sslCert'];
}
if (isset($params['sslKey'])) {
$options[PDO::MYSQL_ATTR_SSL_KEY] = $params['sslKey'];
}
if (isset($params['sslCAPath'])) {
$options[PDO::MYSQL_ATTR_SSL_CAPATH] = $params['sslCAPath'];
}
if (isset($params['sslCipher'])) {
$options[PDO::MYSQL_ATTR_SSL_CIPHER] = $params['sslCipher'];
}
$dsn = $platform . ':host='.$params['host'].$port.$dbname;
$dbh = new \PDO($dsn, $params['user'], $params['password'], $options);
return $dbh;
}
/**
* Get maximum index length. If $tableName is empty get a value for all database tables
*
@@ -85,20 +138,20 @@ class Helper
*/
public function getMaxIndexLength($tableName = null, $default = 1000)
{
$mysqlEngine = $this->getMysqlEngine($tableName);
if (!$mysqlEngine) {
$tableEngine = $this->getTableEngine($tableName);
if (!$tableEngine) {
return $default;
}
switch ($mysqlEngine) {
switch ($tableEngine) {
case 'InnoDB':
$mysqlVersion = $this->getMysqlVersion();
$version = $this->getDatabaseVersion();
if (version_compare($mysqlVersion, '10.0.0') >= 0) {
if (version_compare($version, '10.0.0') >= 0) {
return 767; //InnoDB, MariaDB
}
if (version_compare($mysqlVersion, '5.7.0') >= 0) {
if (version_compare($version, '5.7.0') >= 0) {
return 3072; //InnoDB, MySQL 5.7+
}
@@ -114,7 +167,26 @@ class Helper
return $this->getMaxIndexLength($tableName, $default);
}
protected function getMysqlVersion()
/**
* Get database type (MySQL, MariaDB)
* @return string
*/
public function getDatabaseType($default = 'MySQL')
{
$connection = $this->getDbalConnection();
if (!$connection) {
return $default;
}
$version = $this->getDatabaseVersion();
if (preg_match('/mariadb/i', $version)) {
return 'MariaDB';
}
return $default;
}
protected function getDatabaseVersion()
{
$connection = $this->getDbalConnection();
if (!$connection) {
@@ -131,7 +203,7 @@ class Helper
*
* @return string
*/
protected function getMysqlEngine($tableName = null, $default = null)
protected function getTableEngine($tableName = null, $default = null)
{
$connection = $this->getDbalConnection();
if (!$connection) {
@@ -161,16 +233,16 @@ class Helper
*/
public function isSupportsFulltext($tableName = null, $default = false)
{
$mysqlEngine = $this->getMysqlEngine($tableName);
if (!$mysqlEngine) {
$tableEngine = $this->getTableEngine($tableName);
if (!$tableEngine) {
return $default;
}
switch ($mysqlEngine) {
switch ($tableEngine) {
case 'InnoDB':
$mysqlVersion = $this->getMysqlVersion();
$version = $this->getDatabaseVersion();
if (version_compare($mysqlVersion, '5.6.4') >= 0) {
if (version_compare($version, '5.6.4') >= 0) {
return true; //InnoDB, MySQL 5.6.4+
}
@@ -185,4 +257,24 @@ class Helper
{
return $this->isSupportsFulltext($tableName, $default);
}
}
public function getPdoDatabaseParam($name, \PDO $pdoConnection)
{
if (!method_exists($pdoConnection, 'prepare')) {
return null;
}
$sth = $pdoConnection->prepare("SHOW VARIABLES LIKE '" . $name . "'");
$sth->execute();
$res = $sth->fetch(\PDO::FETCH_NUM);
$version = empty($res[1]) ? null : $res[1];
return $version;
}
public function getPdoDatabaseVersion(\PDO $pdoConnection)
{
return $this->getPdoDatabaseParam('version', $pdoConnection);
}
}

View File

@@ -213,16 +213,16 @@ class Converter
$collectionDefs = $entityMetadata['collection'];
$ormMetadata[$entityName]['collection'] = array();
if (array_key_exists('sortByByColumn', $collectionDefs)) {
$ormMetadata[$entityName]['collection']['orderBy'] = $collectionDefs['sortByByColumn'];
} else if (array_key_exists('sortBy', $collectionDefs)) {
if (array_key_exists($collectionDefs['sortBy'], $ormMetadata[$entityName]['fields'])) {
$ormMetadata[$entityName]['collection']['orderBy'] = $collectionDefs['sortBy'];
if (array_key_exists('orderByColumn', $collectionDefs)) {
$ormMetadata[$entityName]['collection']['orderBy'] = $collectionDefs['orderByColumn'];
} else if (array_key_exists('orderBy', $collectionDefs)) {
if (array_key_exists($collectionDefs['orderBy'], $ormMetadata[$entityName]['fields'])) {
$ormMetadata[$entityName]['collection']['orderBy'] = $collectionDefs['orderBy'];
}
}
$ormMetadata[$entityName]['collection']['order'] = 'ASC';
if (array_key_exists('asc', $collectionDefs)) {
$ormMetadata[$entityName]['collection']['order'] = $collectionDefs['asc'] ? 'ASC' : 'DESC';
if (array_key_exists('order', $collectionDefs)) {
$ormMetadata[$entityName]['collection']['order'] = strtoupper($collectionDefs['order']);
}
}
@@ -262,6 +262,18 @@ class Converter
case 'bool':
$fieldParams['default'] = isset($fieldParams['default']) ? (bool) $fieldParams['default'] : $this->defaultValue['bool'];
break;
//todo: remove the types from ORM Metadata. Types are deprecated.
case 'email':
case 'phone':
break;
default:
$constName = strtoupper(Util::toUnderScore($fieldParams['type']));
if (!defined('\\Espo\\ORM\\Entity::' . $constName)) {
$fieldParams['type'] = $this->defaultFieldType;
}
break;
}
}
}
@@ -414,6 +426,10 @@ class Converter
$fieldParams = Util::merge($fieldParams, $fieldTypeMetadata['fieldDefs']);
}
if ($fieldParams['type'] == 'base' && isset($fieldParams['dbType'])) {
$fieldParams['notStorable'] = false;
}
/** check if need to skipOrmDefs this field in ORM metadata */
if (!empty($fieldTypeMetadata['skipOrmDefs']) || !empty($fieldParams['skipOrmDefs'])) {
return false;

View File

@@ -38,6 +38,7 @@ class Email extends Base
'fields' => array(
$fieldName => array(
'select' => 'emailAddresses.name',
'fieldType' => 'email',
'where' =>
array (
'LIKE' => \Espo\Core\Utils\Util::toUnderScore($entityName) . ".id IN (

View File

@@ -71,6 +71,7 @@ class LinkMultiple extends Base
$data[$entityName]['fields'][$fieldName . 'Columns'] = [
'type' => 'jsonObject',
'notStorable' => true,
'columns' => $columns
];
}

View File

@@ -35,20 +35,19 @@ class PersonName extends Base
{
protected function load($fieldName, $entityName)
{
$subList = array('first' . ucfirst($fieldName), ' ', 'last' . ucfirst($fieldName));
$subList = ['first' . ucfirst($fieldName), ' ', 'last' . ucfirst($fieldName)];
$tableName = Util::toUnderScore($entityName);
$orderByField = 'first' . ucfirst($fieldName); // TODO available in settings
$orderBy1Field = 'first' . ucfirst($fieldName);
$orderBy2Field = 'last' . ucfirst($fieldName);
$fullList = [];
$fieldList = [];
$like = [];
$equal = [];
$fullList = array();
$fullListReverse = array();
$fieldList = array();
$like = array();
$equal = array();
foreach($subList as $subFieldName) {
foreach ($subList as $subFieldName) {
$fieldNameTrimmed = trim($subFieldName);
if (!empty($fieldNameTrimmed)) {
$columnName = $tableName . '.' . Util::toUnderScore($fieldNameTrimmed);
@@ -63,21 +62,21 @@ class PersonName extends Base
$fullListReverse = array_reverse($fullList);
return array(
$entityName => array (
'fields' => array(
$fieldName => array(
return [
$entityName => [
'fields' => [
$fieldName => [
'type' => 'varchar',
'select' => $this->getSelect($fullList),
'where' => array(
'where' => [
'LIKE' => "(".implode(" OR ", $like)." OR CONCAT(".implode(", ", $fullList).") LIKE {value} OR CONCAT(".implode(", ", $fullListReverse).") LIKE {value})",
'=' => "(".implode(" OR ", $equal)." OR CONCAT(".implode(", ", $fullList).") = {value} OR CONCAT(".implode(", ", $fullListReverse).") = {value})",
),
'orderBy' => ''. $tableName . '.' . Util::toUnderScore($orderByField) . ' {direction}'
),
),
),
);
],
'orderBy' => "{$tableName}." . Util::toUnderScore($orderBy1Field) ." {direction}, {$tableName}." . Util::toUnderScore($orderBy2Field)
]
]
]
];
}
protected function getSelect(array $fullList)

View File

@@ -38,6 +38,7 @@ class Phone extends Base
'fields' => array(
$fieldName => array(
'select' => 'phoneNumbers.name',
'fieldType' => 'phone',
'where' =>
array (
'LIKE' => \Espo\Core\Utils\Util::toUnderScore($entityName) . ".id IN (

View File

@@ -306,7 +306,7 @@ class Converter
$table = $this->getSchema()->createTable($tableName);
$table->addColumn('id', 'int', $this->getDbFieldParams(array(
'type' => 'int',
'type' => 'id',
'len' => $this->defaultLength['int'],
'autoincrement' => true,
)));
@@ -405,9 +405,10 @@ class Converter
break;
}
if (isset($fieldParams['autoincrement']) && $fieldParams['autoincrement']) {
if ($fieldParams['type'] != 'id' && isset($fieldParams['autoincrement']) && $fieldParams['autoincrement']) {
$dbFieldParams['unique'] = true;
$dbFieldParams['notnull'] = true;
$dbFieldParams['unsigned'] = true;
}
if (isset($fieldParams['utf8mb3']) && $fieldParams['utf8mb3']) {

View File

@@ -25,29 +25,21 @@
*
* 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\Utils\Database\Schema\rebuildActions;
class AddSystemUser extends \Espo\Core\Utils\Database\Schema\BaseRebuildActions
{
public function afterRebuild()
{
$userId = $this->getConfig()->get('systemUser.id');
{
$userId = $this->getConfig()->get('systemUserAttributes.id');
$entity = $this->getEntityManager()->getEntity('User', $userId);
if (!isset($entity)) {
$systemUser = $this->getConfig()->get('systemUser');
if (!$entity) {
$systemUserAttributes = $this->getConfig()->get('systemUserAttributes');
$entity = $this->getEntityManager()->getEntity('User');
$entity->set($systemUser);
return $this->getEntityManager()->saveEntity($entity);
}
}
$entity->set($systemUserAttributes);
return $this->getEntityManager()->saveEntity($entity);
}
}
}

View File

@@ -36,7 +36,6 @@ return array(
'dbType' => 'int',
'len' => '11',
'autoincrement' => true,
'unique' => true,
),
'entityType' => array(
'type' => 'varchar',

View File

@@ -36,7 +36,6 @@ return array(
'dbType' => 'int',
'len' => '11',
'autoincrement' => true,
'unique' => true
),
'entityId' => array(
'type' => 'varchar',

View File

@@ -27,29 +27,28 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
return array(
return [
'unset' => array(
'unset' => [
'__APPEND__',
'Preferences'
),
'Preferences',
],
'unsetIgnore' => [
'__APPEND__',
['Preferences', 'fields', 'id'],
['Preferences', 'fields', 'data']
['Preferences', 'fields', 'data'],
],
'Preferences' => array(
'fields' => array(
'id' => array(
'Preferences' => [
'fields' => [
'id' => [
'dbType' => 'varchar',
'len' => 24,
'type' => 'id'
),
'data' => array(
'type' => 'text'
)
)
)
);
'type' => 'id',
],
'data' => [
'type' => 'text',
'len' => 16777216,
]
]
],
];

View File

@@ -25,7 +25,7 @@
*
* 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.
************************************************************************/
************************************************************************/
return array(
@@ -36,7 +36,6 @@ return array(
'dbType' => 'int',
'len' => '11',
'autoincrement' => true,
'unique' => true,
),
'entityId' => array(
'type' => 'varchar',

View File

@@ -223,4 +223,28 @@ class DateTime
return $dateTime->format($phpFormat);
}
public static function isAfterThreshold($value, $period)
{
if (is_string($value)) {
try {
$dt = new \DateTime($value);
} catch (\Exception $e) {
return;
}
} else if ($value instanceof \DateTime) {
$dt = clone $value;
} else {
return;
}
$dt->modify($period);
$dtNow = new \DateTime();
if ($dtNow->format('U') > $dt->format('U')) {
return true;
}
return false;
}
}

View File

@@ -36,7 +36,7 @@ class EmailFilterManager
{
private $entityManager;
private $data = array();
private $data = [];
protected $filtersMatcher = null;
@@ -61,10 +61,10 @@ class EmailFilterManager
public function getMatchingFilter(Email $email, $userId)
{
if (!array_key_exists($userId, $this->data)) {
$emailFilterList = $this->getEntityManager()->getRepository('EmailFilter')->where(array(
$emailFilterList = $this->getEntityManager()->getRepository('EmailFilter')->where([
'parentId' => $userId,
'parentType' => 'User'
))->order('LIST:action:Skip;Move to Folder')->find();
])->order('LIST:action:Skip,Move to Folder')->find();
$this->data[$userId] = $emailFilterList;
}
foreach ($this->data[$userId] as $emailFilter) {
@@ -74,5 +74,3 @@ class EmailFilterManager
}
}
}

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