Compare commits

...

286 Commits
5.3.3 ... 5.4.5

Author SHA1 Message Date
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
yuri
e0705293e1 Merge branch 'hotfix/5.4.4' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.4.4 2018-10-12 15:40:23 +03:00
Taras Machyshyn
29f3f7c330 Bug fixes for utf8mb4 2018-10-12 15:39:09 +03:00
yuri
0e0e81f5ae Merge branch 'hotfix/5.4.4' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.4.4 2018-10-10 17:03:27 +03:00
Taras Machyshyn
bf503db267 LDAP: hide new options 2018-10-09 12:38:09 +03:00
Taras Machyshyn
028ad25fc6 LDAP: bug fixes for portal users 2018-10-09 12:09:00 +03:00
yuri
950d98528a fic ics summary 2018-10-01 13:45:28 +03:00
yuri
8b2d62d27a css navbar fix 2018-10-01 11:13:25 +03:00
yuri
15d5985ec4 Merge branch 'hotfix/5.4.4' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.4.4 2018-09-28 17:20:36 +03:00
Taras Machyshyn
bd94543b71 PHP path 2018-09-28 17:19:58 +03:00
yuri
c54bc30bda validation popover error fix 2018-09-28 12:41:31 +03:00
yuri
f2138a4e23 field view fix 2018-09-28 12:06:46 +03:00
yuri
0e3b3c4193 email address lookup fix 2018-09-26 11:21:39 +03:00
yuri
58c767c0a6 Merge branch 'hotfix/5.4.4' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.4.4 2018-09-25 12:55:29 +03:00
yuri
19b4e91e25 lt_LT lang 2018-09-25 10:56:11 +03:00
yuri
67635f0b20 Merge branch 'hotfix/5.4.4' of github.com:espocrm/espocrm into hotfix/5.4.4 2018-09-25 10:44:17 +03:00
yuri
5d90b4f070 multi-enum w/o option list 2018-09-25 10:43:47 +03:00
yuri
c212eb1f20 array search by not defined options 2018-09-25 10:39:57 +03:00
Taras Machyshyn
5ae0b5a3f2 Case insensitive for LDAP 2018-09-24 16:01:12 +03:00
Xiaolu Hong
06d907d633 update zh_CN (#1054) 2018-09-24 14:26:56 +03:00
Xiaolu Hong
737c5ff2ba update zh_CN (#1052) 2018-09-24 11:03:49 +03:00
yuri
ba19934c06 selectize filter height 2018-09-21 17:01:41 +03:00
yuri
8885bb87be account role 100 2018-09-21 13:29:17 +03:00
yuri
690c873291 account role length 2018-09-21 13:22:11 +03:00
Taras Machyshyn
158cdd27bc LDAP Auth for portal users 2018-09-21 12:35:33 +03:00
yuri
3181e54aa6 lead capture send Access-Control-Allow-Origin on posting 2018-09-19 14:52:56 +03:00
yuri
8028a153af fix max portal user count 2018-09-19 14:27:25 +03:00
yuri
0a03750d26 Merge branch 'hotfix/5.4.4' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.4.4 2018-09-19 14:23:31 +03:00
yuri
e9b1a91180 massUpdate is portal user disabled 2018-09-19 14:23:25 +03:00
Taras Machyshyn
8677dd48ad Bug fixes for portal user limit 2018-09-19 14:18:52 +03:00
yuri
c3d309d07e lead capture config leadCaptureAllowOrigin 2018-09-19 13:32:27 +03:00
yuri
f3d2f0b453 cleanup deleted fix 2018-09-18 16:39:04 +03:00
yuri
fe638db414 orm mid key name fix 2018-09-18 16:37:16 +03:00
yuri
e51be3fd85 lead capture options 2018-09-18 12:49:47 +03:00
yuri
4fd345cb88 Merge branch 'hotfix/5.4.4' of github.com:espocrm/espocrm into hotfix/5.4.4 2018-09-17 12:29:23 +03:00
yuri
41e2de18aa email template disable quick create 2018-09-17 12:29:13 +03:00
yuri
df567a0603 email template placeholder list translation 2018-09-17 12:26:53 +03:00
Yuri Kuznetsov
419fb93a91 Update README.md 2018-09-14 16:48:06 +03:00
yuri
c7473be8c7 readme update 2018-09-14 16:01:21 +03:00
yuri
311a01ef41 lead capture php 5.6 compatibility 2018-09-14 14:02:16 +03:00
yuri
1a163907a3 version 2018-09-14 13:43:17 +03:00
yuri
2acdc00547 fix admin panel iframe height 2018-09-14 13:37:13 +03:00
yuri
b622d8865c fix merge 2018-09-14 13:00:03 +03:00
yuri
786fcfb580 fix email assignment permissin 2018-09-13 16:32:34 +03:00
yuri
eec8512f96 fix import 2018-09-13 14:09:19 +03:00
yuri
be62caef2b fix wysiwyg xss 2018-09-13 14:03:01 +03:00
yuri
eabd52b186 fix xss in global search 2018-09-13 13:44:25 +03:00
yuri
101087680b version 2018-09-12 15:33:04 +03:00
yuri
372f1770e0 kanban move over 2018-09-12 13:07:56 +03:00
yuri
a44dcdc9ff disable kanban dragging on touch devices 2018-09-11 15:57:04 +03:00
yuri
d44b172602 tooltip 2018-09-11 15:06:42 +03:00
yuri
241d7610ba email listen to isRepied 2018-09-11 12:16:42 +03:00
yuri
6929b2d717 mass update skipStreamNotesAcl 2018-09-10 15:24:42 +03:00
yuri
e743f61913 version 2018-09-10 12:17:15 +03:00
yuri
7f4e5dd01a note acl populate script 2018-09-10 12:15:42 +03:00
yuri
8a1b58cf7c fix kanban css 2018-09-10 11:22:22 +03:00
yuri
2a20766912 fix UniquId 2018-09-10 10:29:34 +03:00
yuri
b49679eb90 fix notification id 2018-09-10 10:27:09 +03:00
yuri
b2ea073c16 fix notifications 2018-09-10 10:24:09 +03:00
yuri
54085a59c3 fix meeting acl 2018-09-07 14:38:02 +03:00
yuri
858658768d version 2018-09-07 14:21:18 +03:00
yuri
86421010b9 fix meeting/call select manager 2018-09-07 14:20:01 +03:00
yuri
8675db36f3 stream sql optimization 2018-09-07 11:02:49 +03:00
yuri
f36a98a824 lead capture payload see more disabled 2018-09-06 15:21:45 +03:00
yuri
3c71ff05f3 css fix 2018-09-05 12:53:51 +03:00
yuri
6039939308 css fix 2018-09-05 12:19:06 +03:00
yuri
1ddea9938b fix stream list 2018-09-04 16:56:09 +03:00
yuri
5d3b515ef1 cleanup 2018-09-04 12:08:56 +03:00
yuri
31b7db4033 fix sales pipeline 2018-09-04 12:06:51 +03:00
yuri
6a7d59ba20 sales pipilent labels 2018-09-04 11:55:03 +03:00
yuri
0ab0e46d67 css cleanup 2018-09-03 15:55:21 +03:00
yuri
2777fe49d9 strem note acl fix 2018-09-03 15:17:29 +03:00
yuri
1e246e8197 fix stream acl 2018-09-03 14:41:12 +03:00
yuri
d8eec1068d fix tpl 2018-09-03 14:07:08 +03:00
yuri
0021e16962 stream acl 2018-09-03 13:39:32 +03:00
yuri
b046da1d79 fix css 2018-08-31 16:29:00 +03:00
yuri
2ca8f3af6f fix icon 2018-08-31 15:30:47 +03:00
yuri
b180ac2314 lang fix 2018-08-31 15:14:16 +03:00
yuri
9cc646e3b8 ua lang 2018-08-31 15:07:06 +03:00
yuri
a2fee68732 icon change 2018-08-31 14:24:17 +03:00
yuri
dc0d150000 de lang 2018-08-31 12:58:13 +03:00
yuri
76cc4a8d2c icons change 2018-08-31 12:55:07 +03:00
yuri
6e4c097dee fix image preview 2018-08-30 16:55:26 +03:00
yuri
af398cb5d4 css fix 2018-08-30 15:52:34 +03:00
yuri
3865bba926 css fix 2018-08-30 15:49:40 +03:00
yuri
3ad39d3337 icon change 2018-08-30 15:30:16 +03:00
yuri
1dfe1eb9bf image preview do not resize 2018-08-30 15:15:33 +03:00
yuri
7885a9cac3 fix reminder after save 2018-08-30 14:53:28 +03:00
yuri
bb63751332 entity set as being saved 2018-08-30 14:30:17 +03:00
yuri
083770f705 icon fixes 2018-08-30 12:55:16 +03:00
yuri
a65862325f follow icon change 2018-08-30 12:13:42 +03:00
yuri
9465ca46b2 css fix 2018-08-30 11:50:15 +03:00
yuri
62f05f370f navbar item titles 2018-08-30 11:44:50 +03:00
yuri
8e408d93cb css fix 2018-08-30 11:39:40 +03:00
yuri
7c1d587ae0 search by opted out and bool where redefinition in ORM 2018-08-30 11:30:33 +03:00
yuri
cb42bfe8e7 fix typo 2018-08-30 11:10:30 +03:00
yuri
32a340afe0 icons 2018-08-29 16:39:04 +03:00
yuri
0ff8246a00 font awesome 2018-08-29 16:35:50 +03:00
yuri
c9c58d373a update bootstrap to 3.3.7 2018-08-28 16:49:02 +03:00
yuri
af7aea720b css fixes 2018-08-28 16:15:07 +03:00
yuri
dee488a9de fix lang 2018-08-28 15:06:04 +03:00
yuri
88ec71400a config params 2018-08-28 13:27:30 +03:00
yuri
10ad5a804a note edit delete period 2018-08-28 13:24:26 +03:00
yuri
45f948d96c Merge branch 'hotfix/5.3.7' of ssh://172.20.0.1/var/git/espo/backend 2018-08-27 14:39:54 +03:00
yuri
e5e6a32ce5 email: use mailto if no access to create email 2018-08-27 10:41:09 +03:00
yuri
cf5202a065 calendar: apply acl 2018-08-27 10:32:40 +03:00
yuri
f37a49ddaf fix typo 2018-08-27 10:23:15 +03:00
yuri
4858f964c8 case and target list dynamic logic 2018-08-23 15:27:02 +03:00
yuri
5b50215242 fix json format 2018-08-23 15:25:46 +03:00
yuri
461e356419 lead capture test 2018-08-23 15:08:31 +03:00
yuri
7146f3d3aa fix lead capture 2018-08-23 15:08:24 +03:00
yuri
43d289bc56 es_MX lang 2018-08-23 14:22:48 +03:00
yuri
65c843b581 fix lang 2018-08-23 14:06:23 +03:00
Taras Machyshyn
d08704c95c Warning fixes 2018-08-23 13:00:11 +03:00
yuri
fb9100efdc maintenanceMode param 2018-08-23 12:41:03 +03:00
yuri
ee2182638a service unavalable exception 2018-08-23 12:32:30 +03:00
yuri
8ba9fb4693 cronDisabled param 2018-08-23 12:32:16 +03:00
yuri
b427d8cade fix date time optional fetch 2018-08-23 12:07:21 +03:00
yuri
89de19fd2e css 2018-08-22 12:23:35 +03:00
yuri
2f4df63281 foreign bool 2018-08-21 14:52:37 +03:00
yuri
e12eade143 fix template form 2018-08-21 12:05:59 +03:00
yuri
15b0d147d9 fix attachment 2018-08-21 11:48:41 +03:00
yuri
4901b8c5a0 template fonts labels 2018-08-20 16:30:21 +03:00
yuri
8c4f3b1103 template: ability to pick font 2018-08-20 16:16:10 +03:00
yuri
2225906eac fix kanban 2018-08-20 15:40:17 +03:00
yuri
895b3aa7c9 lang 2018-08-20 12:34:03 +03:00
yuri
67b359ff18 kanban css fix 2018-08-20 12:27:34 +03:00
yuri
62200d393e fix css formatting 2018-08-20 10:50:51 +03:00
yuri
166522e4e6 layout fixes 2018-08-20 10:38:42 +03:00
yuri
cc3d7a9f20 css grid-auto-fill 2018-08-17 16:19:56 +03:00
yuri
81ce479293 Merge branch 'hotfix/5.3.7' 2018-08-17 14:57:16 +03:00
yuri
afa6591903 dashboard fix 2018-08-17 14:48:48 +03:00
yuri
e89cb2547e preferences: ability to reset dashboard to default 2018-08-17 14:20:24 +03:00
yuri
7b982acc3e fix button helper 2018-08-17 12:42:35 +03:00
yuri
e24fc19314 preferences layout change 2018-08-17 12:11:57 +03:00
yuri
0d9473b1e9 version 2018-08-17 12:00:28 +03:00
yuri
db4ccbe590 clean up deleted records 2018-08-17 11:39:58 +03:00
yuri
f085d2140c fix strip url 2018-08-16 16:47:16 +03:00
yuri
4f2117453d lang 2018-08-16 15:49:21 +03:00
yuri
919fff6cd5 activities compose email change 2018-08-16 12:59:42 +03:00
yuri
b77f0a705e css fix 2018-08-16 12:04:22 +03:00
yuri
73699894be update window title after save 2018-08-16 11:50:39 +03:00
yuri
4282d1766b fix flotr2 with touch enabled 2018-08-16 11:43:05 +03:00
yuri
3ebb9ea8fc import acl check edit 2018-08-15 16:18:55 +03:00
yuri
5635fddf3d import for regular users 2018-08-15 15:59:46 +03:00
yuri
9501e116cb app error log 2018-08-15 15:45:19 +03:00
yuri
c0997f1d97 fix remove fail 2018-08-15 15:15:22 +03:00
yuri
106961ff03 import fix 2018-08-15 13:56:42 +03:00
yuri
fd2b9cc818 import acl 2018-08-15 13:52:35 +03:00
yuri
d90853ff73 fix lead capture 2018-08-15 13:51:52 +03:00
yuri
a8bcd1dc2f import naming fixes 2018-08-15 13:26:31 +03:00
yuri
1f4ae0f07c date list title 2018-08-15 12:54:30 +03:00
yuri
cf0a9fd808 attachments fixes 2018-08-15 12:47:41 +03:00
yuri
036e10cbf7 fix test 2018-08-15 12:21:48 +03:00
yuri
0a22510566 orm fixes 2018-08-15 12:02:20 +03:00
yuri
44f14c1d29 amount weighted filter 2018-08-15 11:21:10 +03:00
yuri
e6796390db fix login logo 2018-08-14 17:29:15 +03:00
yuri
c11d52cef6 css fix 2018-08-14 16:35:37 +03:00
yuri
33e8b8b0c1 fix email phone add button 2018-08-14 15:35:18 +03:00
yuri
04dbe9129a Merge branch 'hotfix/5.3.7' 2018-08-14 15:22:09 +03:00
yuri
8e1a4ba368 IRR currency 2018-08-14 15:19:41 +03:00
yuri
920b5244a8 attachment filter fix 2018-08-14 14:53:17 +03:00
yuri
cd57f6c600 fix color icon linebreak issue 2018-08-14 14:29:09 +03:00
yuri
7e6a32ab06 attachmets list view 2018-08-14 13:36:04 +03:00
yuri
ee0dc257b2 lang 2018-08-14 11:58:37 +03:00
yuri
a8e6421517 cleanup attachments 15 days 2018-08-14 11:17:17 +03:00
yuri
1c5d2bd756 cleanup attachment limit 5000 2018-08-14 11:14:48 +03:00
yuri
1fc59b28aa Merge branch 'hotfix/5.3.7' 2018-08-14 11:09:31 +03:00
yuri
ed603c5f98 email template skip acl 2018-08-14 10:40:13 +03:00
yuri
0b7743419e array value cleanup 2018-08-14 10:25:10 +03:00
yuri
81ff01e9be array value 2018-08-13 15:43:44 +03:00
yuri
4845622949 fix iframe scroll on mobile 2018-08-13 14:48:18 +03:00
yuri
122cd9c56d orm: join table, join conditions, where not value conditions 2018-08-13 12:46:34 +03:00
yuri
379562b0d7 code style fix 2018-08-10 16:45:54 +03:00
yuri
c72d6d928a Merge branch 'hotfix/5.3.7' 2018-08-10 16:08:12 +03:00
yuri
b09c1366e3 fix email folder select manager 2018-08-10 15:11:56 +03:00
yuri
00e0427fdd options max length 2018-08-10 14:52:05 +03:00
yuri
85f888d60b relatin name max length 2018-08-10 14:46:28 +03:00
yuri
f22856dd91 field max length 100 2018-08-10 14:43:31 +03:00
yuri
5673d253e4 entity manager name maxlength 2018-08-10 14:41:37 +03:00
yuri
27ce836d4a currency entity class 2018-08-10 14:33:51 +03:00
yuri
a490ded36a fix field view fetch 2018-08-10 14:33:10 +03:00
yuri
bd54195403 Merge branch 'hotfix/5.3.7' 2018-08-10 11:08:59 +03:00
yuri
21d8e291e4 array field search type 2018-08-09 16:25:58 +03:00
yuri
b4fa62b403 parentName in assignment notification 2018-08-09 13:11:58 +03:00
yuri
1f3d7063d2 fix lead capture 2018-08-09 11:58:27 +03:00
yuri
e5709571e4 Merge branch 'hotfix/5.3.7' 2018-08-09 11:50:11 +03:00
yuri
a80d847f44 meeting duration list view 2018-08-09 11:38:40 +03:00
yuri
9de7105cde date field list link 2018-08-09 11:33:25 +03:00
yuri
cd7cc188e1 valueIsSet fields 2018-08-09 11:15:02 +03:00
yuri
e68a5f1979 fix phone number ui 2018-08-09 11:01:27 +03:00
yuri
c3e16a896e css fix 2018-08-08 14:52:48 +03:00
yuri
eafd0c3840 hide followers if empty 2018-08-08 13:18:16 +03:00
yuri
d5b5bb31c7 css fix 2018-08-08 13:03:00 +03:00
yuri
eaab96732a version 2018-08-08 12:52:30 +03:00
yuri
0dfed7279e css fix 2018-08-08 12:52:08 +03:00
yuri
b0e973d8ba fix multi enum 2018-08-08 12:07:37 +03:00
yuri
1d809c10b0 css fix 2018-08-08 11:33:01 +03:00
yuri
e0279f403f update font awesome icons 2018-08-08 11:16:48 +03:00
yuri
fa75da658c css fix 2018-08-08 10:57:13 +03:00
yuri
d23689aab7 cleanup 2018-08-08 10:35:32 +03:00
yuri
d4cc305265 fix account tasks panel 2018-08-08 10:34:22 +03:00
yuri
695d4b0cda lead capture job rename 2018-08-07 15:57:08 +03:00
yuri
2107d93cd2 phone number numeric 2018-08-07 15:55:06 +03:00
yuri
fb256ca29f orm noSelect param 2018-08-07 15:17:14 +03:00
yuri
557bcb4ecd lead capture 2018-08-07 13:18:43 +03:00
yuri
50a272473b fix formula modal 2018-08-06 12:31:11 +03:00
yuri
0eb9544371 jquery ui sortable on touch screens 2018-08-06 10:51:05 +03:00
yuri
620fa607d4 css fix 2018-08-06 10:39:40 +03:00
yuri
304d216b0e move to folder max size 2018-08-06 10:30:04 +03:00
yuri
ce2fe9d50c Merge branch 'stable' 2018-08-03 14:15:01 +03:00
yuri
eadf7835db fix css 2018-08-03 13:16:22 +03:00
yuri
048f65c8f6 record view ref 2018-08-03 11:57:35 +03:00
yuri
c30c4163a6 increase container max size 2018-08-02 16:21:40 +03:00
yuri
8d6db72516 icons fix 2018-08-02 15:08:32 +03:00
yuri
71864056bc navbar panel height 2018-08-02 15:05:00 +03:00
yuri
e375105a5f Merge branch 'hotfix/5.3.6' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.3.6 2018-08-02 13:28:18 +03:00
Taras Machyshyn
e066f4c6b0 Merge branch 'hotfix/5.3.6' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.3.6 2018-08-02 13:27:08 +03:00
Taras Machyshyn
b032aa4d8d Upgrades bug fixes 2018-08-02 13:26:46 +03:00
yuri
342a18e0ac fix installer css 2018-08-02 12:59:12 +03:00
yuri
e27e9de701 attachment preview resize 2018-08-02 12:37:16 +03:00
yuri
b14b199d43 preview resize fix 2018-08-02 12:28:14 +03:00
yuri
f77a7c32d5 link multiple preview size param 2018-08-02 12:04:33 +03:00
yuri
9f84d3f233 notifications attachment preview small size 2018-08-02 11:56:00 +03:00
yuri
be82244c6b showing attachments in notifications 2018-08-02 11:30:28 +03:00
yuri
7fed72d391 fix image sized download extension 2018-08-02 10:55:39 +03:00
yuri
54d615b64d fix attachment preview in list 2018-08-02 10:46:16 +03:00
yuri
2dbbd1e23d Merge branch 'hotfix/5.3.6' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.3.6 2018-08-02 10:30:55 +03:00
Taras Machyshyn
cba8867389 Bug fixes for MySQL 8 2018-08-01 18:49:40 +03:00
yuri
32d47db15a emailFolderMaxCount 2018-08-01 11:16:50 +03:00
yuri
2c65c9ff9f modal select attributes 2018-08-01 11:13:00 +03:00
yuri
fa5d63253b naming fix 2018-08-01 10:46:48 +03:00
yuri
b7ae252d3d modal forceSelectAllAttributes 2018-08-01 10:24:41 +03:00
yuri
f07b43abda version 2018-07-30 16:05:13 +03:00
yuri
276db37baf required extensions change 2018-07-30 16:03:20 +03:00
yuri
6adad84a29 installer config formatting 2018-07-30 15:52:16 +03:00
yuri
f137298c49 add exif extension 2018-07-30 15:50:17 +03:00
yuri
f8839518d5 avatar check exif read data exists 2018-07-30 15:48:00 +03:00
yuri
1ae39d1a40 fix campaign revenue empty 2018-07-30 10:20:45 +03:00
yuri
94f920324e fix case compose email 2018-07-30 10:14:17 +03:00
yuri
dbc3f7c7d6 Merge branch 'hotfix/5.3.5' 2018-07-27 12:35:42 +03:00
yuri
94c00e2901 orm: revert forcing updating json object 2018-07-27 11:59:22 +03:00
yuri
7de988104b link multiple attachment multiple list view fixes 2018-07-27 11:55:19 +03:00
yuri
03bc90968c fix email sending 2 2018-07-27 11:32:29 +03:00
yuri
a5401a22b9 fix email sending 2018-07-27 11:13:49 +03:00
yuri
4c1375d8b2 user buttons links 2018-07-27 10:46:18 +03:00
yuri
bab4a3b0d6 fix full text 2018-07-27 10:40:48 +03:00
yuri
33e135f227 fix email fetching 2018-07-27 10:03:26 +03:00
yuri
56b9d8d5c1 fix campaign 2018-07-26 17:56:35 +03:00
yuri
38ec88e302 fix formatting 2018-07-26 17:02:31 +03:00
yuri
10be208d8d fix linkedWith filter 2018-07-26 12:26:25 +03:00
yuri
38d227a948 fix model sync 2018-07-26 10:55:55 +03:00
yuri
1a8e4435e2 use put istead of patch 2018-07-25 15:04:33 +03:00
yuri
12febbdff5 version 2018-07-25 12:44:45 +03:00
yuri
ebea168350 field ui fixes 2018-07-25 12:41:48 +03:00
yuri
704519f80b fix setFetched link multiple and link one 2018-07-25 11:38:48 +03:00
yuri
5f1000ddd3 fix audited 2018-07-25 11:04:17 +03:00
yuri
2ee7e6cf4a orm: isUnordered param 2018-07-25 10:57:53 +03:00
yuri
2989975829 audited fix 2018-07-25 10:41:39 +03:00
yuri
a4c068427d cleanup 2018-07-24 18:22:54 +03:00
yuri
e1a76b9924 orm: isAttributeChanged improvement 2018-07-24 18:22:04 +03:00
yuri
1f2efcd716 link multiple available in list fix 2018-07-23 12:59:04 +03:00
yuri
576dfe068f fix array field setOptionList 2018-07-20 15:51:21 +03:00
yuri
f76860cdfd Merge branch 'stable' 2018-07-19 17:01:24 +03:00
yuri
56f8e68599 fix wysiwyg width 2018-07-19 14:56:28 +03:00
yuri
e8d75808ef fix email scrolling 2018-07-19 14:43:35 +03:00
yuri
bc8bf89d6e css fixes 2018-07-19 12:14:06 +03:00
yuri
91385cc3f0 fix naming 2018-07-19 11:49:43 +03:00
yuri
ad21511c72 email list is replied fix 2018-07-19 11:44:40 +03:00
yuri
938a24f102 fix assignment email notificaiotion 2018-07-19 10:59:32 +03:00
yuri
43b041ed6c fix replied icon 2018-07-18 15:37:37 +03:00
yuri
5e3f048795 replied email icon 2018-07-18 14:57:12 +03:00
yuri
3aec3ef6d2 formula higher 2018-07-18 11:37:55 +03:00
yuri
712a450734 formula mb string functions 2018-07-18 11:35:31 +03:00
yuri
5675b6721b sort by list comma fix 2018-07-18 11:25:58 +03:00
yuri
a4e23b8adb formula parser fix 2018-07-18 10:29:06 +03:00
yuri
9d49f418c8 orm fix order list 2018-07-18 10:11:36 +03:00
yuri
ea9f70dffa global search ability to select text 2018-07-17 17:26:06 +03:00
yuri
b624accbe8 version 2018-07-17 17:07:50 +03:00
yuri
12f974635e global search fixes 2018-07-17 17:07:32 +03:00
yuri
cfce68eb7e default panel complex fields 2018-07-17 15:38:44 +03:00
yuri
db8fbd1ed3 hide default panel if empty 2018-07-17 14:39:54 +03:00
yuri
7c549556ac Merge branch 'hotfix/5.3.3' 2018-07-17 11:15:22 +03:00
ayman-alkom
a6b5f38aed fix typo (#968) 2018-07-13 11:49:44 +03:00
760 changed files with 14740 additions and 4801 deletions

View File

@@ -28,6 +28,7 @@ module.exports = function (grunt) {
'client/lib/handlebars.js',
'client/lib/base64.js',
'client/lib/jquery-ui.min.js',
'client/lib/jquery.ui.touch-punch.min.js',
'client/lib/moment.min.js',
'client/lib/moment-timezone-with-data.min.js',
'client/lib/jquery.timepicker.min.js',

View File

@@ -17,10 +17,14 @@ For more information about server configuration see [this article](https://www.e
Documentation for administrators, users and developers is available [here](https://www.espocrm.com/documentation/).
### How to report bug
### How to report a bug
Create an issue [here](https://github.com/espocrm/espocrm/issues) or post on our [forum](http://forum.espocrm.com/forum/bug-reports).
### How to install a stable version
[Download](https://www.espocrm.com/download/) the latest version. See the [instructions](https://www.espocrm.com/documentation/administration/installation/) about installation.
### How to get started (for developers)
1. Clone repository to your local computer.
@@ -34,7 +38,7 @@ Now you can build. Build will create compiled css files.
To compose a proper config.php and populate database you can run install by opening `http(s)://{YOUR_CRM_URL}/install` location in a browser. Then open `data/config.php` file and add `isDeveloperMode => true`.
### How to build
### How to build (for developers)
You need to have nodejs and Grunt CLI installed.

View File

@@ -34,6 +34,7 @@ use \Espo\ORM\Entity;
class Email extends \Espo\Core\Acl\Base
{
protected $ownerUserIdAttribute = 'usersIds';
public function checkEntityRead(EntityUser $user, Entity $entity, $data)
{
@@ -118,4 +119,3 @@ class Email extends \Espo\Core\Acl\Base
return false;
}
}

View File

@@ -0,0 +1,53 @@
<?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\Acl;
use \Espo\Entities\User as EntityUser;
use \Espo\ORM\Entity;
class Import extends \Espo\Core\Acl\Base
{
public function checkEntityRead(EntityUser $user, Entity $entity, $data)
{
if ($user->isAdmin()) return true;
if ($user->id === $entity->get('createdById')) return true;
return false;
}
public function checkEntityDelete(EntityUser $user, Entity $entity, $data)
{
if ($user->isAdmin()) return true;
if ($user->id === $entity->get('createdById')) return true;
return false;
}
}

View File

@@ -34,6 +34,10 @@ use \Espo\ORM\Entity;
class Note extends \Espo\Core\Acl\Base
{
protected $deleteThresholdPeriod = '1 month';
protected $editThresholdPeriod = '7 days';
public function checkIsOwner(EntityUser $user, Entity $entity)
{
if ($entity->get('type') === 'Post' && $user->id === $entity->get('createdById')) {
@@ -41,5 +45,60 @@ class Note extends \Espo\Core\Acl\Base
}
return false;
}
}
public function checkEntityEdit(EntityUser $user, Entity $entity, $data)
{
if ($user->isAdmin()) {
return true;
}
if ($this->checkEntity($user, $entity, $data, 'edit')) {
if ($this->checkIsOwner($user, $entity)) {
$createdAt = $entity->get('createdAt');
if ($createdAt) {
$noteEditThresholdPeriod = '-' . $this->getConfig()->get('noteEditThresholdPeriod', $this->editThresholdPeriod);
$dt = new \DateTime();
$dt->modify($noteEditThresholdPeriod);
try {
if ($dt->format('U') > (new \DateTime($createdAt))->format('U')) {
return false;
}
} catch (\Exception $e) {
return false;
}
}
}
return true;
}
return false;
}
public function checkEntityDelete(EntityUser $user, Entity $entity, $data)
{
if ($user->isAdmin()) {
return true;
}
if ($this->checkEntity($user, $entity, $data, 'delete')) {
if ($this->checkIsOwner($user, $entity)) {
$createdAt = $entity->get('createdAt');
if ($createdAt) {
$deleteThresholdPeriod = '-' . $this->getConfig()->get('noteDeleteThresholdPeriod', $this->deleteThresholdPeriod);
$dt = new \DateTime();
$dt->modify($deleteThresholdPeriod);
try {
if ($dt->format('U') > (new \DateTime($createdAt))->format('U')) {
return false;
}
} catch (\Exception $e) {
return false;
}
}
}
return true;
}
return false;
}
}

View File

@@ -34,6 +34,7 @@ use \Espo\ORM\Entity;
class Email extends \Espo\Core\AclPortal\Base
{
protected $ownerUserIdAttribute = 'usersIds';
public function checkEntityRead(EntityUser $user, Entity $entity, $data)
{

View File

@@ -34,5 +34,11 @@ use \Espo\Core\Exceptions\BadRequest;
class Attachment extends \Espo\Core\Controllers\Record
{
public function actionList($params, $data, $request)
{
if (!$this->getUser()->isAdmin()) {
throw new Forbidden();
}
return parent::actionList($params, $data, $request);
}
}

View File

@@ -38,7 +38,7 @@ class Import extends \Espo\Core\Controllers\Record
{
protected function checkControllerAccess()
{
if (!$this->getUser()->isAdmin()) {
if (!$this->getAcl()->check('Import')) {
throw new Forbidden();
}
}
@@ -90,10 +90,9 @@ class Import extends \Espo\Core\Controllers\Record
$attachment->set('type', 'text/csv');
$attachment->set('role', 'Import File');
$attachment->set('name', 'import-file.csv');
$attachment->set('contents', $contents);
$this->getEntityManager()->saveEntity($attachment);
$this->getFileStorageManager()->putContents($attachment, $contents);
return array(
'attachmentId' => $attachment->id
);
@@ -127,7 +126,7 @@ class Import extends \Espo\Core\Controllers\Record
throw new BadRequest();
}
if (!isset($data->fieldDelimiter)) {
if (!isset($data->delimiter)) {
throw new BadRequest();
}
@@ -167,7 +166,7 @@ class Import extends \Espo\Core\Controllers\Record
throw new BadRequest();
}
if (!isset($data->fields)) {
if (!isset($data->attributeList)) {
throw new BadRequest();
}
@@ -178,7 +177,7 @@ class Import extends \Espo\Core\Controllers\Record
$importParams = array(
'headerRow' => !empty($data->headerRow),
'fieldDelimiter' => $data->fieldDelimiter,
'delimiter' => $data->delimiter,
'textQualifier' => $data->textQualifier,
'dateFormat' => $data->dateFormat,
'timeFormat' => $data->timeFormat,
@@ -202,7 +201,7 @@ class Import extends \Espo\Core\Controllers\Record
throw new Forbidden();
}
return $this->getService('Import')->import($data->entityType, $data->fields, $attachmentId, $importParams);
return $this->getService('Import')->import($data->entityType, $data->attributeList, $attachmentId, $importParams);
}
public function postActionUnmarkAsDuplicate($params, $data)

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\Controllers;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\BadRequest;
use \Espo\Core\Exceptions\NotFound;
class LeadCapture extends \Espo\Core\Controllers\Record
{
public function postActionLeadCapture($params, $data, $request, $response)
{
if (empty($params['apiKey'])) throw new BadRequest('No API key provided.');
if (empty($data)) throw new BadRequest('No payload provided.');
$allowOrigin = $this->getConfig()->get('leadCaptureAllowOrigin', '*');
$response->headers->set('Access-Control-Allow-Origin', $allowOrigin);
return $this->getRecordService()->leadCapture($params['apiKey'], $data);
}
public function optionsActionLeadCapture($params, $data, $request, $response)
{
if (empty($params['apiKey'])) throw new BadRequest('No API key provided.');
if (!$this->getRecordService()->isApiKeyValid($params['apiKey'])) {
throw new NotFound();
}
$allowOrigin = $this->getConfig()->get('leadCaptureAllowOrigin', '*');
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Accept');
$response->headers->set('Access-Control-Allow-Origin', $allowOrigin);
$response->headers->set('Access-Control-Allow-Methods', 'POST');
return true;
}
public function postActionGenerateNewApiKey($params, $data, $request)
{
if (empty($data->id)) throw new BadRequest();
return $this->getRecordService()->generateNewApiKeyForEntity($data->id)->getValueMap();
}
}

View File

@@ -0,0 +1,38 @@
<?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\Exceptions\Forbidden;
use \Espo\Core\Exceptions\BadRequest;
class LeadCaptureLogRecord extends \Espo\Core\Controllers\Record
{
}

View File

@@ -88,7 +88,7 @@ class Preferences extends \Espo\Core\Controllers\Base
throw new BadRequest();
}
if ($this->getAcl()->getLevel('Preferences', 'read') === 'no') {
if ($this->getAcl()->getLevel('Preferences', 'edit') === 'no') {
throw new Forbidden();
}
@@ -142,5 +142,45 @@ class Preferences extends \Espo\Core\Controllers\Base
return $entity->getValueMap();
}
}
public function postActionResetDashboard($params, $data)
{
if (empty($data->id)) throw new BadRequest();
$userId = $data->id;
$this->handleUserAccess($userId);
$user = $this->getEntityManager()->getEntity('User', $userId);
$preferences = $this->getEntityManager()->getEntity('Preferences', $userId);
if (!$user) throw new NotFound();
if (!$preferences) throw new NotFound();
if ($user->isPortal()) throw new Forbidden();
if ($this->getAcl()->getLevel('Preferences', 'edit') === 'no') {
throw new Forbidden();
}
$forbiddenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList('Preferences', 'edit');
if (in_array('dashboardLayout', $forbiddenAttributeList)) {
throw new Forbidden();
}
$dashboardLayout = $this->getConfig()->get('dashboardLayout');
$dashletsOptions = $this->getConfig()->get('dashletsOptions');
$preferences->set([
'dashboardLayout' => $dashboardLayout,
'dashletsOptions' => $dashletsOptions
]);
$this->getEntityManager()->saveEntity($preferences);
return (object) [
'dashboardLayout' => $preferences->get('dashboardLayout'),
'dashletsOptions' => $preferences->get('dashletsOptions')
];
}
}

View File

@@ -105,5 +105,19 @@ 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

@@ -46,6 +46,8 @@ class Base implements Injectable
protected $injections = array();
protected $ownerUserIdAttribute = null;
public function inject($name, $object)
{
$this->injections[$name] = $object;
@@ -281,5 +283,23 @@ class Base implements Injectable
return false;
}
}
public function getOwnerUserIdAttribute(Entity $entity)
{
if ($this->ownerUserIdAttribute) {
return $this->ownerUserIdAttribute;
}
if ($entity->hasLinkMultipleField('assignedUsers')) {
return 'assignedUsersIds';
}
if ($entity->hasAttribute('assignedUserId')) {
return 'assignedUserId';
}
if ($entity->hasAttribute('createdById')) {
return 'createdById';
}
}
}

View File

@@ -81,6 +81,11 @@ class Application
return $this->container;
}
protected function getConfig()
{
return $this->getContainer()->get('config');
}
public function run($name = 'default')
{
$this->routeHooks();
@@ -134,6 +139,11 @@ class Application
public function runCron()
{
if ($this->getConfig()->get('cronDisabled')) {
$GLOBALS['log']->warning("Cron is not run because it's disabled with 'cronDisabled' param.");
return;
}
$auth = $this->createAuth();
$auth->useNoAuth();
@@ -155,7 +165,7 @@ class Application
public function isInstalled()
{
$config = $this->getContainer()->get('config');
$config = $this->getConfig();
if (file_exists($config->getConfigPath()) && $config->get('isInstalled')) {
return true;
@@ -224,7 +234,7 @@ class Application
try {
$controllerManager = $this->getContainer()->get('controllerManager');
$result = $controllerManager->process($controllerName, $actionName, $params, $data, $slim->request());
$result = $controllerManager->process($controllerName, $actionName, $params, $data, $slim->request(), $slim->response());
$container->get('output')->render($result);
} catch (\Exception $e) {
$container->get('output')->processError($e->getMessage(), $e->getCode(), false, $e);
@@ -244,7 +254,7 @@ class Application
protected function getRouteList()
{
$routes = new \Espo\Core\Utils\Route($this->getContainer()->get('config'), $this->getMetadata(), $this->getContainer()->get('fileManager'));
$routes = new \Espo\Core\Utils\Route($this->getConfig(), $this->getMetadata(), $this->getContainer()->get('fileManager'));
return $routes->getAll();
@@ -252,11 +262,11 @@ class Application
protected function initRoutes()
{
$crudList = array_keys($this->getContainer()->get('config')->get('crud'));
$crudList = array_keys($this->getConfig()->get('crud'));
foreach ($this->getRouteList() as $route) {
$method = strtolower($route['method']);
if (!in_array($method, $crudList)) {
if (!in_array($method, $crudList) && $method !== 'options') {
$GLOBALS['log']->error('Route: Method ['.$method.'] does not exist. Please check your route ['.$route['route'].']');
continue;
}
@@ -273,7 +283,7 @@ class Application
protected function initAutoloads()
{
$autoload = new \Espo\Core\Utils\Autoload($this->getContainer()->get('config'), $this->getMetadata(), $this->getContainer()->get('fileManager'));
$autoload = new \Espo\Core\Utils\Autoload($this->getConfig(), $this->getMetadata(), $this->getContainer()->get('fileManager'));
try {
$autoloadList = $autoload->getAll();

View File

@@ -178,7 +178,7 @@ class Container
protected function loadMailSender()
{
$className = $this->getServiceClassName('mailSernder', '\\Espo\\Core\\Mail\\Sender');
$className = $this->getServiceClassName('mailSender', '\\Espo\\Core\\Mail\\Sender');
return new $className(
$this->get('config'),
$this->get('entityManager')
@@ -247,6 +247,14 @@ class Container
);
}
protected function loadInternalAclManager()
{
$className = $this->getServiceClassName('acl', '\\Espo\\Core\\AclManager');
return new $className(
$this->get('container')
);
}
protected function loadAcl()
{
$className = $this->getServiceClassName('acl', '\\Espo\\Core\\Acl');

View File

@@ -58,7 +58,7 @@ class ControllerManager
return $this->metadata;
}
public function process($controllerName, $actionName, $params, $data, $request)
public function process($controllerName, $actionName, $params, $data, $request, $response)
{
$customClassName = '\\Espo\\Custom\\Controllers\\' . Util::normilizeClassName($controllerName);
if (class_exists($customClassName)) {
@@ -112,13 +112,13 @@ class ControllerManager
}
if (method_exists($controller, $beforeMethodName)) {
$controller->$beforeMethodName($params, $data, $request);
$controller->$beforeMethodName($params, $data, $request, $response);
}
$result = $controller->$primaryActionMethodName($params, $data, $request);
$result = $controller->$primaryActionMethodName($params, $data, $request, $response);
if (method_exists($controller, $afterMethodName)) {
$controller->$afterMethodName($params, $data, $request);
$controller->$afterMethodName($params, $data, $request, $response);
}
if (is_array($result) || is_bool($result) || $result instanceof \StdClass) {

View File

@@ -0,0 +1,36 @@
<?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\Exceptions;
class ServiceUnavailable extends \Exception
{
protected $code = 503;
}

View File

@@ -48,6 +48,6 @@ class ContainsType extends \Espo\Core\Formula\Functions\Base
return false;
}
return strpos($string, $needle) !== false;
return mb_strpos($string, $needle) !== false;
}
}

View File

@@ -53,6 +53,6 @@ class LowerCaseType extends \Espo\Core\Formula\Functions\Base
$value = strval($value);
}
return strtolower($value);
return mb_strtolower($value);
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php
/************************************************************************
* This file is part of EspoCRM.
*
@@ -52,9 +52,9 @@ class SubstringType extends \Espo\Core\Formula\Functions\Base
if (count($item->value) > 2) {
$length = $this->evaluate($item->value[2]);
return substr($string, $start, $length);
return mb_substr($string, $start, $length);
} else {
return substr($string, $start);
return mb_substr($string, $start);
}
}
}

View File

@@ -53,6 +53,6 @@ class UpperCaseType extends \Espo\Core\Formula\Functions\Base
$value = strval($value);
}
return strtoupper($value);
return mb_strtoupper($value);
}
}

View File

@@ -176,6 +176,8 @@ class Parser
$this->processStrings($expression, $modifiedExpression, $splitterIndexList, true);
$expressionOutOfBraceList = [];
for ($i = 0; $i < strlen($modifiedExpression); $i++) {
if ($modifiedExpression[$i] === '(') {
$braceCounter++;
@@ -186,6 +188,11 @@ class Parser
if ($braceCounter === 0 && $i < strlen($modifiedExpression) - 1) {
$hasExcessBraces = false;
}
if ($braceCounter === 0) {
$expressionOutOfBraceList[] = true;
} else {
$expressionOutOfBraceList[] = false;
}
}
if ($braceCounter !== 0) {
throw new Error('Incorrect round brackets in expression ' . $expression . '.');
@@ -226,7 +233,13 @@ class Parser
foreach ($this->priorityList as $operationList) {
foreach ($operationList as $operator) {
$index = strpos($expression, $operator, 1);
$startFrom = 1;
while (true) {
$index = strpos($expression, $operator, $startFrom);
if ($index === false) break;
if ($expressionOutOfBraceList[$index]) break;
$startFrom = $index + 1;
}
if ($index !== false) {
$possibleRightOperator = null;
if (strlen($operator) === 1) {

View File

@@ -137,7 +137,7 @@ class Entity extends \Espo\ORM\Entity
}
$this->set($idsAttribute, $ids);
if (!$this->hasFetched($idsAttribute)) {
if (!$this->isNew() && !$this->hasFetched($idsAttribute)) {
$this->setFetched($idsAttribute, $ids);
}
@@ -170,7 +170,13 @@ class Entity extends \Espo\ORM\Entity
$entityName = $entity->get('name');
}
$this->set($field . 'Id', $entityId);
$idAttribute = $field . 'Id';
if (!$this->isNew() && !$this->hasFetched($idAttribute)) {
$this->setFetched($idAttribute, $entityId);
}
$this->set($idAttribute, $entityId);
$this->set($field . 'Name', $entityName);
}

View File

@@ -55,6 +55,8 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
protected $processFieldsBeforeSaveDisabled = false;
protected $processFieldsAfterRemoveDisabled = false;
protected function addDependency($name)
{
$this->dependencies[] = $name;
@@ -124,20 +126,17 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
return;
}
$defs = $metadata->get('entityDefs.' . $entityType);
$defs = $metadata->get(['entityDefs', $entityType]);
foreach ($defs['fields'] as $field => $d) {
if (isset($d['type']) && $d['type'] == 'currency') {
if (!empty($d['notStorable'])) {
continue;
}
if (empty($params['customJoin'])) {
$params['customJoin'] = '';
}
$alias = Util::toUnderScore($field) . "_currency_alias";
$params['customJoin'] .= "
LEFT JOIN currency AS `{$alias}` ON {$alias}.id = ".Util::toUnderScore($entityType).".".Util::toUnderScore($field)."_currency
";
if (!empty($d['notStorable'])) continue;
if (empty($params['leftJoins'])) $params['leftJoins'] = [];
$alias = $field . 'CurrencyRate';
$params['leftJoins'][] = ['Currency', $alias, [
$alias . '.id:' => $field . 'Currency'
]];
}
}
@@ -145,45 +144,23 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
protected function handleEmailAddressParams(&$params)
{
$entityType = $this->entityType;
$defs = $this->getEntityManager()->getMetadata()->get($entityType);
$defs = $this->getEntityManager()->getMetadata()->get($this->entityType);
if (!empty($defs['relations']) && array_key_exists('emailAddresses', $defs['relations'])) {
if (empty($params['leftJoins'])) {
$params['leftJoins'] = array();
}
if (empty($params['whereClause'])) {
$params['whereClause'] = array();
}
if (empty($params['joinConditions'])) {
$params['joinConditions'] = array();
}
$params['leftJoins'][] = 'emailAddresses';
$params['joinConditions']['emailAddresses'] = array(
if (empty($params['leftJoins'])) $params['leftJoins'] = [];
$params['leftJoins'][] = ['emailAddresses', null, [
'primary' => 1
);
]];
}
}
protected function handlePhoneNumberParams(&$params)
{
$entityType = $this->entityType;
$defs = $this->getEntityManager()->getMetadata()->get($entityType);
$defs = $this->getEntityManager()->getMetadata()->get($this->entityType);
if (!empty($defs['relations']) && array_key_exists('phoneNumbers', $defs['relations'])) {
if (empty($params['leftJoins'])) {
$params['leftJoins'] = array();
}
if (empty($params['whereClause'])) {
$params['whereClause'] = array();
}
if (empty($params['joinConditions'])) {
$params['joinConditions'] = array();
}
$params['leftJoins'][] = 'phoneNumbers';
$params['joinConditions']['phoneNumbers'] = array(
if (empty($params['leftJoins'])) $params['leftJoins'] = [];
$params['leftJoins'][] = ['phoneNumbers', null, [
'primary' => 1
);
]];
}
}
@@ -206,6 +183,11 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
protected function afterRemove(Entity $entity, array $options = array())
{
parent::afterRemove($entity, $options);
if (!$this->processFieldsAfterRemoveDisabled) {
$this->processArrayFieldsRemove($entity);
}
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
$this->getEntityManager()->getHookManager()->process($this->entityType, 'afterRemove', $entity, $options);
}
@@ -287,6 +269,8 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
$this->processPhoneNumberSave($entity);
$this->processSpecifiedRelationsSave($entity);
$this->processFileFieldsSave($entity);
$this->processArrayFieldsSave($entity);
$this->processWysiwygFieldsSave($entity);
}
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
@@ -405,6 +389,57 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
}
}
protected function processArrayFieldsSave(Entity $entity)
{
foreach ($entity->getAttributes() as $attribute => $defs) {
if (!isset($defs['type']) || $defs['type'] !== Entity::JSON_ARRAY) continue;
if (!$entity->has($attribute)) continue;
if (!$entity->isAttributeChanged($attribute)) continue;
if (!$entity->getAttributeParam($attribute, 'storeArrayValues')) continue;
if ($entity->getAttributeParam($attribute, 'notStorable')) continue;
$this->getEntityManager()->getRepository('ArrayValue')->storeEntityAttribute($entity, $attribute);
}
}
protected function processWysiwygFieldsSave(Entity $entity)
{
if (!$entity->isNew()) return;
$fieldsDefs = $this->getMetadata()->get(['entityDefs', $entity->getEntityType(), 'fields'], []);
foreach ($fieldsDefs as $field => $defs) {
if (!empty($defs['type']) && $defs['type'] === 'wysiwyg') {
$content = $entity->get($field);
if (!$content) continue;
if (preg_match_all("/\?entryPoint=attachment&amp;id=([^&=\"']+)/", $content, $matches)) {
if (!empty($matches[1]) && is_array($matches[1])) {
foreach ($matches[1] as $id) {
$attachment = $this->getEntityManager()->getEntity('Attachment', $id);
if ($attachment) {
if (!$attachment->get('relatedId') && !$attachment->get('sourceId')) {
$attachment->set([
'relatedId' => $entity->id,
'relatedType' => $entity->getEntityType()
]);
$this->getEntityManager()->saveEntity($attachment);
}
}
}
}
}
}
}
}
protected function processArrayFieldsRemove(Entity $entity)
{
foreach ($entity->getAttributes() as $attribute => $defs) {
if (!isset($defs['type']) || $defs['type'] !== Entity::JSON_ARRAY) continue;
if (!$entity->getAttributeParam($attribute, 'storeArrayValues')) continue;
if ($entity->getAttributeParam($attribute, 'notStorable')) continue;
$this->getEntityManager()->getRepository('ArrayValue')->deleteEntityAttribute($entity, $attribute);
}
}
protected function processEmailAddressSave(Entity $entity)
{
if ($entity->hasRelation('emailAddresses') && $entity->hasAttribute('emailAddress')) {
@@ -465,17 +500,21 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
$data->$columnName = $foreignEntity->get($columnField);
}
$existingColumnsData->$foreignId = $data;
$entity->setFetched($columnsFieldsName, $existingColumnsData);
if (!$entity->isNew()) {
$entity->setFetched($columnsFieldsName, $existingColumnsData);
}
}
}
}
if ($entity->has($fieldName)) {
$entity->setFetched($fieldName, $existingIds);
}
if ($entity->has($columnsFieldsName) && !empty($columns)) {
$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) {
@@ -540,13 +579,17 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
$where[$foreignKey] = $entity->id;
$previousForeignEntity = $this->getEntityManager()->getRepository($foreignEntityType)->where($where)->findOne();
if ($previousForeignEntity) {
$entity->setFetched($idFieldName, $previousForeignEntity->id);
if (!$entity->isNew()) {
$entity->setFetched($idFieldName, $previousForeignEntity->id);
}
if ($previousForeignEntity->id !== $entity->get($idFieldName)) {
$previousForeignEntity->set($foreignKey, null);
$this->getEntityManager()->saveEntity($previousForeignEntity);
}
} else {
$entity->setFetched($idFieldName, null);
if (!$entity->isNew()) {
$entity->setFetched($idFieldName, null);
}
}
if ($entity->get($idFieldName)) {

View File

@@ -61,10 +61,15 @@ class Event extends \Espo\Core\ORM\Repositories\RDB
$pdo->query($sql);
}
protected function afterSave(Entity $entity, array $options = array())
protected function afterSave(Entity $entity, array $options = [])
{
parent::afterSave($entity, $options);
$this->processReminderAfterSave($entity, $options);
parent::afterSave($entity, $options);
}
protected function processReminderAfterSave(Entity $entity, array $options = [])
{
if (
$entity->isNew() ||
$entity->isAttributeChanged('assignedUserId') ||

View File

@@ -150,7 +150,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']);
@@ -161,6 +161,9 @@ class Base
if ($desc) {
$list = array_reverse($list);
}
foreach ($list as $i => $listItem) {
$list[$i] = str_replace(',', '_COMMA_', $listItem);
}
$result['orderBy'] = 'LIST:' . $sortBy . ':' . implode(',', $list);
return;
}
@@ -969,7 +972,7 @@ class Base
protected function getWherePart($item, &$result = null)
{
$part = array();
$part = [];
$attribute = null;
if (!empty($item['field'])) { // for backward compatibility
@@ -1001,165 +1004,199 @@ class Base
if (!array_key_exists('value', $item)) {
$item['value'] = null;
}
$value = $item['value'];
if (!empty($item['type'])) {
switch ($item['type']) {
$type = $item['type'];
switch ($type) {
case 'or':
case 'and':
case 'not':
if (is_array($item['value'])) {
$arr = array();
foreach ($item['value'] as $i) {
if (is_array($value)) {
$arr = [];
foreach ($value as $i) {
$a = $this->getWherePart($i, $result);
foreach ($a as $left => $right) {
if (!empty($right) || is_null($right) || $right === '' || $right === 0 || $right === false) {
$arr[] = array($left => $right);
$arr[] = [$left => $right];
}
}
}
$part[strtoupper($item['type'])] = $arr;
$part[strtoupper($type)] = $arr;
}
break;
case 'like':
$part[$attribute . '*'] = $item['value'];
$part[$attribute . '*'] = $value;
break;
case 'notLike':
$part[$attribute . '!*'] = $item['value'];
$part[$attribute . '!*'] = $value;
break;
case 'equals':
case 'on':
$part[$attribute . '='] = $item['value'];
$part[$attribute . '='] = $value;
break;
case 'startsWith':
$part[$attribute . '*'] = $item['value'] . '%';
$part[$attribute . '*'] = $value . '%';
break;
case 'endsWith':
$part[$attribute . '*'] = '%' . $item['value'];
$part[$attribute . '*'] = '%' . $value;
break;
case 'contains':
$part[$attribute . '*'] = '%' . $item['value'] . '%';
$part[$attribute . '*'] = '%' . $value . '%';
break;
case 'notContains':
$part[$attribute . '!*'] = '%' . $item['value'] . '%';
$part[$attribute . '!*'] = '%' . $value . '%';
break;
case 'notEquals':
case 'notOn':
$part[$attribute . '!='] = $item['value'];
$part[$attribute . '!='] = $value;
break;
case 'greaterThan':
case 'after':
$part[$attribute . '>'] = $item['value'];
$part[$attribute . '>'] = $value;
break;
case 'lessThan':
case 'before':
$part[$attribute . '<'] = $item['value'];
$part[$attribute . '<'] = $value;
break;
case 'greaterThanOrEquals':
$part[$attribute . '>='] = $item['value'];
$part[$attribute . '>='] = $value;
break;
case 'lessThanOrEquals':
$part[$attribute . '<='] = $item['value'];
$part[$attribute . '<='] = $value;
break;
case 'in':
$part[$attribute . '='] = $item['value'];
$part[$attribute . '='] = $value;
break;
case 'notIn':
$part[$attribute . '!='] = $item['value'];
$part[$attribute . '!='] = $value;
break;
case 'isNull':
$part[$attribute . '='] = null;
break;
case 'isNotNull':
case 'ever':
$part[$attribute . '!='] = null;
break;
case 'isTrue':
$part[$attribute . '='] = true;
break;
case 'isFalse':
$part[$attribute . '='] = false;
break;
case 'today':
$part[$attribute . '='] = date('Y-m-d');
break;
case 'past':
$part[$attribute . '<'] = date('Y-m-d');
break;
case 'future':
$part[$attribute . '>='] = date('Y-m-d');
break;
case 'lastSevenDays':
$dt1 = new \DateTime();
$dt2 = clone $dt1;
$dt2->modify('-7 days');
$part['AND'] = array(
$part['AND'] = [
$attribute . '>=' => $dt2->format('Y-m-d'),
$attribute . '<=' => $dt1->format('Y-m-d'),
);
];
break;
case 'lastXDays':
$dt1 = new \DateTime();
$dt2 = clone $dt1;
$number = strval(intval($item['value']));
$number = strval(intval($value));
$dt2->modify('-'.$number.' days');
$part['AND'] = array(
$part['AND'] = [
$attribute . '>=' => $dt2->format('Y-m-d'),
$attribute . '<=' => $dt1->format('Y-m-d'),
);
];
break;
case 'nextXDays':
$dt1 = new \DateTime();
$dt2 = clone $dt1;
$number = strval(intval($item['value']));
$number = strval(intval($value));
$dt2->modify('+'.$number.' days');
$part['AND'] = array(
$part['AND'] = [
$attribute . '>=' => $dt1->format('Y-m-d'),
$attribute . '<=' => $dt2->format('Y-m-d'),
);
];
break;
case 'olderThanXDays':
$dt1 = new \DateTime();
$number = strval(intval($item['value']));
$number = strval(intval($value));
$dt1->modify('-'.$number.' days');
$part[$attribute . '<'] = $dt1->format('Y-m-d');
break;
case 'afterXDays':
$dt1 = new \DateTime();
$number = strval(intval($item['value']));
$number = strval(intval($value));
$dt1->modify('+'.$number.' days');
$part[$attribute . '>'] = $dt1->format('Y-m-d');
break;
case 'currentMonth':
$dt = new \DateTime();
$part['AND'] = array(
$part['AND'] = [
$attribute . '>=' => $dt->modify('first day of this month')->format('Y-m-d'),
$attribute . '<' => $dt->add(new \DateInterval('P1M'))->format('Y-m-d'),
);
];
break;
case 'lastMonth':
$dt = new \DateTime();
$part['AND'] = array(
$part['AND'] = [
$attribute . '>=' => $dt->modify('first day of last month')->format('Y-m-d'),
$attribute . '<' => $dt->add(new \DateInterval('P1M'))->format('Y-m-d'),
);
];
break;
case 'nextMonth':
$dt = new \DateTime();
$part['AND'] = array(
$part['AND'] = [
$attribute . '>=' => $dt->modify('first day of next month')->format('Y-m-d'),
$attribute . '<' => $dt->add(new \DateInterval('P1M'))->format('Y-m-d'),
);
];
break;
case 'currentQuarter':
$dt = new \DateTime();
$quarter = ceil($dt->format('m') / 3);
$dt->modify('first day of January this year');
$part['AND'] = array(
$part['AND'] = [
$attribute . '>=' => $dt->add(new \DateInterval('P'.(($quarter - 1) * 3).'M'))->format('Y-m-d'),
$attribute . '<' => $dt->add(new \DateInterval('P3M'))->format('Y-m-d'),
);
];
break;
case 'lastQuarter':
$dt = new \DateTime();
$quarter = ceil($dt->format('m') / 3);
@@ -1169,33 +1206,37 @@ class Base
$quarter = 4;
$dt->modify('-1 year');
}
$part['AND'] = array(
$part['AND'] = [
$attribute . '>=' => $dt->add(new \DateInterval('P'.(($quarter - 1) * 3).'M'))->format('Y-m-d'),
$attribute . '<' => $dt->add(new \DateInterval('P3M'))->format('Y-m-d'),
);
];
break;
case 'currentYear':
$dt = new \DateTime();
$part['AND'] = array(
$part['AND'] = [
$attribute . '>=' => $dt->modify('first day of January this year')->format('Y-m-d'),
$attribute . '<' => $dt->add(new \DateInterval('P1Y'))->format('Y-m-d'),
);
];
break;
case 'lastYear':
$dt = new \DateTime();
$part['AND'] = array(
$part['AND'] = [
$attribute . '>=' => $dt->modify('first day of January last year')->format('Y-m-d'),
$attribute . '<' => $dt->add(new \DateInterval('P1Y'))->format('Y-m-d'),
);
];
break;
case 'between':
if (is_array($item['value'])) {
$part['AND'] = array(
$attribute . '>=' => $item['value'][0],
$attribute . '<=' => $item['value'][1],
);
if (is_array($value)) {
$part['AND'] = [
$attribute . '>=' => $value[0],
$attribute . '<=' => $value[1],
];
}
break;
case 'columnLike':
case 'columnIn':
case 'columnIsNull':
@@ -1205,30 +1246,30 @@ class Base
$alias = $link . 'Filter' . strval(rand(10000, 99999));
$this->setDistinct(true, $result);
$this->addLeftJoin([$link, $alias], $result);
$value = $item['value'];
$columnKey = $alias . 'Middle.' . $column;
if ($item['type'] === 'columnIn') {
if ($type === 'columnIn') {
$part[$columnKey] = $value;
} else if ($item['type'] === 'columnNotIn') {
} else if ($type === 'columnNotIn') {
$part[$columnKey . '!='] = $value;
} else if ($item['type'] === 'columnIsNull') {
} else if ($type === 'columnIsNull') {
$part[$columnKey] = null;
} else if ($item['type'] === 'columnIsNotNull') {
} else if ($type === 'columnIsNotNull') {
$part[$columnKey . '!='] = null;
} else if ($item['type'] === 'columnLike') {
} else if ($type === 'columnLike') {
$part[$columnKey . '*'] = $value;
} else if ($item['type'] === 'columnStartsWith') {
} else if ($type === 'columnStartsWith') {
$part[$columnKey . '*'] = $value . '%';
} else if ($item['type'] === 'columnEndsWith') {
} else if ($type === 'columnEndsWith') {
$part[$columnKey . '*'] = '%' . $value;
} else if ($item['type'] === 'columnContains') {
} else if ($type === 'columnContains') {
$part[$columnKey . '*'] = '%' . $value . '%';
} else if ($item['type'] === 'columnEquals') {
} else if ($type === 'columnEquals') {
$part[$columnKey . '='] = $value;
} else if ($item['type'] === 'columnNotEquals') {
} else if ($type === 'columnNotEquals') {
$part[$columnKey . '!='] = $value;
}
break;
case 'isNotLinked':
if (!$result) break;
$alias = $attribute . 'IsNotLinkedFilter' . strval(rand(10000, 99999));
@@ -1236,6 +1277,7 @@ class Base
$this->setDistinct(true, $result);
$this->addLeftJoin([$attribute, $alias], $result);
break;
case 'isLinked':
if (!$result) break;
$alias = $attribute . 'IsLinkedFilter' . strval(rand(10000, 99999));
@@ -1243,6 +1285,7 @@ class Base
$this->setDistinct(true, $result);
$this->addLeftJoin([$attribute, $alias], $result);
break;
case 'linkedWith':
$seed = $this->getSeed();
$link = $attribute;
@@ -1250,9 +1293,7 @@ class Base
$alias = $link . 'Filter' . strval(rand(10000, 99999));
$value = $item['value'];
if (is_null($value)) break;
if (is_null($value) || !$value && !is_array($value)) break;
$relationType = $seed->getRelationType($link);
@@ -1281,13 +1322,12 @@ class Base
}
$this->setDistinct(true, $result);
break;
case 'notLinkedWith':
$seed = $this->getSeed();
$link = $attribute;
if (!$seed->hasRelation($link)) break;
$value = $item['value'];
if (is_null($value)) break;
$relationType = $seed->getRelationType($link);
@@ -1313,12 +1353,64 @@ class Base
$part[$key . '!='] = $value;
}
} else if ($relationType == 'hasOne') {
$this->addLeftJoin([$link, alias], $result);
$this->addLeftJoin([$link, $alias], $result);
$part[$alias . '.id!='] = $value;
} else {
break;
}
$this->setDistinct(true, $result);
break;
case 'arrayAnyOf':
case 'arrayNoneOf':
case 'arrayIsEmpty':
case 'arrayIsNotEmpty':
$arrayValueAlias = 'arrayFilter' . strval(rand(10000, 99999));
$arrayAttribute = $attribute;
$arrayEntityType = $this->getEntityType();
$idPart = 'id';
if (strpos($attribute, '.') > 0) {
list($arrayAttributeLink, $arrayAttribute) = explode('.', $attribute);
$seed = $this->getSeed();
$arrayEntityType = $seed->getRelationParam($arrayAttributeLink, 'entity');
$idPart = $arrayAttributeLink . '.id';
}
if ($type === 'arrayAnyOf') {
if (is_null($value) || !$value && !is_array($value)) break;
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
$arrayValueAlias . '.entityId:' => $idPart,
$arrayValueAlias . '.entityType' => $arrayEntityType,
$arrayValueAlias . '.attribute' => $arrayAttribute
]], $result);
$part[$arrayValueAlias . '.value'] = $value;
} else if ($type === 'arrayNoneOf') {
if (is_null($value) || !$value && !is_array($value)) break;
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
$arrayValueAlias . '.entityId:' => $idPart,
$arrayValueAlias . '.entityType' => $arrayEntityType,
$arrayValueAlias . '.attribute' => $arrayAttribute,
$arrayValueAlias . '.value=' => $value
]], $result);
$part[$arrayValueAlias . '.id'] = null;
} else if ($type === 'arrayIsEmpty') {
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
$arrayValueAlias . '.entityId:' => $idPart,
$arrayValueAlias . '.entityType' => $arrayEntityType,
$arrayValueAlias . '.attribute' => $arrayAttribute
]], $result);
$part[$arrayValueAlias . '.id'] = null;
} else if ($type === 'arrayIsNotEmpty') {
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
$arrayValueAlias . '.entityId:' => $idPart,
$arrayValueAlias . '.entityType' => $arrayEntityType,
$arrayValueAlias . '.attribute' => $arrayAttribute
]], $result);
$part[$arrayValueAlias . '.id!='] = null;
}
$this->setDistinct(true, $result);
}
}
@@ -1562,6 +1654,12 @@ class Base
$useFullTextSearch = false;
}
if ($isAuxiliaryUse) {
if (mb_strpos($textFilter, '@') !== false) {
$useFullTextSearch = false;
}
}
if ($useFullTextSearch) {
$textFilter = str_replace(['(', ')'], '', $textFilter);
@@ -1579,10 +1677,11 @@ class Base
$function = 'MATCH_NATURAL_LANGUAGE';
} else {
$function = 'MATCH_BOOLEAN';
$textFilter = str_replace('@', '*', $textFilter);
}
$textFilter = str_replace('"*', '"', $textFilter);
$textFilter = str_replace('*"', '"', $textFilter);
while (strpos($textFilter, '**')) {
$textFilter = str_replace('**', '*', $textFilter);
$textFilter = trim($textFilter);
@@ -1636,18 +1735,31 @@ class Base
$textFilterForFullTextSearch = $textFilter;
$skipWidlcards = false;
if (!$useFullTextSearch) {
if (mb_strpos($textFilter, '*') !== false) {
$skipWidlcards = true;
$textFilter = str_replace('*', '%', $textFilter);
} else {
if (mb_strpos($textFilter, '*') !== false) {
$skipWidlcards = true;
$textFilter = str_replace('*', '%', $textFilter);
} else {
if (!$useFullTextSearch) {
$textFilterForFullTextSearch .= '*';
}
$textFilterForFullTextSearch = str_replace('%', '*', $textFilterForFullTextSearch);
}
$fullTextSearchData = $this->getFullTextSearchDataForTextFilter($textFilterForFullTextSearch, !$useFullTextSearch);
$textFilterForFullTextSearch = str_replace('%', '*', $textFilterForFullTextSearch);
$skipFullTextSearch = false;
if (!$forceFullTextSearch) {
if (mb_strpos($textFilterForFullTextSearch, '*') === 0) {
$skipFullTextSearch = true;
} else if (mb_strpos($textFilterForFullTextSearch, ' *') !== false) {
$skipFullTextSearch = true;
}
}
$fullTextSearchData = null;
if (!$skipFullTextSearch) {
$fullTextSearchData = $this->getFullTextSearchDataForTextFilter($textFilterForFullTextSearch, !$useFullTextSearch);
}
$fullTextGroup = [];

View File

@@ -9,7 +9,8 @@
"type": "text"
},
"website": {
"type": "url"
"type": "url",
"strip": true
},
"emailAddress": {
"type": "email"

View File

@@ -1,6 +1,5 @@
{
"links": {
"meetings": "Meetings",
"calls": "Anrufe",
"tasks": "Aufgaben"
},

View File

@@ -5,7 +5,6 @@
"website": "Webseite"
},
"links": {
"meetings": "Meetings",
"calls": "Anrufe",
"tasks": "Aufgaben"
},

View File

@@ -4,7 +4,6 @@
"dateStart": "Startdatum",
"dateEnd": "Enddatum",
"duration": "Dauer",
"status": "Status",
"reminders": "Erinnerungen"
},
"links": {
@@ -20,7 +19,6 @@
"labels": {
"Create {entityType}": "{entityTypeTranslated} erstellen",
"Schedule {entityType}": "{entityTypeTranslated} planen",
"Log {entityType}": "Log {entityTypeTranslated}",
"Set Held": "Auf durchgeführt setzen",
"Set Not Held": "Auf nicht durchgeführt setzen"
},

View File

@@ -3,7 +3,6 @@
"address": "Adresse"
},
"links": {
"meetings": "Meetings",
"calls": "Anrufe",
"tasks": "Aufgaben"
},

View File

@@ -1,6 +1,6 @@
{
"links": {
"meetings": "Juntas",
"meetings": "Presentaciones",
"calls": "Llamadas",
"tasks": "Tareas"
},

View File

@@ -5,7 +5,7 @@
"website": "Sitio Web"
},
"links": {
"meetings": "Juntas",
"meetings": "Presentaciones",
"calls": "Llamadas",
"tasks": "Tareas"
},

View File

@@ -3,7 +3,7 @@
"address": "Dirección"
},
"links": {
"meetings": "Juntas",
"meetings": "Presentaciones",
"calls": "Llamadas",
"tasks": "Tareas"
},

View File

@@ -631,7 +631,13 @@ abstract class Base
protected function systemRebuild()
{
return $this->getContainer()->get('dataManager')->rebuild();
try {
return $this->getContainer()->get('dataManager')->rebuild();
} catch (\Exception $e) {
$GLOBALS['log']->error('Database rebuild failure, details: '.$e->getMessage().'.');
}
return false;
}
/**

View File

@@ -168,7 +168,7 @@ class Auth
return;
}
$user = $this->authentication->login($username, $password, $authToken);
$user = $this->authentication->login($username, $password, $authToken, $this->isPortal());
$authLogRecord = null;
@@ -180,6 +180,10 @@ class Auth
return;
}
if (!$user->isAdmin() && $this->getConfig()->get('maintenanceMode')) {
throw new \Espo\Core\Exceptions\ServiceUnavailable("Application is in maintenance mode.");
}
if (!$user->isActive()) {
$GLOBALS['log']->info("AUTH: Trying to login as user '".$user->get('userName')."' which is not active.");
$this->logDenied($authLogRecord, 'INACTIVE_USER');

View File

@@ -33,7 +33,7 @@ use \Espo\Core\Exceptions\Error;
class Espo extends Base
{
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null)
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null, $isPortal = null)
{
if ($authToken) {
$hash = $authToken->get('hash');
@@ -51,4 +51,3 @@ class Espo extends Base
return $user;
}
}

View File

@@ -34,7 +34,7 @@ use Espo\Core\Utils\Config;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Utils\Auth;
class LDAP extends Base
class LDAP extends Espo
{
private $utils;
@@ -64,6 +64,16 @@ class LDAP extends Base
'defaultTeamId' => 'userDefaultTeamId',
);
/**
* User field name => option name
*
* @var array
*/
protected $portalUserFieldMap = array(
'portalsIds' => 'portalUserPortalsIds',
'portalRolesIds' => 'portalUserRolesIds',
);
public function __construct(Config $config, EntityManager $entityManager, Auth $auth)
{
parent::__construct($config, $entityManager, $auth);
@@ -100,12 +110,19 @@ class LDAP extends Base
*
* @return \Espo\Entities\User | null
*/
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null)
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null, $isPortal = null)
{
if ($authToken) {
return $this->loginByToken($username, $authToken);
}
if ($isPortal) {
$useLdapAuthForPortalUser = $this->getUtils()->getOption('portalUserLdapAuth');
if (!$useLdapAuthForPortalUser) {
return parent::login($username, $password, $authToken, $isPortal);
}
}
$ldapClient = $this->getLdapClient();
/* Login LDAP system user (ldapUsername, ldapPassword) */
@@ -160,7 +177,7 @@ class LDAP extends Base
$isCreateUser = $this->getUtils()->getOption('createEspoUser');
if (!isset($user) && $isCreateUser) {
$userData = $ldapClient->getEntry($userDn);
$user = $this->createUser($userData);
$user = $this->createUser($userData, $isPortal);
}
return $user;
@@ -184,7 +201,7 @@ class LDAP extends Base
$user = $this->getEntityManager()->getEntity('User', $userId);
$tokenUsername = $user->get('userName');
if ($username != $tokenUsername) {
if (strtolower($username) != strtolower($tokenUsername)) {
$GLOBALS['log']->alert('Unauthorized access attempt for user ['.$username.'] from IP ['.$_SERVER['REMOTE_ADDR'].']');
return null;
}
@@ -224,10 +241,11 @@ class LDAP extends Base
* Create Espo user with data gets from LDAP server
*
* @param array $userData LDAP entity data
* @param boolean $isPortal Is portal user
*
* @return \Espo\Entities\User
*/
protected function createUser(array $userData)
protected function createUser(array $userData, $isPortal = false)
{
$GLOBALS['log']->info('Creating new user ...');
$data = array();
@@ -246,7 +264,13 @@ class LDAP extends Base
}
//set user fields
$userFields = $this->loadFields('user');
if ($isPortal) {
$userFields = $this->loadFields('portalUser');
$userFields['isPortalUser'] = true;
} else {
$userFields = $this->loadFields('user');
}
foreach ($userFields as $fieldName => $fieldValue) {
$data[$fieldName] = $fieldValue;
}
@@ -328,4 +352,4 @@ class LDAP extends Base
return $fields;
}
}
}

View File

@@ -67,6 +67,9 @@ class Utils
'userTeamsIds' => 'ldapUserTeamsIds',
'userDefaultTeamId' => 'ldapUserDefaultTeamId',
'userObjectClass' => 'ldapUserObjectClass',
'portalUserLdapAuth' => 'ldapPortalUserLdapAuth',
'portalUserPortalsIds' => 'ldapPortalUserPortalsIds',
'portalUserRolesIds' => 'ldapPortalUserRolesIds',
);
/**
@@ -86,6 +89,9 @@ class Utils
'userLoginFilter',
'userTeamsIds',
'userDefaultTeamId',
'portalUserLdapAuth',
'portalUserPortalsIds',
'portalUserRolesIds',
);
/**
@@ -163,7 +169,7 @@ class Utils
*/
public function getOption($name, $returns = null)
{
if (isset($this->options)) {
if (!isset($this->options)) {
$this->getOptions();
}
@@ -187,4 +193,4 @@ class Utils
return $zendOptions;
}
}
}

View File

@@ -379,14 +379,14 @@ class MySqlPlatform extends \Doctrine\DBAL\Platforms\MySqlPlatform
// Charset
if ( ! isset($options['charset'])) {
$options['charset'] = 'utf8';
$options['charset'] = 'utf8mb4';
}
$tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']);
// Collate
if ( ! isset($options['collate'])) {
$options['collate'] = 'utf8_unicode_ci';
$options['collate'] = 'utf8mb4_unicode_ci';
}
$tableOptions[] = sprintf('COLLATE %s', $options['collate']);
@@ -452,5 +452,17 @@ class MySqlPlatform extends \Doctrine\DBAL\Platforms\MySqlPlatform
return 'MEDIUMTEXT';
}
public function getColumnDeclarationListSQL(array $fields)
{
$queryFields = array();
foreach ($fields as $fieldName => $field) {
$quotedFieldName = $this->espoQuote($fieldName);
$queryFields[] = $this->getColumnDeclarationSQL($quotedFieldName, $field);
}
return implode(', ', $queryFields);
}
//end: ESPO
}
}

View File

@@ -90,6 +90,7 @@ class Converter
'select' => 'select',
'orderBy' => 'orderBy',
'where' => 'where',
'storeArrayValues' => 'storeArrayValues'
);
protected $idParams = array(

View File

@@ -39,28 +39,28 @@ class Currency extends Base
$currencyColumnName = Util::toUnderScore($fieldName);
$alias = Util::toUnderScore($fieldName) . "_currency_alias";
$alias = $fieldName . 'CurrencyRate';
$d = array(
$entityName => array(
'fields' => array(
$fieldName => array(
"type" => "float",
"orderBy" => $converedFieldName . " {direction}"
)
),
),
);
$d = [
$entityName => [
'fields' => [
$fieldName => [
'type' => 'float',
'orderBy' => $converedFieldName . ' {direction}'
]
]
]
];
$params = $this->getFieldParams($fieldName);
if (!empty($params['notStorable'])) {
$d[$entityName]['fields'][$fieldName]['notStorable'] = true;
} else {
$d[$entityName]['fields'][$fieldName . 'Converted'] = array(
$d[$entityName]['fields'][$fieldName . 'Converted'] = [
'type' => 'float',
'select' => Util::toUnderScore($entityName) . "." . $currencyColumnName . " * {$alias}.rate" ,
'where' =>
array (
[
"=" => Util::toUnderScore($entityName) . "." . $currencyColumnName . " * {$alias}.rate = {value}",
">" => Util::toUnderScore($entityName) . "." . $currencyColumnName . " * {$alias}.rate > {value}",
"<" => Util::toUnderScore($entityName) . "." . $currencyColumnName . " * {$alias}.rate < {value}",
@@ -68,11 +68,11 @@ class Currency extends Base
"<=" => Util::toUnderScore($entityName) . "." . $currencyColumnName . " * {$alias}.rate <= {value}",
"<>" => Util::toUnderScore($entityName) . "." . $currencyColumnName . " * {$alias}.rate <> {value}",
"IS NULL" => Util::toUnderScore($entityName) . "." . $currencyColumnName . ' IS NULL',
"IS NOT NULL" => Util::toUnderScore($entityName) . "." . $currencyColumnName . ' IS NOT NULL',
),
"IS NOT NULL" => Util::toUnderScore($entityName) . "." . $currencyColumnName . ' IS NOT NULL'
],
'notStorable' => true,
'orderBy' => $converedFieldName . " {direction}"
);
];
}
return $d;

View File

@@ -88,35 +88,43 @@ class Email extends Base
$fieldName .'IsOptedOut' => array(
'type' => 'bool',
'notStorable' => true,
'select' => 'emailAddresses.opt_out'
'select' => 'emailAddresses.opt_out',
'where' => [
'= TRUE' => [
'sql' => 'emailAddresses.opt_out = true AND emailAddresses.opt_out IS NOT NULL'
],
'= FALSE' => [
'sql' => 'emailAddresses.opt_out = false OR emailAddresses.opt_out IS NULL'
]
],
'orderBy' => 'emailAddresses.opt_out {direction}'
)
),
'relations' => array(
'emailAddresses' => array(
'relations' => [
'emailAddresses' => [
'type' => 'manyMany',
'entity' => 'EmailAddress',
'relationName' => 'entityEmailAddress',
'midKeys' => array(
'entity_id',
'email_address_id',
),
'conditions' => array(
'entityType' => $entityName,
),
'additionalColumns' => array(
'entityType' => array(
'midKeys' => [
'entityId',
'emailAddressId'
],
'conditions' => [
'entityType' => $entityName
],
'additionalColumns' => [
'entityType' => [
'type' => 'varchar',
'len' => 100,
),
'primary' => array(
'len' => 100
],
'primary' => [
'type' => 'bool',
'default' => false,
),
),
),
),
),
'default' => false
]
]
]
]
)
);
}
}

View File

@@ -33,28 +33,29 @@ class LinkMultiple extends Base
{
protected function load($fieldName, $entityName)
{
$data = array(
$entityName => array (
'fields' => array(
$fieldName.'Ids' => array(
$data = [
$entityName => [
'fields' => [
$fieldName.'Ids' => [
'type' => 'jsonArray',
'notStorable' => true,
'isLinkMultipleIdList' => true,
'relation' => $fieldName
),
$fieldName.'Names' => array(
'relation' => $fieldName,
'isUnordered' => true
],
$fieldName.'Names' => [
'type' => 'jsonObject',
'notStorable' => true,
'isLinkMultipleNameMap' => true
)
)
),
'unset' => array(
$entityName => array(
'fields.'.$fieldName
)
)
);
]
]
],
'unset' => [
$entityName => [
'fields.' . $fieldName
]
]
];
$fieldParams = $this->getFieldParams();
@@ -67,10 +68,10 @@ class LinkMultiple extends Base
$columns = $this->getMetadata()->get("entityDefs.{$entityName}.fields.{$fieldName}.columns");
if (!empty($columns)) {
$data[$entityName]['fields'][$fieldName . 'Columns'] = array(
$data[$entityName]['fields'][$fieldName . 'Columns'] = [
'type' => 'jsonObject',
'notStorable' => true,
);
];
}
return $data;

View File

@@ -43,7 +43,8 @@ class LinkParent extends Base
$fieldName.'Type' => array(
'type' => 'foreignType',
'notNull' => false,
'index' => $fieldName
'index' => $fieldName,
'len' => 100
),
$fieldName.'Name' => array(
'type' => 'varchar',

View File

@@ -85,34 +85,76 @@ class Phone extends Base
'type' => 'text',
'notStorable' => true
),
$fieldName . 'Numeric' => [
'type' => 'varchar',
'notStorable' => true,
'where' => [
'LIKE' => \Espo\Core\Utils\Util::toUnderScore($entityName) . ".id IN (
SELECT entity_id
FROM entity_phone_number
JOIN phone_number ON phone_number.id = entity_phone_number.phone_number_id
WHERE
entity_phone_number.deleted = 0 AND entity_phone_number.entity_type = '{$entityName}' AND
phone_number.deleted = 0 AND phone_number.numeric LIKE {value}
)",
'=' => [
'leftJoins' => [['phoneNumbers', 'phoneNumbersNumericMultiple']],
'sql' => 'phoneNumbersNumericMultiple.numeric = {value}',
'distinct' => true
],
'<>' => [
'leftJoins' => [['phoneNumbers', 'phoneNumbersNumericMultiple']],
'sql' => 'phoneNumbersNumericMultiple.numeric <> {value}',
'distinct' => true
],
'IN' => [
'leftJoins' => [['phoneNumbers', 'phoneNumbersNumericMultiple']],
'sql' => 'phoneNumbersNumericMultiple.numeric IN {value}',
'distinct' => true
],
'NOT IN' => [
'leftJoins' => [['phoneNumbers', 'phoneNumbersNumericMultiple']],
'sql' => 'phoneNumbersNumericMultiple.numeric NOT IN {value}',
'distinct' => true
],
'IS NULL' => [
'leftJoins' => [['phoneNumbers', 'phoneNumbersNumericMultiple']],
'sql' => 'phoneNumbersNumericMultiple.numeric IS NULL',
'distinct' => true
],
'IS NOT NULL' => [
'leftJoins' => [['phoneNumbers', 'phoneNumbersNumericMultiple']],
'sql' => 'phoneNumbersNumericMultiple.numeric IS NOT NULL',
'distinct' => true
]
]
]
),
'relations' => array(
'phoneNumbers' => array(
'relations' => [
'phoneNumbers' => [
'type' => 'manyMany',
'entity' => 'PhoneNumber',
'relationName' => 'entityPhoneNumber',
'midKeys' => array(
'entity_id',
'phone_number_id',
),
'conditions' => array(
'entityType' => $entityName,
),
'additionalColumns' => array(
'entityType' => array(
'midKeys' => [
'entityId',
'phoneNumberId'
],
'conditions' => [
'entityType' => $entityName
],
'additionalColumns' => [
'entityType' => [
'type' => 'varchar',
'len' => 100,
),
'primary' => array(
'len' => 100
],
'primary' => [
'type' => 'bool',
'default' => false,
),
),
),
),
),
'default' => false
]
]
]
]
)
);
}
}

View File

@@ -36,30 +36,30 @@ class EntityTeam extends Base
$linkParams = $this->getLinkParams();
$foreignEntityName = $this->getForeignEntityName();
return array(
$entityName => array(
'relations' => array(
$linkName => array(
return [
$entityName => [
'relations' => [
$linkName => [
'type' => 'manyMany',
'entity' => $foreignEntityName,
'relationName' => lcfirst($linkParams['relationName']),
'midKeys' => array(
'entity_id',
'team_id',
),
'conditions' => array(
'entityType' => $entityName,
),
'additionalColumns' => array(
'entityType' => array(
'midKeys' => [
'entityId',
'teamId'
],
'conditions' => [
'entityType' => $entityName
],
'additionalColumns' => [
'entityType' => [
'type' => 'varchar',
'len' => 100,
),
),
),
),
),
);
'len' => 100
]
]
]
]
]
];
}
}

View File

@@ -44,7 +44,7 @@ class HasChildren extends Base
'notStorable' => true,
),
$linkName.'Names' => array(
'type' => 'varchar',
'type' => 'jsonObject',
'notStorable' => true,
),
),

View File

@@ -373,6 +373,13 @@ class Converter
}
}
$databaseParams = $this->getConfig()->get('database');
if (!isset($databaseParams['charset']) || $databaseParams['charset'] == 'utf8mb4') {
$dbFieldParams['platformOptions'] = array(
'collation' => 'utf8mb4_unicode_ci',
);
}
switch ($fieldParams['type']) {
case 'id':
case 'foreignId':
@@ -515,4 +522,4 @@ class Converter
return $tables;
}
}
}

View File

@@ -582,6 +582,9 @@ class EntityManager
} else {
$relationName = lcfirst($entity) . $entityForeign;
}
if (strlen($relationName) > 100) {
throw new Error('Relation name should not be longer than 100.');
}
if ($this->getMetadata()->get(['scopes', ucfirst($relationName)])) {
throw new Conflict("Entity with the same name '{$relationName}' exists.");
}
@@ -594,8 +597,8 @@ class EntityManager
throw new BadRequest();
}
if (strlen($link) > 255 || strlen($linkForeign) > 255) {
throw new Error('Link name should not be longer than 255.');
if (strlen($link) > 100 || strlen($linkForeign) > 100) {
throw new Error('Link name should not be longer than 100.');
}
if (is_numeric($link[0]) || is_numeric($linkForeign[0])) {
@@ -662,7 +665,6 @@ class EntityManager
$link => array(
"type" => "linkMultiple",
"layoutDetailDisabled" => !$linkMultipleField,
"layoutListDisabled" => true,
"layoutMassUpdateDisabled" => !$linkMultipleField,
"noLoad" => !$linkMultipleField,
"importDisabled" => !$linkMultipleField,
@@ -727,7 +729,6 @@ class EntityManager
$linkForeign => array(
"type" => "linkMultiple",
"layoutDetailDisabled" => !$linkMultipleFieldForeign,
"layoutListDisabled" => true,
"layoutMassUpdateDisabled" => !$linkMultipleFieldForeign,
"noLoad" => !$linkMultipleFieldForeign,
"importDisabled" => !$linkMultipleFieldForeign,
@@ -751,7 +752,6 @@ class EntityManager
$link => array(
"type" => "linkMultiple",
"layoutDetailDisabled" => !$linkMultipleField,
"layoutListDisabled" => true,
"layoutMassUpdateDisabled" => !$linkMultipleField,
"importDisabled" => !$linkMultipleField,
"noLoad" => !$linkMultipleField,
@@ -774,7 +774,6 @@ class EntityManager
$linkForeign => array(
"type" => "linkMultiple",
"layoutDetailDisabled" => !$linkMultipleFieldForeign,
"layoutListDisabled" => true,
"layoutMassUpdateDisabled" => !$linkMultipleFieldForeign,
"importDisabled" => !$linkMultipleFieldForeign,
"noLoad" => !$linkMultipleFieldForeign,
@@ -848,7 +847,6 @@ class EntityManager
$link => array(
"type" => "linkMultiple",
"layoutDetailDisabled" => !$linkMultipleField,
"layoutListDisabled" => true,
"layoutMassUpdateDisabled" => !$linkMultipleField,
"noLoad" => !$linkMultipleField,
"importDisabled" => !$linkMultipleField,
@@ -873,7 +871,6 @@ class EntityManager
$linkForeign => array(
"type" => "linkMultiple",
"layoutDetailDisabled" => !$linkMultipleFieldForeign,
"layoutListDisabled" => true,
"layoutMassUpdateDisabled" => !$linkMultipleFieldForeign,
"noLoad" => !$linkMultipleFieldForeign,
"importDisabled" => !$linkMultipleFieldForeign,

View File

@@ -98,8 +98,8 @@ class FieldManager
throw new BadRequest();
}
if (strlen($name) > 255) {
throw new Error('Field name should not be longer than 255.');
if (strlen($name) > 100) {
throw new Error('Field name should not be longer than 100.');
}
if (is_numeric($name[0])) {

View File

@@ -589,7 +589,7 @@ class Manager
}
}
if ($removeWithDir) {
if ($removeWithDir && $this->isDirEmpty($dirPath)) {
$result &= $this->rmdir($dirPath);
}
@@ -908,4 +908,3 @@ class Manager
return $fullPath;
}
}

View File

@@ -106,6 +106,10 @@ class System
*/
public function getPhpBin()
{
if (isset($_SERVER['PHP_PATH']) && !empty($_SERVER['PHP_PATH'])) {
return $_SERVER['PHP_PATH'];
}
return defined("PHP_BINDIR") ? PHP_BINDIR . DIRECTORY_SEPARATOR . 'php' : 'php';
}

View File

@@ -136,6 +136,11 @@ class Util
return static::fromCamelCase($name, '_');
}
public static function camelCaseToUnderscore($value)
{
return static::toUnderScore($value);
}
/**
* Merge arrays recursively (default PHP function is not suitable)
*

View File

@@ -174,6 +174,8 @@ return array (
'massPrintPdfMaxCount' => 50,
'emailKeepParentTeamsEntityList' => ['Case'],
'recordListMaxSizeLimit' => 200,
'noteDeleteThresholdPeriod' => '1 month',
'noteEditThresholdPeriod' => '7 days',
'isInstalled' => false
);

View File

@@ -133,6 +133,7 @@ return array ( 'defaultPermissions' =>
'ldapAccountFilterFormat',
'ldapTryUsernameSplit',
'ldapOptReferrals',
'ldapPortalUserLdapAuth',
'ldapCreateEspoUser',
'ldapAccountDomainName',
'ldapAccountDomainNameShort',
@@ -153,12 +154,17 @@ return array ( 'defaultPermissions' =>
'ldapUserDefaultTeamName',
'ldapUserTeamsIds',
'ldapUserTeamsNames',
'ldapPortalUserPortalsIds',
'ldapPortalUserPortalsNames',
'ldapPortalUserRolesIds',
'ldapPortalUserRolesNames',
'cleanupJobPeriod',
'cleanupActionHistoryPeriod',
'adminNotifications',
'adminNotificationsNewVersion',
'adminNotificationsCronIsNotConfigured',
'adminNotificationsNewExtensionVersion'
'adminNotificationsNewExtensionVersion',
'leadCaptureAllowOrigin'
),
'userItems' =>
array (
@@ -175,5 +181,5 @@ return array ( 'defaultPermissions' =>
'ldapUserEmailAddressAttribute' => 'mail',
'ldapUserPhoneNumberAttribute' => 'telephoneNumber',
'ldapUserObjectClass' => 'person',
'ldapPortalUserLdapAuth' => false,
);

View File

@@ -0,0 +1,37 @@
<?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\Entities;
use Espo\Core\Exceptions\Error;
class ArrayValue extends \Espo\Core\ORM\Entity
{
}

View File

@@ -0,0 +1,35 @@
<?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\Entities;
class Currency extends \Espo\Core\ORM\Entity
{
}

View File

@@ -0,0 +1,35 @@
<?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\Entities;
class LeadCapture extends \Espo\Core\ORM\Entity
{
}

View File

@@ -0,0 +1,35 @@
<?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\Entities;
class LeadCaptureLogRecord extends \Espo\Core\ORM\Entity
{
}

View File

@@ -31,6 +31,18 @@ namespace Espo\Entities;
class Note extends \Espo\Core\ORM\Entity
{
private $aclIsProcessed = false;
public function setAclIsProcessed()
{
$this->aclIsProcessed = true;
}
public function isAclProcessed()
{
return $this->aclIsProcessed;
}
public function loadAttachments()
{
$data = $this->get('data');

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\EntryPoints;
use \Espo\Core\Exceptions\NotFound;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\BadRequest;
use \Espo\Core\Exceptions\Error;
class ConfirmOptIn extends \Espo\Core\EntryPoints\Base
{
public static $authRequired = false;
public function run()
{
if (empty($_GET['id'])) throw new BadRequest();
$id = $_GET['id'];
$data = $this->getServiceFactory()->create('LeadCapture')->confirmOptIn($id);
if ($data->status === 'success') {
$action = 'optInConfirmationSuccess';
} else if ($data->status === 'expired') {
$action = 'optInConfirmationExpired';
} else {
throw new Error();
}
$runScript = "
Espo.require('controllers/lead-capture-opt-in-confirmation', function (Controller) {
var controller = new Controller(app.baseController.params, app.getControllerInjection());
controller.masterView = app.masterView;
controller.doAction('".$action."', ".json_encode($data).");
});
";
$this->getClientManager()->display($runScript);
}
}

View File

@@ -129,7 +129,7 @@ class Image extends \Espo\Core\EntryPoints\Base
}
if (!empty($size)) {
$fileName = $sourceId . '_' . $size . '.jpg';
$fileName = $size . '-' . $attachment->get('name');
} else {
$fileName = $attachment->get('name');
}
@@ -197,9 +197,10 @@ class Image extends \Espo\Core\EntryPoints\Base
break;
}
$targetImage = imagerotate($targetImage, array_values([0, 0, 0, 180, 0, 0, -90, 0, 90])[@exif_read_data($filePath)['Orientation'] ?: 0], 0);
if (function_exists('exif_read_data')) {
$targetImage = imagerotate($targetImage, array_values([0, 0, 0, 180, 0, 0, -90, 0, 90])[@exif_read_data($filePath)['Orientation'] ?: 0], 0);
}
return $targetImage;
}
}

View File

@@ -58,18 +58,32 @@ class AssignmentEmailNotification extends \Espo\Core\Hooks\Base
foreach ($userIdList as $userId) {
if (in_array($userId, $fetchedAssignedUserIdList)) continue;
if ($this->getUser()->id === $userId) continue;
if (!$this->isNotSelfAssignment($entity, $userId)) continue;
$this->createJob($entity, $userId);
}
} else {
$userId = $entity->get('assignedUserId');
if (!empty($userId) && $userId != $this->getUser()->id && $entity->isAttributeChanged('assignedUserId')) {
if (!empty($userId) && $entity->isAttributeChanged('assignedUserId') && $this->isNotSelfAssignment($entity, $userId)) {
$this->createJob($entity, $userId);
}
}
}
}
protected function isNotSelfAssignment(Entity $entity, $assignedUserId)
{
if ($entity->hasAttribute('createdById') && $entity->hasAttribute('modifiedById')) {
if ($entity->isNew()) {
$isNotSelfAssignment = $assignedUserId !== $entity->get('createdById');
} else {
$isNotSelfAssignment = $assignedUserId !== $entity->get('modifiedById');
}
} else {
$isNotSelfAssignment = $assignedUserId !== $this->getUser()->id;
}
return $isNotSelfAssignment;
}
protected function createJob(Entity $entity, $userId)
{
$job = $this->getEntityManager()->getEntity('Job');

View File

@@ -178,7 +178,7 @@ class Stream extends \Espo\Core\Hooks\Base
return $userIdList;
}
public function afterSave(Entity $entity, array $options = array())
public function afterSave(Entity $entity, array $options = [])
{
$entityType = $entity->getEntityType();

View File

@@ -0,0 +1,126 @@
<?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\Hooks\Common;
use Espo\ORM\Entity;
class StreamNotesAcl extends \Espo\Core\Hooks\Base
{
protected $streamService = null;
public static $order = 10;
protected function init()
{
parent::init();
$this->addDependency('serviceFactory');
$this->addDependency('aclManager');
}
protected function getServiceFactory()
{
return $this->getInjection('serviceFactory');
}
protected function getAclManager()
{
return $this->getInjection('aclManager');
}
public function afterSave(Entity $entity, array $options = [])
{
if (!empty($options['noStream'])) return;
if (!empty($options['silent'])) return;
if (!empty($options['skipStreamNotesAcl'])) return;
if ($entity->isNew()) return;
$entityType = $entity->getEntityType();
if (in_array($entityType, ['Note', 'User', 'Team', 'Role', 'Portal', 'PortalRole'])) return;
if (!$this->getMetadata()->get(['scopes', $entityType, 'acl'])) return;
if (!$this->getMetadata()->get(['scopes', $entityType, 'object'])) return;
$ownerUserIdAttribute = $this->getAclManager()->getImplementation($entityType)->getOwnerUserIdAttribute($entity);
$usersAttributeIsChanged = false;
$teamsAttributeIsChanged = false;
if ($ownerUserIdAttribute) {
if ($entity->isAttributeChanged($ownerUserIdAttribute)) {
$usersAttributeIsChanged = true;
if ($entity->getAttributeParam($ownerUserIdAttribute, 'isLinkMultipleIdList')) {
$userIdList = $entity->get($ownerUserIdAttribute);
} else {
$userId = $entity->get($ownerUserIdAttribute);
if ($userId) {
$userIdList = [$userId];
} else {
$userIdList = [];
}
}
}
}
if ($entity->hasLinkMultipleField('teams') && $entity->isAttributeChanged('teamsIds')) {
$teamsAttributeIsChanged = true;
$teamIdList = $entity->get('teamsIds');
}
if ($usersAttributeIsChanged || $teamsAttributeIsChanged) {
$noteList = $this->getEntityManager()->getRepository('Note')->where([
'OR' => [
[
'relatedId' => $entity->id,
'relatedType' => $entityType
],
[
'parentId' => $entity->id,
'parentType' => $entityType,
'superParentId!=' => null,
'relatedId' => null
]
]
])->select(['id'])->find();
foreach ($noteList as $note) {
if ($teamsAttributeIsChanged) {
$note->set('teamsIds', $teamIdList);
}
if ($usersAttributeIsChanged) {
$note->set('usersIds', $userIdList);
}
$this->getEntityManager()->saveEntity($note);
}
}
}
}

View File

@@ -40,6 +40,7 @@ class Notifications extends \Espo\Core\Hooks\Base
protected function init()
{
$this->addDependency('serviceFactory');
$this->addDependency('container');
}
protected function getServiceFactory()
@@ -47,6 +48,16 @@ class Notifications extends \Espo\Core\Hooks\Base
return $this->getInjection('serviceFactory');
}
protected function getInternalAclManager()
{
return $this->getInjection('container')->get('internalAclManager');
}
protected function getPortalAclManager()
{
return $this->getInjection('container')->get('portalAclManager');
}
protected function getMentionedUserIdList($entity)
{
$mentionedUserList = array();
@@ -60,7 +71,7 @@ class Notifications extends \Espo\Core\Hooks\Base
return $mentionedUserList;
}
protected function getSubscriberIdList($parentType, $parentId, $isInternal = false)
protected function getSubscriberList($parentType, $parentId, $isInternal = false)
{
$pdo = $this->getEntityManager()->getPDO();
@@ -80,15 +91,14 @@ class Notifications extends \Espo\Core\Hooks\Base
user.is_portal_user = 0
";
}
$sth = $pdo->prepare($sql);
$sth->execute();
$userIdList = [];
while ($row = $sth->fetch(\PDO::FETCH_ASSOC)) {
if ($this->getUser()->id != $row['userId']) {
$userIdList[] = $row['userId'];
}
}
return $userIdList;
$userList = $this->getEntityManager()->getRepository('User')->where([
'isActive' => true
])->select(['id', 'isPortalUser', 'isAdmin'])->find([
'customWhere' => "AND user.id IN (".$sql.")"
]);
return $userList;
}
public function afterSave(Entity $entity)
@@ -99,13 +109,86 @@ class Notifications extends \Espo\Core\Hooks\Base
$superParentType = $entity->get('superParentType');
$superParentId = $entity->get('superParentId');
$userIdList = [];
$notifyUserIdList = [];
if ($parentType && $parentId) {
$userIdList = array_merge($userIdList, $this->getSubscriberIdList($parentType, $parentId, $entity->get('isInternal')));
if ($superParentType && $superParentId) {
$userIdList = array_merge($userIdList, $this->getSubscriberIdList($superParentType, $superParentId, $entity->get('isInternal')));
$userList = $this->getSubscriberList($parentType, $parentId, $entity->get('isInternal'));
$userIdMetList = [];
foreach ($userList as $user) {
$userIdMetList[] = $user->id;
}
if ($superParentType && $superParentId) {
$additionalUserList = $this->getSubscriberList($superParentType, $superParentId, $entity->get('isInternal'));
foreach ($additionalUserList as $user) {
if ($user->isPortal()) continue;
if (in_array($user->id, $userIdMetList)) continue;
$userIdMetList[] = $user->id;
$userList[] = $user;
}
}
if ($entity->get('relatedType')) {
$targetType = $entity->get('relatedType');
} else {
$targetType = $parentType;
}
$skipAclCheck = false;
if (!$entity->isAclProcessed()) {
$skipAclCheck = true;
} else {
$teamIdList = $entity->getLinkMultipleIdList('teams');
$userIdList = $entity->getLinkMultipleIdList('users');
}
foreach ($userList as $user) {
if ($skipAclCheck) {
$notifyUserIdList[] = $user->id;
continue;
}
if ($user->isAdmin()) {
$notifyUserIdList[] = $user->id;
continue;
}
if ($user->isPortal()) {
if ($entity->get('relatedType')) {
continue;
} else {
$notifyUserIdList[] = $user->id;
}
continue;
}
$level = $this->getInternalAclManager()->getLevel($user, $targetType, 'read');
if ($level === 'all') {
$notifyUserIdList[] = $user->id;
continue;
} else if ($level === 'team') {
if (in_array($user->id, $userIdList)) {
$notifyUserIdList[] = $user->id;
continue;
}
if (!empty($teamIdList)) {
$userTeamIdList = $user->getLinkMultipleIdList('teams');
foreach ($teamIdList as $teamId) {
if (in_array($teamId, $userTeamIdList)) {
$notifyUserIdList[] = $user->id;
break;
}
}
}
continue;
} else if ($level === 'own') {
if (in_array($user->id, $userIdList)) {
$notifyUserIdList[] = $user->id;
continue;
}
}
}
} else {
$targetType = $entity->get('targetType');
if ($targetType === 'users') {
@@ -113,8 +196,8 @@ class Notifications extends \Espo\Core\Hooks\Base
if (is_array($targetUserIdList)) {
foreach ($targetUserIdList as $userId) {
if ($userId === $this->getUser()->id) continue;
if (in_array($userId, $userIdList)) continue;
$userIdList[] = $userId;
if (in_array($userId, $notifyUserIdList)) continue;
$notifyUserIdList[] = $userId;
}
}
} else if ($targetType === 'teams') {
@@ -130,8 +213,8 @@ class Notifications extends \Espo\Core\Hooks\Base
));
foreach ($targetUserList as $user) {
if ($user->id === $this->getUser()->id) continue;
if (in_array($user->id, $userIdList)) continue;
$userIdList[] = $user->id;
if (in_array($user->id, $notifyUserIdList)) continue;
$notifyUserIdList[] = $user->id;
}
}
}
@@ -148,8 +231,8 @@ class Notifications extends \Espo\Core\Hooks\Base
));
foreach ($targetUserList as $user) {
if ($user->id === $this->getUser()->id) continue;
if (in_array($user->id, $userIdList)) continue;
$userIdList[] = $user->id;
if (in_array($user->id, $notifyUserIdList)) continue;
$notifyUserIdList[] = $user->id;
}
}
}
@@ -162,22 +245,22 @@ class Notifications extends \Espo\Core\Hooks\Base
));
foreach ($targetUserList as $user) {
if ($user->id === $this->getUser()->id) continue;
$userIdList[] = $user->id;
$notifyUserIdList[] = $user->id;
}
}
}
$userIdList = array_unique($userIdList);
$notifyUserIdList = array_unique($notifyUserIdList);
foreach ($userIdList as $i => $userId) {
foreach ($notifyUserIdList as $i => $userId) {
if ($entity->isUserIdNotified($userId)) {
unset($userIdList[$i]);
unset($notifyUserIdList[$i]);
}
}
$userIdList = array_values($userIdList);
$notifyUserIdList = array_values($notifyUserIdList);
if (!empty($userIdList)) {
$this->getNotificationService()->notifyAboutNote($userIdList, $entity);
if (!empty($notifyUserIdList)) {
$this->getNotificationService()->notifyAboutNote($notifyUserIdList, $entity);
}
}
}
@@ -190,4 +273,3 @@ class Notifications extends \Espo\Core\Hooks\Base
return $this->notificationService;
}
}

View File

@@ -45,7 +45,7 @@ class Cleanup extends \Espo\Core\Jobs\Base
protected $cleanupRemovedNotesPeriod = '2 months';
protected $cleanupAttachmentsPeriod = '1 month';
protected $cleanupAttachmentsPeriod = '15 days';
protected $cleanupAttachmentsFromPeriod = '3 months';
@@ -53,6 +53,8 @@ class Cleanup extends \Espo\Core\Jobs\Base
protected $cleanupBackupPeriod = '2 month';
protected $cleanupDeletedRecordsPeriod = '3 months';
public function run()
{
$this->cleanupJobs();
@@ -66,6 +68,7 @@ class Cleanup extends \Espo\Core\Jobs\Base
$this->cleanupAuthLog();
$this->cleanupUpgradeBackups();
$this->cleanupUniqueIds();
$this->cleanupDeletedRecords();
}
protected function cleanupJobs()
@@ -195,7 +198,7 @@ class Cleanup extends \Espo\Core\Jobs\Base
)
),
'createdAt<' => $datetime->format('Y-m-d H:i:s')
))->limit(0, 1000)->find();
))->limit(0, 5000)->find();
foreach ($collection as $e) {
$this->getEntityManager()->removeEntity($e);
@@ -220,7 +223,7 @@ class Cleanup extends \Espo\Core\Jobs\Base
),
'createdAt<' => $datetime->format('Y-m-d H:i:s'),
'createdAt>' => '2017-05-10 00:00:00'
))->limit(0, 1000)->find();
))->limit(0, 5000)->find();
foreach ($collection as $e) {
$this->getEntityManager()->removeEntity($e);
@@ -252,25 +255,30 @@ class Cleanup extends \Espo\Core\Jobs\Base
}
if (!$hasAttachmentField) continue;
$deletedEntityList = $this->getEntityManager()->getRepository($scope)->where([
if (!$this->getEntityManager()->hasRepository($scope)) continue;
$repository = $this->getEntityManager()->getRepository($scope);
if (!method_exists($repository, 'find')) continue;
if (!method_exists($repository, 'where')) continue;
$deletedEntityList = $repository->where([
'deleted' => 1,
'modifiedAt<' => $datetime->format('Y-m-d H:i:s'),
'modifiedAt>' => $datetimeFrom->format('Y-m-d H:i:s'),
])->find(['withDeleted' => true]);
foreach ($deletedEntityList as $deletedEntity) {
$attachmentToRemoveList = $this->getEntityManager()->getRepository('Attachment')->where(array(
'OR' => array(
array(
$attachmentToRemoveList = $this->getEntityManager()->getRepository('Attachment')->where([
'OR' => [
[
'relatedType' => $scope,
'relatedId' => $deletedEntity->id
),
array(
],
[
'parentType' => $scope,
'parentId' => $deletedEntity->id
)
)
))->find();
]
]
])->find();
foreach ($attachmentToRemoveList as $attachmentToRemove) {
$this->getEntityManager()->removeEntity($attachmentToRemove);
@@ -369,4 +377,35 @@ class Cleanup extends \Espo\Core\Jobs\Base
}
}
}
protected function cleanupDeletedRecords()
{
if (!$this->getConfig()->get('cleanupDeletedRecords')) return;
$period = '-' . $this->getConfig()->get('cleanupNotificationsPeriod', $this->cleanupNotificationsPeriod);
$datetime = new \DateTime('-' . $period);
$scopeList = array_keys($this->getMetadata()->get(['scopes']));
foreach ($scopeList as $scope) {
if (!$this->getMetadata()->get(['scopes', $scope, 'entity'])) continue;
if ($scope === 'Attachment') continue;
if (!$this->getMetadata()->get(['entityDefs', $scope, 'fields', 'modifiedAt'])) continue;
if (!$this->getEntityManager()->hasRepository($scope)) continue;
$repository = $this->getEntityManager()->getRepository($scope);
if (!$repository) continue;
if (!method_exists($repository, 'find')) continue;
if (!method_exists($repository, 'where')) continue;
if (!method_exists($repository, 'select')) continue;
if (!method_exists($repository, 'deleteFromDb')) continue;
$deletedEntityList = $repository->select(['id', 'deleted'])->where([
'deleted' => 1,
'modifiedAt<' => $datetime->format('Y-m-d H:i:s')
])->find(['withDeleted' => true]);
foreach ($deletedEntityList as $e) {
if (!$e->get('deleted')) continue;
$repository->deleteFromDb($e->id);
}
}
}
}

View File

@@ -34,6 +34,8 @@ use \Espo\ORM\Entity;
class Meeting extends \Espo\Core\Acl\Base
{
protected $ownerUserIdAttribute = 'usersIds';
public function checkEntityRead(User $user, Entity $entity, $data)
{
if ($this->checkEntity($user, $entity, $data, 'read')) {

View File

@@ -176,7 +176,7 @@ class Ics
"BEGIN:VEVENT\n".
"DTSTART:".$this->dateToCal($this->startDate)."\n".
"DTEND:".$this->dateToCal($this->endDate)."\n".
"SUMMARY:New ".$this->escapeString($this->summary)."\n".
"SUMMARY:".$this->escapeString($this->summary)."\n".
"LOCATION:".$this->escapeString($this->address)."\n".
"ORGANIZER;CN=".$this->escapeString($this->who).":MAILTO:" . $this->escapeString($this->email)."\n".
"DESCRIPTION:".$this->escapeString($this->formatMultiline($this->description))."\n".

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.
************************************************************************/
************************************************************************/
namespace Espo\Modules\Crm\Controllers;
@@ -33,4 +33,11 @@ class CaseObj extends \Espo\Core\Controllers\Record
{
protected $name = 'Case';
public function getActionEmailAddressList($params, $data, $request)
{
if (!$request->get('id')) throw new BadRequest();
if (!$this->getAcl()->checkScope($this->name, 'read')) throw new Forbidden();
return $this->getRecordService()->getEmailAddressList($request->get('id'));
}
}

View File

@@ -113,4 +113,12 @@ class Opportunity extends \Espo\Core\Controllers\Record
return $this->getRecordService()->massConvertCurrency($data->field, $data->targetCurrency, $params, $data->baseCurrency, $data->currencyRates);
}
public function getActionEmailAddressList($params, $data, $request)
{
if (!$request->get('id')) throw new BadRequest();
if (!$this->getAcl()->checkScope($this->name, 'read')) throw new Forbidden();
return $this->getRecordService()->getEmailAddressList($request->get('id'));
}
}

View File

@@ -1,6 +1,5 @@
{
"fields": {
"name": "Name",
"emailAddress": "E-Mail",
"website": "Webseite",
"phoneNumber": "Telefon",
@@ -14,7 +13,8 @@
"campaign": "Kampagne",
"targetLists": "Kontaktlisten",
"targetList": "Kontaktliste",
"originalLead": "Ursprünglicher Interessent"
"originalLead": "Ursprünglicher Interessent",
"contactIsInactive": "Inaktiv"
},
"links": {
"contacts": "Kontakte",
@@ -34,65 +34,63 @@
"options": {
"type": {
"Customer": "Kunde",
"Investor": "Investor",
"Partner": "Partner",
"Reseller": "Wiederverkäufer"
},
"industry": {
"Aerospace": "Luft- und Raumfahrt",
"Agriculture": "Landwirtschaft",
"Advertising": "Werbewirtschaft",
"Apparel & Accessories": "Bekleidungsindustrie",
"Architecture": "Architekten",
"Automotive": "Automobilindustrie",
"Banking": "Bankwesen",
"Biotechnology": "Biotechnologie",
"Building Materials & Equipment": "Baumaterial & -ausstattung",
"Chemical": "Chemieindustrie",
"Construction": "Konstruktion",
"Computer": "Informationstechnologie",
"Defense": "Verteidigung",
"Creative": "Kreative",
"Culture": "Kultur",
"Education": "Bildungswesen",
"Electronics": "Elektronik",
"Electric Power": "Elektroenergie",
"Energy": "Energieerzeuger",
"Entertainment & Leisure": "Freizeit- und Unterhaltungsindustrie",
"Finance": "Finanzsektor",
"Food & Beverage": "Speisen und Getränke",
"Grocery": "Einzelhandel",
"Hospitality": "Gastronomie",
"Healthcare": "Gesundheitswesen",
"Insurance": "Versicherung",
"Legal": "Rechtswesen",
"Manufacturing": "Produktion",
"Mass Media": "Massenmedien",
"Mining": "Bergbau",
"Music": "Musik",
"Marketing": "Marketing",
"Publishing": "Medien",
"Petroleum": "Öl Industrie",
"Real Estate": "Immobilien",
"Retail": "Verkauf",
"Shipping": "Versand und Transport",
"Service": "Service",
"Support": "Service",
"Sports": "Sport",
"Software": "Software",
"Technology": "Technologie",
"Telecommunications": "Telekommunikation",
"Television": "Fernsehen",
"Testing, Inspection & Certification": "Test, Inspektion & Zertifizierung",
"Transportation": "Transportwesen",
"Venture Capital": "Risikokapital",
"Aerospace": "Luft- und Raumfahrt",
"Architecture": "Architekten",
"Construction": "Konstruktion",
"Defense": "Verteidigung",
"Creative": "Kreative",
"Culture": "Kultur",
"Consulting": "Beratung",
"Electric Power": "Elektroenergie",
"Hospitality": "Gastronomie",
"Mass Media": "Massenmedien",
"Mining": "Bergbau",
"Music": "Musik",
"Petroleum": "Öl Industrie",
"Retail": "Verkauf",
"Shipping": "Versand und Transport",
"Support": "Service",
"Testing, Inspection & Certification": "Test, Inspektion & Zertifizierung",
"Wholesale": "Großhandel",
"Water": "Wasserwesen"
"Water": "Wasserwesen",
"Travel": "Reisen"
}
},
"labels": {
"Create Account": "Firma erstellen",
"Copy Billing": "Rechnungsadresse kopieren"
"Copy Billing": "Rechnungsadresse kopieren",
"Set Primary": "Primär setzen"
},
"presetFilters": {
"customers": "Kunden",

View File

@@ -1,6 +1,7 @@
{
"layouts": {
"detailConvert": "Interessent umwandeln",
"listForAccount": "Liste (für Firma)"
"listForAccount": "Liste (für Firma)",
"listForContact": "Liste (für Kontakt)"
}
}

View File

@@ -15,6 +15,8 @@
"current": "aktuell",
"time": "Zeit",
"User List": "Benutzerliste",
"Manage Users": "Benutzer verwalten"
"Manage Users": "Benutzer verwalten",
"View Calendar": "Kalender anzeigen",
"Create Shared View": "Gemeinsame Ansicht erstellen"
}
}

View File

@@ -1,8 +1,6 @@
{
"fields": {
"name": "Name",
"parent": "Bezieht sich auf",
"status": "Status",
"dateStart": "Startdatum",
"dateEnd": "Enddatum",
"direction": "Richtung",

View File

@@ -1,8 +1,6 @@
{
"fields": {
"name": "Name",
"description": "Beschreibung",
"status": "Status",
"type": "Typ",
"startDate": "Startdatum",
"endDate": "Enddatum",
@@ -18,8 +16,13 @@
"leadCreatedCount": "Erstellte Interessenten",
"revenue": "Umsatz",
"revenueConverted": "Umsatz (umgerechnet)",
"budget": "Budget",
"budgetConverted": "Budget (umgerechnet)"
"budgetConverted": "Budget (umgerechnet)",
"contactsTemplate": "Kontaktvorlage",
"leadsTemplate": "Interessentenvorlage",
"accountsTemplate": "Kontenvorlage",
"usersTemplate": "Benutzervorlage",
"mailMergeOnlyWithAddress": "Datensätze ohne gefüllte Adresse überspringen",
"optedInCount": "Opt-In gesetzt"
},
"links": {
"targetLists": "Kontaktlisten",
@@ -30,16 +33,15 @@
"opportunities": "Verkaufschancen",
"campaignLogRecords": "Protokoll",
"massEmails": "Massen E-Mails",
"trackingUrls": "Tracking URLs"
"contactsTemplate": "Kontaktvorlage",
"leadsTemplate": "Interessentenvorlage",
"accountsTemplate": "Kontenvorlage",
"usersTemplate": "Benutzervorlage"
},
"options": {
"type": {
"Email": "E-Mail",
"Web": "Web",
"Television": "Fernsehen",
"Radio": "Radio",
"Newsletter": "Newsletter",
"Mail": "Mail"
"Television": "Fernsehen"
},
"status": {
"Planning": "Planung",
@@ -59,7 +61,9 @@
"Email Templates": "E-Mail Vorlagen",
"Unsubscribe again": "Abmelden",
"Subscribe again": "Anmelden",
"Create Target List": "Interessentenliste erstellen"
"Create Target List": "Interessentenliste erstellen",
"Mail Merge": "Serienbrief",
"Generate Mail Merge PDF": "Serienbrief-PDF generieren"
},
"presetFilters": {
"active": "Aktiv"

View File

@@ -9,12 +9,14 @@
"application": "Applikation",
"queueItem": "Warteschlangeneintrag",
"stringData": "String Daten",
"stringAdditionalData": "String zusätzliche Daten"
"stringAdditionalData": "String zusätzliche Daten",
"isTest": "Ist Test"
},
"links": {
"queueItem": "Warteschlangeneintrag",
"parent": "Bezieht sich auf",
"object": "Objekt"
"object": "Objekt",
"campaign": "Kampagne"
},
"options": {
"action": {
@@ -23,7 +25,8 @@
"Opted Out": "Opt-Out gesetzt",
"Bounced": "Nicht zustellbar",
"Clicked": "Geklickt",
"Lead Created": "Interessent erstellt"
"Lead Created": "Interessent erstellt",
"Opted In": "Opt-In gesetzt"
}
},
"labels": {
@@ -35,6 +38,7 @@
"optedOut": "Opt-Out gesetzt",
"bounced": "Nicht zustellbar",
"clicked": "Geklickt",
"leadCreated": "Interessent erstellt"
"leadCreated": "Interessent erstellt",
"optedIn": "Opt-In gesetzt"
}
}

View File

@@ -1,6 +1,5 @@
{
"fields": {
"url": "URL",
"urlToUse": "Code zum Einfügen anstelle einer URL",
"campaign": "Kampagne"
},

View File

@@ -1,30 +1,27 @@
{
"fields": {
"name": "Name",
"number": "Nummer",
"status": "Status",
"account": "Firma",
"contact": "Kontakt",
"contacts": "Kontakte",
"priority": "Priorität",
"type": "Typ",
"description": "Beschreibung",
"inboundEmail": "Eingehende E-Mail",
"lead": "Interessent",
"attachments": "Anhänge"
"attachments": "Anhänge",
"inboundEmail": "Gruppen E-Mail Konto"
},
"links": {
"inboundEmail": "Eingehende E-Mail",
"account": "Firma",
"contact": "Kontakt (Primär)",
"Contacts": "Kontakte",
"meetings": "Meetings",
"calls": "Anrufe",
"tasks": "Aufgaben",
"emails": "E-Mails",
"articles": "Wissensbasis Artikel",
"lead": "Interessent",
"attachments": "Anhänge"
"attachments": "Anhänge",
"inboundEmail": "Gruppen E-Mail Konto"
},
"options": {
"status": {
@@ -37,14 +34,12 @@
},
"priority": {
"Low": "Niedrig",
"Normal": "Normal",
"High": "Hoch",
"Urgent": "Dringend"
},
"type": {
"Question": "Frage",
"Incident": "Vorfall",
"Problem": "Problem"
"Incident": "Vorfall"
}
},
"labels": {

View File

@@ -1,6 +1,5 @@
{
"fields": {
"name": "Name",
"emailAddress": "E-Mail",
"title": "Funktion",
"accountRole": "Funktion",
@@ -16,7 +15,11 @@
"targetLists": "Kontaktlisten",
"targetList": "Kontaktliste",
"portalUser": "Portal Benutzer",
"originalLead": "Ursprünglicher Interessent"
"originalLead": "Ursprünglicher Interessent",
"acceptanceStatus": "Akzeptanzstatus",
"accountIsInactive": "Inaktives Konto",
"acceptanceStatusMeetings": "Akzeptanzstatus (Meetings)",
"acceptanceStatusCalls": "Akzeptanzstatus (Anrufe)"
},
"links": {
"opportunities": "Verkaufschancen",
@@ -29,7 +32,8 @@
"casesPrimary": "Fälle (Primär)",
"portalUser": "Portal Benutzer",
"originalLead": "Ursprünglicher Interessent",
"documents": "Dokumente"
"documents": "Dokumente",
"tasksPrimary": "Aufgaben (erweitert)"
},
"labels": {
"Create Contact": "Kontakt erstellen"
@@ -44,6 +48,7 @@
},
"presetFilters": {
"portalUsers": "Portal Benutzer",
"notPortalUsers": "Keine Portal Benutzer"
"notPortalUsers": "Keine Portal Benutzer",
"accountActive": "Aktiv"
}
}

View File

@@ -0,0 +1,6 @@
{
"fields": {
"futureDays": "Nächste X Tage",
"useLastStage": "Gruppierung nach der zuletzt erreichten Verkaufsphase"
}
}

View File

@@ -1,11 +1,8 @@
{
"labels": {
"Create Document": "Dokument erstellen",
"Details": "Details"
"Create Document": "Dokument erstellen"
},
"fields": {
"name": "Name",
"status": "Status",
"file": "Datei",
"type": "Typ",
"publishDate": "Veröffentlichungsdatum",
@@ -31,8 +28,6 @@
"type": {
"": "Kein(e)",
"Contract": "Vertrag",
"NDA": "NDA",
"EULA": "EULA",
"License Agreement": "Lizenzvereinbarung"
}
},

View File

@@ -3,6 +3,8 @@
"Create Lead": "Interessent erstellen",
"Create Contact": "Kontakt erstellen",
"Create Task": "Neue Aufgabe",
"Create Case": "Fall erstellen"
"Create Case": "Fall erstellen",
"Add to Contact": "als Kontakt hinzufügen",
"Add to Lead": "als Interessent hinzufügen"
}
}

View File

@@ -1,7 +1,5 @@
{
"fields": {
"name": "Name",
"status": "Status",
"target": "Ziel",
"sentAt": "Sendedatum",
"attemptCount": "Versuche",
@@ -17,7 +15,8 @@
"status": {
"Pending": "Schwebend",
"Sent": "Gesendet",
"Failed": "Fehlgeschlagen"
"Failed": "Fehlgeschlagen",
"Sending": "Wird gesendet"
}
},
"presetFilters": {

View File

@@ -4,7 +4,6 @@
"contacts": "Kontakte",
"opportunities": "Verkaufschancen",
"leads": "Interessenten",
"meetings": "Meetings",
"calls": "Anrufe",
"tasks": "Aufgaben",
"emails": "E-Mails",
@@ -21,7 +20,6 @@
"Lead": "Interessent",
"Target": "Ziel",
"Opportunity": "Verkaufschance",
"Meeting": "Meeting",
"Calendar": "Kalender",
"Call": "Anruf",
"Task": "Aufgabe",
@@ -32,7 +30,6 @@
"TargetList": "Kontaktliste",
"MassEmail": "Massen E-Mails",
"EmailQueueItem": "E-Mail Warteschlangeneintrag",
"CampaignTrackingUrl": "Tracking URL",
"Activities": "Aktivitäten",
"KnowledgeBaseArticle": "Wissensbasis Artikel",
"KnowledgeBaseCategory": "Wissensbasis Kategorie",
@@ -44,7 +41,6 @@
"Lead": "Interessenten",
"Target": "Zielkontakte",
"Opportunity": "Verkaufschancen",
"Meeting": "Meetings",
"Calendar": "Kalender",
"Call": "Anrufe",
"Task": "Aufgaben",
@@ -55,7 +51,6 @@
"TargetList": "Kontaktlisten",
"MassEmail": "Massen E-Mails",
"EmailQueueItem": "E-Mail Warteschlangeneinträge",
"CampaignTrackingUrl": "Tracking URLs",
"Activities": "Aktivitäten",
"KnowledgeBaseArticle": "Wissensbasis",
"KnowledgeBaseCategory": "Wissensbasis Kategorien",
@@ -111,7 +106,6 @@
},
"options": {
"reminderTypes": {
"Popup": "Popup",
"Email": "E-Mail"
}
}

View File

@@ -9,8 +9,6 @@
"Move to Bottom": "zum Ende"
},
"fields": {
"name": "Name",
"status": "Status",
"type": "Typ",
"attachments": "Anhänge",
"publishDate": "Veröffentlichungsdatum",
@@ -38,9 +36,10 @@
"Article": "Artikel"
}
},
"tooltips": {
},
"presetFilters": {
"published": "Publiziert"
},
"tooltips": {
"portals": "Artikel wird nur in bestimmten Portalen verfügbar sein."
}
}

View File

@@ -6,7 +6,6 @@
"convert": "umwandeln"
},
"fields": {
"name": "Name",
"emailAddress": "E-Mail",
"title": "Funktion",
"website": "Webseite",
@@ -14,7 +13,6 @@
"accountName": "Firmenname",
"doNotCall": "Anrufe unerwünscht",
"address": "Adresse",
"status": "Status",
"source": "Quelle",
"opportunityAmount": "Verkaufschance Volumen",
"opportunityAmountConverted": "Verkaufschance Volumen (umgerechnet)",
@@ -25,7 +23,11 @@
"campaign": "Kampagne",
"targetLists": "Kontaktlisten",
"targetList": "Kontaktliste",
"industry": "Industrie"
"industry": "Industrie",
"acceptanceStatus": "Akzeptanzstatus",
"opportunityAmountCurrency": "Währungsbetrag der Verkaufschance",
"acceptanceStatusMeetings": "Akzeptanzstatus (Meetings)",
"acceptanceStatusCalls": "Akzeptanzstatus (Anrufe)"
},
"links": {
"targetLists": "Kontaktlisten",
@@ -51,8 +53,6 @@
"Call": "Anruf",
"Email": "E-Mail",
"Existing Customer": "Bestandskunde",
"Partner": "Partner",
"Public Relations": "Public Relations",
"Web Site": "Webseite",
"Campaign": "Kampagne",
"Other": "Andere"

View File

@@ -1,7 +1,5 @@
{
"fields": {
"name": "Name",
"status": "Status",
"storeSentEmails": "Gesendete E-Mails speichern",
"startAt": "Startdatum",
"fromAddress": "Absenderadresse",
@@ -13,7 +11,8 @@
"inboundEmail": "E-Mail Konto",
"targetLists": "Kontaktlisten",
"excludingTargetLists": "Kontaktlisten ausschließen",
"optOutEntirely": "Vollständiger Opt-Out"
"optOutEntirely": "Vollständiger Opt-Out",
"smtpAccount": "SMTP-Konto"
},
"links": {
"targetLists": "Kontaktlisten",
@@ -35,7 +34,10 @@
},
"labels": {
"Create MassEmail": "Massen E-Mail erstellen",
"Send Test": "Test senden"
"Send Test": "Test senden",
"System SMTP": "SMTP-System",
"system": "System",
"group": "Gruppe"
},
"messages": {
"selectAtLeastOneTarget": "Zumindest ein Ziel auswählen",
@@ -44,7 +46,8 @@
"tooltips": {
"optOutEntirely": "Für E-Mail Adressen von Empfängern, die sich abgemeldet haben, wird ein Opt-out gesetzt. Diese werden keine Massenaussendungen mehr erhalten.",
"targetLists": "Zielkontakte die Nachrichten empfangen sollen",
"excludingTargetLists": "Zielkontakte die keine Nachrichten empfangen sollen"
"excludingTargetLists": "Zielkontakte die keine Nachrichten empfangen sollen",
"storeSentEmails": "E-Mails werden in CRM gespeichert."
},
"presetFilters": {
"actual": "Aktuell",

View File

@@ -1,8 +1,6 @@
{
"fields": {
"name": "Name",
"parent": "Bezieht sich auf",
"status": "Status",
"dateStart": "Startdatum",
"dateEnd": "Enddatum",
"duration": "Dauer",

View File

@@ -1,6 +1,5 @@
{
"fields": {
"name": "Name",
"account": "Firma",
"stage": "Verkaufsphase",
"amount": "Betrag",
@@ -13,7 +12,10 @@
"amountConverted": "Betrag (umgerechnet)",
"amountWeightedConverted": "Betrag gewichtet",
"campaign": "Kampagne",
"originalLead": "Ursprünglicher Interessent"
"originalLead": "Ursprünglicher Interessent",
"amountCurrency": "Währungsbetrag",
"contactRole": "Kontaktrolle",
"lastStage": "Letzte Verkaufsphase"
},
"links": {
"contacts": "Kontakte",
@@ -25,8 +27,6 @@
"stage": {
"Prospecting": "Identifikation",
"Qualification": "Qualifikation",
"Proposal": "Angebot",
"Negotiation": "Verhandlung",
"Needs Analysis": "Bedarfserhebung",
"Value Proposition": "Richtangebot",
"Id. Decision Makers": "Identifikation der Entscheider",
@@ -34,7 +34,9 @@
"Proposal/Price Quote": "Angebot/Kostenvoranschlag",
"Negotiation/Review": "Verhandlung/Überarbeitung",
"Closed Won": "Gewonnen",
"Closed Lost": "Verloren"
"Closed Lost": "Verloren",
"Proposal": "Angebot",
"Negotiation": "Verhandlung"
}
},
"labels": {

View File

@@ -1,13 +1,15 @@
{
"fields": {
"name": "Name",
"description": "Beschreibung",
"entryCount": "Eingabezähler",
"campaigns": "Kampagnen",
"endDate": "Enddatum",
"targetLists": "Kontaktlisten",
"includingActionList": "Inklusive",
"excludingActionList": "Exklusive"
"excludingActionList": "Exklusive",
"optedOutCount": "gesetzte Opt-Out Anzahl",
"targetStatus": "Zielstatus",
"isOptedOut": "Ist Opt-Out gesetzt"
},
"links": {
"accounts": "Firmen",
@@ -19,16 +21,16 @@
"options": {
"type": {
"Email": "E-Mail",
"Web": "Web",
"Television": "Fernsehen",
"Radio": "Radio",
"Newsletter": "Newsletter"
"Television": "Fernsehen"
},
"targetStatus": {
"Opted Out": "Opt-Out gesetzt",
"Listed": "aufgelistet"
}
},
"labels": {
"Create TargetList": "Kontaktliste erstellen",
"Opted Out": "Opt-Out gesetzt",
"Cancel Opt-Out": "Opt-Out zurücksetzen",
"Opt-Out": "Opt-Out"
"Cancel Opt-Out": "Opt-Out zurücksetzen"
}
}

View File

@@ -1,8 +1,6 @@
{
"fields": {
"name": "Name",
"parent": "Bezieht sich auf",
"status": "Status",
"dateStart": "Startdatum",
"dateEnd": "Fällig am",
"dateStartDate": "Startdatum (ganztägig)",
@@ -13,10 +11,13 @@
"account": "Firma",
"dateCompleted": "Datum erledigt",
"attachments": "Anhänge",
"reminders": "Erinnerungen"
"reminders": "Erinnerungen",
"contact": "Kontakt"
},
"links": {
"attachments": "Anhänge"
"attachments": "Anhänge",
"account": "Firma",
"contact": "Kontakt"
},
"options": {
"status": {
@@ -28,7 +29,6 @@
},
"priority": {
"Low": "Niedrig",
"Normal": "Normal",
"High": "Hoch",
"Urgent": "Dringend"
}
@@ -40,8 +40,8 @@
"presetFilters": {
"actual": "Aktuell",
"completed": "Abgeschlossen",
"deferred": "Zurückgestellt",
"todays": "Heutige",
"overdue": "Überfällig"
"overdue": "Überfällig",
"deferred": "Zurückgestellt"
}
}

View File

@@ -1,5 +1,10 @@
{
"links": {
"targetLists": "Kontaktlisten"
},
"fields": {
"acceptanceStatus": "Akzeptanzstatus",
"acceptanceStatusMeetings": "Akzeptanzstatus (Meetings)",
"acceptanceStatusCalls": "Akzeptanzstatus (Anrufe)"
}
}

View File

@@ -13,6 +13,7 @@
"clickedCount": "Clicked",
"optedOutCount": "Opted Out",
"bouncedCount": "Bounced",
"optedInCount": "Opted In",
"hardBouncedCount": "Hard Bounced",
"softBouncedCount": "Soft Bounced",
"leadCreatedCount": "Leads Created",

View File

@@ -25,7 +25,8 @@
"Opted Out": "Opted Out",
"Bounced": "Bounced",
"Clicked": "Clicked",
"Lead Created": "Lead Created"
"Lead Created": "Lead Created",
"Opted In": "Opted In"
}
},
"labels": {
@@ -35,6 +36,7 @@
"sent": "Sent",
"opened": "Opened",
"optedOut": "Opted Out",
"optedIn": "Opted In",
"bounced": "Bounced",
"clicked": "Clicked",
"leadCreated": "Lead Created"

View File

@@ -12,8 +12,8 @@
"type": "Tipo",
"contactRole": "Título",
"campaign": "Campaña",
"targetLists": "Listas de Objetivos",
"targetList": "Lista de Objetivos",
"targetLists": "Listas de Intereses",
"targetList": "Lista de Intereses",
"originalLead": "Referencia Original",
"contactIsInactive": "Inactivo"
},
@@ -22,11 +22,11 @@
"opportunities": "Oportunidades",
"cases": "Casos",
"documents": "Documentos",
"meetingsPrimary": "Juntas (ampliado)",
"meetingsPrimary": "Presentaciones (ampliado)",
"callsPrimary": "Llamadas (ampliado)",
"tasksPrimary": "Tareas (ampliado)",
"emailsPrimary": "Correos (ampliado)",
"targetLists": "Listas de Objetivos",
"targetLists": "Listas de Intereses",
"campaignLogRecords": "Historial de Campañas",
"campaign": "Campaña",
"portalUsers": "Usuarios del Portal",

View File

@@ -6,8 +6,8 @@
"type": "Tipo",
"startDate": "Fecha de Inicio",
"endDate": "Fecha de Fin",
"targetLists": "Lista de Objetivos",
"excludingTargetLists": "Listas de Objetivos Excluidas",
"targetLists": "Lista de Intereses",
"excludingTargetLists": "Listas de Intereses Excluidas",
"sentCount": "Enviado",
"openedCount": "Abierto",
"clickedCount": "Leídos",
@@ -19,18 +19,27 @@
"revenue": "Ingresos",
"revenueConverted": "ingresos (convertido)",
"budget": "Presupuesto",
"budgetConverted": "Presupuesto (convertido)"
"budgetConverted": "Presupuesto (convertido)",
"contactsTemplate": "Formato de Contactos",
"leadsTemplate": "Formato de Referencias",
"accountsTemplate": "Formato de Cuentas",
"usersTemplate": "Formato de Usuarios",
"mailMergeOnlyWithAddress": "Saltar registros sin dirección capturada"
},
"links": {
"targetLists": "Listas de Objetivos",
"excludingTargetLists": "Listas de Objetivos Excluidas",
"targetLists": "Listas de Intereses",
"excludingTargetLists": "Listas de Intereses Excluidas",
"accounts": "Cuentas",
"contacts": "Contactos",
"leads": "Referencias",
"opportunities": "Oportunidades",
"campaignLogRecords": "Historial",
"massEmails": "Correos Masivos",
"trackingUrls": "Seguimiento a URLs"
"trackingUrls": "Seguimiento a URLs",
"contactsTemplate": "Formato de Contactos",
"leadsTemplate": "Formato de Referencias",
"accountsTemplate": "Formato de Cuentas",
"usersTemplate": "Formato de Usuarios"
},
"options": {
"type": {
@@ -48,7 +57,7 @@
},
"labels": {
"Create Campaign": "Crear Campaña",
"Target Lists": "Listas de Objetivos",
"Target Lists": "Listas de Intereses",
"Statistics": "Estadísticas",
"hard": "duro",
"soft": "suave",
@@ -57,7 +66,9 @@
"Email Templates": "Correo Modelo",
"Unsubscribe again": "Cancelar otra vez la suscripción",
"Subscribe again": "Volverse a suscribir",
"Create Target List": "Crear Lista de Obejtivos"
"Create Target List": "Crear Lista de Obejtivos",
"Mail Merge": "Generar Correos",
"Generate Mail Merge PDF": "Generar PDF para Correos"
},
"presetFilters": {
"active": "Activo"
@@ -67,7 +78,7 @@
"subscribedAgain": "Usted se ha vuelto a suscribir."
},
"tooltips": {
"targetLists": "Los objetivos que deben recibir los mensajes.",
"excludingTargetLists": "Los objetivos que no deben recibir mensajes."
"targetLists": "Intereses que deben recibir mensajes.",
"excludingTargetLists": "Los intereses que no deben recibir mensajes."
}
}

View File

@@ -4,7 +4,7 @@
"actionDate": "Fecha",
"data": "Datos",
"campaign": "Campaña",
"parent": "Objetivo",
"parent": "Interés",
"object": "Objeto",
"application": "Aplicacion",
"queueItem": "Item de la Lista",

View File

@@ -17,7 +17,7 @@
"account": "Cuenta",
"contact": "Contacto (Primario)",
"Contacts": "Contactos",
"meetings": "Juntas",
"meetings": "Presentaciones",
"calls": "Llamadas",
"tasks": "Tareas",
"emails": "Correos",

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