Compare commits

...

135 Commits

Author SHA1 Message Date
Yuri Kuznetsov
d9f3b83a41 v 2019-12-19 14:56:47 +02:00
Yuri Kuznetsov
96b2f9e1a6 fix integrations 2019-12-19 14:56:21 +02:00
Yuri Kuznetsov
500e8ca89b Merge branch 'hotfix/5.7.10' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.10 2019-12-18 12:15:37 +02:00
Taras Machyshyn
f7fb3f50c3 Сompatibility fix 2019-12-18 12:15:10 +02:00
Yuri Kuznetsov
4763cc80c3 fix div 2019-12-18 10:52:26 +02:00
Yuri Kuznetsov
0d744c5e7f fix email 2019-12-18 10:11:37 +02:00
Yuri Kuznetsov
629f410204 Merge branch 'hotfix/5.7.10' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.10 2019-12-17 13:07:38 +02:00
Yuri Kuznetsov
34b3f52276 oauth debug 2019-12-17 10:33:25 +02:00
Taras Machyshyn
4b508e3295 Installer changes 2019-12-16 17:16:28 +02:00
Taras Machyshyn
dfdde2ef83 File permissions changes 2019-12-16 16:28:48 +02:00
Yuri Kuznetsov
88cac5c6c0 css fix 2019-12-16 10:44:47 +02:00
Yuri Kuznetsov
5e877dd374 grunt fixes 2019-12-13 11:57:45 +02:00
Yuri Kuznetsov
bcf54d9fb0 diff fix and npm dep 2019-12-13 11:01:14 +02:00
Yuri Kuznetsov
5c42133e7b grunt zip 2019-12-12 17:18:00 +02:00
Yuri Kuznetsov
93c9afe338 readme fix 2019-12-12 16:42:41 +02:00
Yuri Kuznetsov
9b12c5c9c1 grunt improvements 2019-12-12 16:39:54 +02:00
Yuri Kuznetsov
1bd4468f57 grunt composer install 2019-12-12 15:48:50 +02:00
Yuri Kuznetsov
e183bb4fa6 diff vendor 2019-12-12 15:21:35 +02:00
Yuri Kuznetsov
a79ece4a3d Merge branch 'hotfix/5.7.10' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.10 2019-12-12 12:25:33 +02:00
Taras Machyshyn
93bfa83b46 Installer: improvements 2019-12-12 12:24:28 +02:00
Yuri Kuznetsov
97fa72042e currency translation 2019-12-12 12:21:29 +02:00
Yuri Kuznetsov
d5069487ae add currencies 2019-12-12 12:01:14 +02:00
Yuri Kuznetsov
0a5f852d04 diff zip 2019-12-12 11:47:08 +02:00
Yuri Kuznetsov
dcc2fd0382 diff fix 2019-12-11 17:10:04 +02:00
Yuri Kuznetsov
bf7f0d5cbc settings get rid of required 2019-12-11 16:29:15 +02:00
Yuri Kuznetsov
a337aef8a7 settings validation 2019-12-11 16:15:28 +02:00
Yuri Kuznetsov
30dec3be52 orm intefrace changes and cleanup 2019-12-11 15:27:57 +02:00
Yuri Kuznetsov
604aca50c8 array valiation 2019-12-11 15:08:12 +02:00
Yuri Kuznetsov
949dc96e7b diff imrovements 2019-12-11 13:52:07 +02:00
Yuri Kuznetsov
e4c2d4eae1 v 2019-12-10 11:50:08 +02:00
Yuri Kuznetsov
66bdb7b547 email template placeholders ui improvement 2019-12-10 11:49:28 +02:00
Yuri Kuznetsov
9fccc3c1f3 fix css 2019-12-10 10:26:47 +02:00
Yuri Kuznetsov
0effcab5c2 Merge branch 'hotfix/5.7.9' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.9 2019-12-09 15:52:36 +02:00
Taras Machyshyn
1f5ab5f45c Installer: currency fixes 2019-12-09 15:44:51 +02:00
Yuri Kuznetsov
4789fc15a5 Merge branch 'hotfix/5.7.9' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.9 2019-12-09 15:32:27 +02:00
Taras Machyshyn
6f2270eea7 Installer: currency fixes 2019-12-09 15:31:56 +02:00
Yuri Kuznetsov
a0f43c8f8f fix modal resize 2019-12-09 12:15:46 +02:00
Yuri Kuznetsov
b36b5857c1 diff improvement 2019-12-06 16:14:04 +02:00
Yuri Kuznetsov
c6de42fd80 v 2019-12-06 15:16:18 +02:00
Yuri Kuznetsov
5e7f61b46d Merge branch 'hotfix/5.7.8' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.8 2019-12-06 12:38:42 +02:00
Taras Machyshyn
c5e41faa0f Upgrade fixes 2019-12-06 12:38:28 +02:00
Yuri Kuznetsov
465377c67b email update fix 2019-12-06 12:09:24 +02:00
Yuri Kuznetsov
f41cc85cba Merge branch 'hotfix/5.7.8' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.8 2019-12-05 16:26:33 +02:00
Taras Machyshyn
fdf08624cb Added cli installation 2019-12-05 16:26:11 +02:00
Yuri Kuznetsov
5c89f4f389 fix 2019-12-05 15:34:24 +02:00
Yuri Kuznetsov
acce8f1b1f fix 2019-12-05 15:30:07 +02:00
Yuri Kuznetsov
f1382e802e fix 2019-12-05 15:17:12 +02:00
Yuri Kuznetsov
f64e59b0de Merge branch 'hotfix/5.7.8' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.8 2019-12-05 15:08:03 +02:00
Yuri Kuznetsov
5b41abe76f auth refactoring 2019-12-05 15:07:48 +02:00
Taras Machyshyn
f286d2277e Fixed typo 2019-12-05 10:45:47 +02:00
Yuri Kuznetsov
b38c983e73 wysiwyg copy image issue 2019-12-04 15:51:06 +02:00
Yuri Kuznetsov
a70d133471 fix sumRelated 2019-12-04 15:06:53 +02:00
Yuri Kuznetsov
bc3d488bfe message fix 2019-12-04 13:25:47 +02:00
Yuri Kuznetsov
e9578d8d44 fix smtp message 2019-12-04 13:21:33 +02:00
Yuri Kuznetsov
4c38d96730 fix users teams positions 2019-12-04 13:01:45 +02:00
Yuri Kuznetsov
b962d1572a fix users positions 2019-12-04 12:58:56 +02:00
Yuri Kuznetsov
3f1f686bd1 fix layout acl check 2019-12-04 10:30:32 +02:00
Yuri Kuznetsov
611bbb8fb2 fallback dashboard overflow hidden 2019-11-28 12:06:01 +02:00
Yuri Kuznetsov
b89b39cd44 fix enum 2019-11-27 15:04:18 +02:00
Yuri Kuznetsov
9a6e2d6578 enum: options w/ quotes not working 2019-11-27 13:10:41 +02:00
Yuri Kuznetsov
9f63c00f5c portal user list view access 2019-11-26 13:29:56 +02:00
Yuri Kuznetsov
f11b9c0bbc user access to portal users 2019-11-26 13:13:10 +02:00
Yuri Kuznetsov
6ea109f712 app error 2019-11-26 12:57:44 +02:00
Yuri Kuznetsov
1c3dc61264 fix stream input disapear 2019-11-21 11:23:53 +02:00
Yuri Kuznetsov
c415ce677d stream noteRelate 2019-11-21 11:09:57 +02:00
Yuri Kuznetsov
927a580dce stream follow check acl 2019-11-21 10:37:45 +02:00
Yuri Kuznetsov
8e3b44c2e1 email import: support multiple references 2019-11-18 13:55:06 +02:00
Yuri Kuznetsov
4048b7207b lang fix 2019-11-18 13:09:02 +02:00
Yuri Kuznetsov
a87231552b case distribution fixes 2019-11-18 13:00:38 +02:00
Yuri Kuznetsov
a234f503a1 list with categories no data if text filter 2019-11-13 12:07:52 +02:00
Yuri Kuznetsov
55be5b12b2 formula record relate 2019-11-13 10:43:51 +02:00
Yuri Kuznetsov
1f400649f5 Merge branch 'hotfix/5.7.8' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.8 2019-11-12 15:23:24 +02:00
Yuri Kuznetsov
e6f65440f2 add more currencies 2019-11-12 14:59:05 +02:00
Taras Machyshyn
def8455d78 Improved Integration tests 2019-11-12 14:58:25 +02:00
Yuri Kuznetsov
1ad2144432 currency list in app metadata 2019-11-12 14:55:30 +02:00
Yuri Kuznetsov
c2828e3273 v 2019-11-12 14:46:36 +02:00
Yuri Kuznetsov
b1f272215a email index skip mysql 5.7 2019-11-12 11:08:46 +02:00
Taras Machyshyn
032199d9c7 Save database type and version while rebuild 2019-11-12 10:56:12 +02:00
Yuri Kuznetsov
cce78ac258 fix metadata and language acl 2019-11-11 10:22:22 +02:00
Yuri Kuznetsov
67074f2b0a dashlet options button 2019-11-06 15:29:00 +02:00
Yuri Kuznetsov
ea3189ebc3 metadata & language acl dependencies 2019-11-04 14:04:10 +02:00
Yuri Kuznetsov
83e7e0b517 fix link multiple with role acl 2019-11-04 13:36:12 +02:00
Yuri Kuznetsov
2c1fed8adf fix email header 2019-11-04 10:33:42 +02:00
Yuri Kuznetsov
45e6f3bb34 array field: display as list param 2019-11-01 16:46:27 +02:00
Yuri Kuznetsov
f53708c38e remove no joins 2019-11-01 14:34:57 +02:00
Yuri Kuznetsov
d1aa3f721d no full text threshold for emails 2019-11-01 13:16:08 +02:00
Yuri Kuznetsov
0afe715d9a full text search threshold 2019-11-01 13:08:40 +02:00
Yuri Kuznetsov
9cbe31300a lock user table before checking duplicate 2019-11-01 11:59:33 +02:00
Yuri Kuznetsov
dde139f0a1 full text changes 2019-10-31 16:53:26 +02:00
Yuri Kuznetsov
5fb8b2c71c orm match test 2019-10-31 15:56:21 +02:00
Yuri Kuznetsov
63c07a1f17 fulltext search improvements 2019-10-31 15:42:08 +02:00
Yuri Kuznetsov
a338bbca59 integration language 2019-10-31 13:38:34 +02:00
Yuri Kuznetsov
157bf87d40 ouathcallback message 2019-10-31 12:49:53 +02:00
Yuri Kuznetsov
911a9f80de bool search change 2019-10-29 15:56:19 +02:00
Yuri Kuznetsov
c9bbbeb2c4 fix history email list 2019-10-29 12:03:52 +02:00
Yuri Kuznetsov
57803c0828 v 2019-10-28 12:24:03 +02:00
Yuri Kuznetsov
3d8be61fbf amend 2019-10-28 12:05:33 +02:00
Yuri Kuznetsov
690e87a3fb select manager error if order by non existing field 2019-10-28 12:05:00 +02:00
Yuri Kuznetsov
cd45f07e54 fix account description 2019-10-24 14:34:46 +03:00
Yuri Kuznetsov
a35f9625af fix email reply id 2019-10-24 13:33:07 +03:00
Yuri Kuznetsov
f5245ef3eb email import message id fix 2019-10-24 13:06:11 +03:00
yuri
4ad9ef770b pdf: not latin filename support 2019-10-23 11:05:26 +03:00
yuri
acbc74d858 fix entity plural label 2019-10-22 15:42:41 +03:00
yuri
8da3350523 base plus emails link 2019-10-22 15:40:29 +03:00
yuri
496879dd79 pdf link support 2019-10-22 15:30:49 +03:00
yuri
84cda80fd8 forbid base entity type name 2019-10-22 13:13:13 +03:00
yuri
50a81b1247 date picker disable keyboard navigation 2019-10-22 12:56:57 +03:00
yuri
f0f225f349 propagate remove event for stored main view 2019-10-22 12:25:40 +03:00
yuri
d49fff1289 cleanup 2019-10-22 11:44:26 +03:00
yuri
abaa1b302b email filters from to 2019-10-22 11:38:44 +03:00
yuri
0eaa7825ea email filters layout change 2019-10-22 10:40:34 +03:00
yuri
1028b7c5b3 v 2019-10-22 10:32:36 +03:00
yuri
f319e5219d Merge branch 'hotfix/5.7.6' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.6 2019-10-22 10:05:32 +03:00
Taras Machyshyn
0daae5ced5 Integration tests changes 2019-10-21 16:38:46 +03:00
Taras Machyshyn
abee63d269 FieldManager fix 2019-10-21 16:38:15 +03:00
yuri
be33d90986 all of filter 2019-10-21 15:44:41 +03:00
yuri
3a7cac824d formula comments 2019-10-21 13:44:16 +03:00
yuri
7d616b075f fix formula sumRelated 2019-10-21 12:22:05 +03:00
yuri
b718765138 system requirements tpl fix 2019-10-21 11:46:27 +03:00
yuri
3104be7c99 link multiple default 2019-10-21 10:46:29 +03:00
yuri
a8d182b4cd Merge branch 'hotfix/5.7.5' into stable 2019-10-18 16:14:48 +03:00
yuri
3bfc99c88b entity array clone 2019-10-18 14:46:18 +03:00
yuri
ac3300a5cf fix password labels 2019-10-18 12:58:49 +03:00
yuri
636587a22c Merge branch 'hotfix/5.7.5' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.5 2019-10-18 12:48:48 +03:00
Taras Machyshyn
4ed2c71dfd Integration tests fix 2019-10-18 12:48:30 +03:00
yuri
723229d8e6 Merge branch 'hotfix/5.7.5' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.7.5 2019-10-18 12:12:31 +03:00
yuri
080ab488fa fix json object save 2019-10-18 11:11:55 +03:00
Taras Machyshyn
fd32696c5a Integration tests optimization 2019-10-18 10:40:28 +03:00
Taras Machyshyn
9a56858f12 Label changes 2019-10-18 10:40:20 +03:00
Taras Machyshyn
5923757810 Database indexes changes 2019-10-18 10:40:07 +03:00
yuri
62948a7740 v 2019-10-17 17:21:22 +03:00
yuri
821d57ce00 fix stream 2019-10-17 17:20:47 +03:00
yuri
efd0505ead fix 2019-10-17 16:59:37 +03:00
yuri
91c55965a7 fix 2019-10-17 16:43:16 +03:00
yuri
974305e152 v 2019-10-17 16:42:57 +03:00
158 changed files with 4141 additions and 1689 deletions

View File

@@ -18,6 +18,15 @@
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
************************************************************************/
/**
* * `grunt` - full build
* * `grunt dev` - build only items needed for development
* * `grunt offline` - skips *composer install*
* * `grant release` - full build plus upgrade packages`
* * `grant tests` - build and run tests
*/
module.exports = function (grunt) {
var jsFilesToMinify = [
@@ -83,6 +92,10 @@ module.exports = function (grunt) {
}
var fs = require('fs');
var cp = require('child_process');
var path = require('path');
var currentPath = path.dirname(fs.realpathSync(__filename));
var themeList = [];
fs.readdirSync('application/Espo/Resources/metadata/themes').forEach(function (file) {
@@ -108,8 +121,10 @@ module.exports = function (grunt) {
lessData[theme] = o;
});
var pkg = grunt.file.readJSON('package.json');
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
pkg: pkg,
mkdir: {
tmp: {
@@ -125,8 +140,13 @@ module.exports = function (grunt) {
clean: {
start: ['build/EspoCRM-*'],
final: ['build/tmp'],
release: ['build/EspoCRM-' + pkg.version],
beforeFinal: {
src: ['build/tmp/custom/Espo/Custom/*', '!build/tmp/custom/Espo/Custom/.htaccess', 'build/tmp/install/config.php']
src: [
'build/tmp/custom/Espo/Custom/*',
'!build/tmp/custom/Espo/Custom/.htaccess',
'build/tmp/install/config.php',
]
}
},
less: lessData,
@@ -146,6 +166,9 @@ module.exports = function (grunt) {
})
},
copy: {
options: {
mode: true,
},
frontendFolders: {
expand: true,
cwd: 'client',
@@ -207,9 +230,6 @@ module.exports = function (grunt) {
},
},
chmod: {
options: {
mode: '755'
},
php: {
options: {
mode: '644'
@@ -238,7 +258,21 @@ module.exports = function (grunt) {
'build/EspoCRM-<%= pkg.version %>/api/v1/portal-access',
'build/EspoCRM-<%= pkg.version %>',
]
}
},
foldersWritable: {
options: {
mode: '775'
},
src: [
'build/EspoCRM-<%= pkg.version %>/data',
'build/EspoCRM-<%= pkg.version %>/custom',
'build/EspoCRM-<%= pkg.version %>/custom/Espo',
'build/EspoCRM-<%= pkg.version %>/custom/Espo/Custom',
'build/EspoCRM-<%= pkg.version %>/client/custom',
'build/EspoCRM-<%= pkg.version %>/client/modules',
'build/EspoCRM-<%= pkg.version %>/application/Espo/Modules',
]
},
},
replace: {
version: {
@@ -258,17 +292,53 @@ module.exports = function (grunt) {
]
}
},
compress: {
final: {
options: {
archive: 'build/EspoCRM-<%= pkg.version %>.zip',
mode: 'zip'
},
src: ['**'],
cwd: 'build/EspoCRM-<%= pkg.version %>',
dest: 'EspoCRM-<%= pkg.version %>'
}
}
});
grunt.registerTask("chmod-folders", function() {
cp.execSync("find . -type d -exec chmod 755 {} + ", {stdio: 'ignore', cwd: 'build/EspoCRM-' + pkg.version});
});
grunt.registerTask("composer", function() {
cp.execSync("composer install", {stdio: 'ignore'});
});
grunt.registerTask("upgrade", function() {
cp.execSync("node diff --all --vendor", {stdio: 'inherit'});
});
grunt.registerTask("unit-tests", function() {
cp.execSync("phpunit --bootstrap=vendor/autoload.php tests/unit", {stdio: 'inherit'});
});
grunt.registerTask("integration-tests", function() {
cp.execSync("phpunit --bootstrap=vendor/autoload.php tests/integration", {stdio: 'inherit'});
});
grunt.registerTask("zip", function() {
var fs = require('fs');
var resolve = this.async();
var folder = 'EspoCRM-' + pkg.version;
var zipPath = 'build/' + folder +'.zip';
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
var archiver = require('archiver');
var archive = archiver('zip');
archive.on('error', function (err) {
grunt.fail.warn(err);
});
var zipOutput = fs.createWriteStream(zipPath);
zipOutput.on('close', function () {
console.log("EspoCRM package has been built.");
resolve();
});
archive.directory(currentPath + '/build/' + folder, folder).pipe(zipOutput);
archive.finalize();
});
grunt.loadNpmTasks('grunt-contrib-clean');
@@ -278,10 +348,9 @@ module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-replace');
grunt.loadNpmTasks('grunt-contrib-compress');
grunt.loadNpmTasks('grunt-chmod');
grunt.registerTask('default', [
grunt.registerTask('offline', [
'clean:start',
'mkdir:tmp',
'less',
@@ -293,7 +362,33 @@ module.exports = function (grunt) {
'replace',
'clean:beforeFinal',
'copy:final',
'chmod',
'clean:final'
'chmod-folders',
'chmod:php',
'chmod:folders',
'chmod:foldersWritable',
'clean:final',
]);
grunt.registerTask('default', [
'composer',
'offline',
]);
grunt.registerTask('release', [
'default',
'upgrade',
'zip',
'clean:release',
]);
grunt.registerTask('tests', [
'default',
'unit-tests',
'integration-tests',
]);
grunt.registerTask('dev', [
'composer',
'less',
]);
};

View File

@@ -25,7 +25,7 @@ Create an issue [here](https://github.com/espocrm/espocrm/issues) or post on our
[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)
### Getting started (for developers)
1. Clone repository to your local computer.
2. Change to the project's root directory.
@@ -48,15 +48,33 @@ You need to have nodejs and Grunt CLI installed.
The build will be created in the `build` directory.
### How to contribute
Upgrade packages can be built with `grunt upgrade`.
### How to contribute (for developers)
Before we can merge your pull request you need to accept our CLA [here](https://github.com/espocrm/cla). It's very simple to do.
Branches:
* hotfix/* upcoming maintenance release; fixes should be pushed to this branch;
* master develop branch; new features should be pushed to this branch;
* stable last stable release.
* *hotfix/** upcoming maintenance release; fixes should be pushed to this branch;
* *master* develop branch; new features should be pushed to this branch;
* *stable* last stable release.
### Running tests (for developers)
You need to have *phpunit* installed.
Unit tests:
```
phpunit --bootstrap=vendor/autoload.php tests/unit
```
Integration tests:
```
phpunit --bootstrap=vendor/autoload.php tests/integration
```
### How to make a translation

View File

@@ -39,6 +39,16 @@ class User extends \Espo\Core\Acl\Base
return $user->id === $entity->id;
}
public function checkEntityRead(EntityUser $user, Entity $entity, $data)
{
if (!$user->isAdmin() && $entity->isPortal()) {
if ($this->getAclManager()->get($user, 'portalPermission') === 'yes') {
return true;
}
}
return $this->checkEntity($user, $entity, $data, 'read');
}
public function checkEntityCreate(EntityUser $user, Entity $entity, $data)
{
if (!$user->isAdmin()) {

View File

@@ -75,7 +75,7 @@ class Email extends \Espo\Core\Controllers\Record
throw new NotFound();
}
if (!$this->getUser()->isAdmin()) {
if ($emailAccount->get('assigniedUserId') !== $this->getUser()->id) {
if ($emailAccount->get('assignedUserId') !== $this->getUser()->id) {
throw new Forbidden();
}
}

View File

@@ -91,8 +91,10 @@ class DataManager
*/
public function rebuildDatabase($entityList = null)
{
$schema = $this->getContainer()->get('schema');
try {
$result = $this->getContainer()->get('schema')->rebuild($entityList);
$result = $schema->rebuild($entityList);
} catch (\Exception $e) {
$result = false;
$GLOBALS['log']->error('Fault to rebuild database schema'.'. Details: '.$e->getMessage());
@@ -102,6 +104,18 @@ class DataManager
throw new Exceptions\Error("Error while rebuilding database. See log file for details.");
}
$config = $this->getContainer()->get('config');
$databaseType = strtolower($schema->getDatabaseHelper()->getDatabaseType());
if (!$config->get('actualDatabaseType') || $config->get('actualDatabaseType') != $databaseType) {
$config->set('actualDatabaseType', $databaseType);
}
$databaseVersion = $schema->getDatabaseHelper()->getDatabaseVersion();
if (!$config->get('actualDatabaseVersion') || $config->get('actualDatabaseVersion') != $databaseVersion) {
$config->set('actualDatabaseVersion', $databaseVersion);
}
$this->updateCacheTimestamp();
return $result;
@@ -180,7 +194,7 @@ class DataManager
public function updateCacheTimestamp()
{
$this->getContainer()->get('config')->updateCacheTimestamp();
$this->getContainer()->get('config')->save();
$this->getContainer()->get('config')->save(); /* correct rebuildDatabase() method when remove this line */
return true;
}

View File

@@ -105,19 +105,23 @@ abstract class OAuth2Abstract implements IClient
public function getAccessTokenFromAuthorizationCode($code)
{
$r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_AUTHORIZATION_CODE, array(
$r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_AUTHORIZATION_CODE, [
'code' => $code,
'redirect_uri' => $this->getParam('redirectUri')
));
'redirect_uri' => $this->getParam('redirectUri'),
]);
if ($r['code'] == 200) {
$data = array();
$data = [];
if (!empty($r['result'])) {
$data['accessToken'] = $r['result']['access_token'];
$data['tokenType'] = $r['result']['token_type'];
$data['refreshToken'] = $r['result']['refresh_token'];
} else {
$GLOBALS['log']->debug("OAuth getAccessTokenFromAuthorizationCode; Response: " . json_encode($r));
}
return $data;
} else {
$GLOBALS['log']->debug("OAuth getAccessTokenFromAuthorizationCode; Response: " . json_encode($r));
}
return null;
}

View File

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

View File

@@ -44,6 +44,13 @@ class ArrayType extends BaseType
return true;
}
public function checkArray(\Espo\ORM\Entity $entity, string $field, $validationValue, $data) : bool
{
if (!$entity->has($field) || $entity->get($field) === null) return true;
return is_array($entity->get($field));
}
protected function isNotEmpty(\Espo\ORM\Entity $entity, $field)
{
if (!$entity->has($field) || $entity->get($field) === null) return false;

View File

@@ -84,6 +84,7 @@ class SumRelatedType extends \Espo\Core\Formula\Functions\Base
$foreignSelectManager = $this->getInjection('selectManagerFactory')->create($foreignEntityType);
$foreignLink = $entity->getRelationParam($link, 'foreign');
$foreignLinkAlias = $foreignLink . 'SumRelated';
if (empty($foreignLink)) {
throw new Error("No foreign link for link {$link}.");
@@ -95,29 +96,49 @@ class SumRelatedType extends \Espo\Core\Formula\Functions\Base
$foreignSelectManager->applyFilter($filter, $selectParams);
}
$selectParams['select'] = [[$foreignLink . '.id', 'foreignId'], 'SUM:' . $field];
$selectParams['select'] = [[$foreignLinkAlias . '.id', 'foreignId'], 'SUM:' . $field];
if ($entity->getRelationType($link) === 'hasChildren') {
$foreignSelectManager->addJoin([
$entity->getEntityType(),
$foreignLink,
$foreignLinkAlias,
[
$foreignLink . '.id:' => $foreignLink . 'Id',
$foreignLinkAlias . '.id:' => $foreignLink . 'Id',
'deleted' => false,
$foreignLink . '.id!=' => null,
$foreignLinkAlias . '.id!=' => null,
]
], $selectParams);
$selectParams['whereClause'][] = [$foreignLink . 'Type' => $entity->getEntityType()];
} else {
$foreignSelectManager->addJoin($foreignLink, $selectParams);
$foreignSelectManager->addJoin([$foreignLink, $foreignLinkAlias], $selectParams);
}
$selectParams['groupBy'] = [$foreignLink . '.id'];
if (!empty($selectParams['distinct'])) {
$sqSelectParams = $selectParams;
$selectParams['whereClause'][] = [
$foreignLink . '.id' => $entity->id
];
$sqSelectParams['whereClause'][] = [
$foreignLinkAlias . '.id' => $entity->id
];
$sqSelectParams['select'] = ['id'];
unset($sqSelectParams['distinct']);
unset($sqSelectParams['orderBy']);
unset($sqSelectParams['order']);
$selectParams['whereClause'][] = [
'id=s' => [
'entityType' => $foreignEntityType,
'selectParams' => $sqSelectParams,
]
];
} else {
$selectParams['whereClause'][] = [
$foreignLinkAlias . '.id' => $entity->id
];
}
$selectParams['groupBy'] = [$foreignLinkAlias . '.id'];
$entityManager->getRepository($foreignEntityType)->handleSelectParams($selectParams);

View File

@@ -0,0 +1,69 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Exceptions\Error;
class RelateType extends \Espo\Core\Formula\Functions\Base
{
protected function init()
{
$this->addDependency('entityManager');
}
public function process(\StdClass $item)
{
$args = $item->value ?? [];
if (count($args) < 4) throw new Error("Formula: record\\relate: Not enough arguments.");
$entityType = $this->evaluate($args[0]);
$id = $this->evaluate($args[1]);
$link = $this->evaluate($item->value[2]);
$foreignId = $this->evaluate($item->value[3]);
if (!$entityType) throw new Error("Formula record\\relate: Empty entityType.");
if (!$id) return null;
if (!$link) throw new Error("Formula record\\relate: Empty link.");
if (!$foreignId) return null;
$em = $this->getInjection('entityManager');
if (!$em->hasRepository($entityType)) throw new Error("Formula: record\\relate: Repository does not exist.");
$entity = $em->getEntity($entityType, $id);
if (!$entity) return null;
if ($em->getRepository($entityType)->isRelated($entity, $link, $foreignId))
return true;
return $em->getRepository($entityType)->relate($entity, $link, $foreignId);
}
}

View File

@@ -0,0 +1,69 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Exceptions\Error;
class UnrelateType extends \Espo\Core\Formula\Functions\Base
{
protected function init()
{
$this->addDependency('entityManager');
}
public function process(\StdClass $item)
{
$args = $item->value ?? [];
if (count($args) < 4) throw new Error("Formula: record\\unrelate: Not enough arguments.");
$entityType = $this->evaluate($args[0]);
$id = $this->evaluate($args[1]);
$link = $this->evaluate($item->value[2]);
$foreignId = $this->evaluate($item->value[3]);
if (!$entityType) throw new Error("Formula record\\unrelate: Empty entityType.");
if (!$id) return null;
if (!$link) throw new Error("Formula record\\unrelate: Empty link.");
if (!$foreignId) return null;
$em = $this->getInjection('entityManager');
if (!$em->hasRepository($entityType)) throw new Error("Formula: record\\unrelate: Repository does not exist.");
$entity = $em->getEntity($entityType, $id);
if (!$entity) return null;
if (!$em->getRepository($entityType)->isRelated($entity, $link, $foreignId))
return true;
return $em->getRepository($entityType)->unrelate($entity, $link, $foreignId);
}
}

View File

@@ -115,10 +115,12 @@ class Parser
$braceCounter = 0;
for ($i = 0; $i < strlen($string); $i++) {
$isStringStart = false;
if ($string[$i] === "'" && ($i === 0 || $string[$i - 1] !== "\\")) {
if (!$isString) {
$isString = true;
$isSingleQuote = true;
$isStringStart = true;
} else {
if ($isSingleQuote) {
$isString = false;
@@ -127,6 +129,7 @@ class Parser
} else if ($string[$i] === "\"" && ($i === 0 || $string[$i - 1] !== "\\")) {
if (!$isString) {
$isString = true;
$isStringStart = true;
$isSingleQuote = false;
} else {
if (!$isSingleQuote) {
@@ -137,6 +140,8 @@ class Parser
if ($isString) {
if ($string[$i] === '(' || $string[$i] === ')') {
$modifiedString[$i] = '_';
} else if (!$isStringStart) {
$modifiedString[$i] = ' ';
}
} else {
if ($string[$i] === '(') {
@@ -176,6 +181,16 @@ class Parser
$this->processStrings($expression, $modifiedExpression, $splitterIndexList, true);
$this->stripComments($expression, $modifiedExpression);
foreach ($splitterIndexList as $i => $index) {
if ($expression[$index] !== ';') {
unset($splitterIndexList[$i]);
}
}
$splitterIndexList = array_values($splitterIndexList);
$expressionOutOfBraceList = [];
for ($i = 0; $i < strlen($modifiedExpression); $i++) {
@@ -402,6 +417,43 @@ class Parser
}
}
protected function stripComments(&$expression, &$modifiedExpression)
{
$commentIndexStart = null;
for ($i = 0; $i < strlen($modifiedExpression); $i++) {
if (is_null($commentIndexStart)) {
if ($modifiedExpression[$i] === '/' && $i < strlen($modifiedExpression) - 1 && $modifiedExpression[$i + 1] === '/') {
$commentIndexStart = $i;
}
} else {
if ($modifiedExpression[$i] === "\n" || $i === strlen($modifiedExpression) - 1) {
for ($j = $commentIndexStart; $j <= $i; $j++) {
$modifiedExpression[$j] = ' ';
$expression[$j] = ' ';
}
$commentIndexStart = null;
}
}
}
for ($i = 0; $i < strlen($modifiedExpression) - 1; $i++) {
if (is_null($commentIndexStart)) {
if ($modifiedExpression[$i] === '/' && $modifiedExpression[$i + 1] === '*') {
$commentIndexStart = $i;
}
} else {
if ($modifiedExpression[$i] === '*' && $modifiedExpression[$i + 1] === '/') {
for ($j = $commentIndexStart; $j <= $i + 1; $j++) {
$modifiedExpression[$j] = ' ';
$expression[$j] = ' ';
}
$commentIndexStart = null;
}
}
}
}
protected function parseArgumentListFromFunctionContent($functionContent)
{
$functionContent = trim($functionContent);

View File

@@ -35,26 +35,33 @@ use Espo\Core\Exceptions\Error;
use Espo\Core\Utils\File\Manager as FileManager;
use Espo\Core\Utils\DateTime;
use Espo\Core\Utils\NumberUtil;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Language;
use Espo\Core\Utils\Metadata;
use Espo\ORM\EntityManager;
require('vendor/zordius/lightncandy/src/lightncandy.php');
class Htmlizer
{
protected $fileManager;
protected $dateTime;
protected $config;
protected $acl;
protected $entityManager;
protected $metadata;
protected $language;
public function __construct(FileManager $fileManager, DateTime $dateTime, NumberUtil $number, $acl = null, $entityManager = null, $metadata = null, $language = null)
public function __construct(
FileManager $fileManager,
DateTime $dateTime,
NumberUtil $number,
$acl = null,
?EntityManager $entityManager = null,
?Metadata $metadata = null,
?Language $language = null,
?Config $config = null
)
{
$this->fileManager = $fileManager;
$this->dateTime = $dateTime;
@@ -63,6 +70,7 @@ class Htmlizer
$this->entityManager = $entityManager;
$this->metadata = $metadata;
$this->language = $language;
$this->config = $config;
}
protected function getAcl()
@@ -92,7 +100,7 @@ class Htmlizer
return $value;
}
protected function getDataFromEntity(Entity $entity, $skipLinks = false, $level = 0)
protected function getDataFromEntity(Entity $entity, $skipLinks = false, $level = 0, ?string $template = null)
{
$data = $entity->toArray();
@@ -118,15 +126,32 @@ class Htmlizer
if (!$skipLinks && $level === 0) {
foreach ($relationList as $relation) {
if (!$entity->hasLinkMultipleField($relation)) continue;
$collection = null;
$collection = $entity->getLinkMultipleCollection($relation);
$data[$relation] = $collection;
if ($entity->hasLinkMultipleField($relation)) {
$toLoad = true;
$collection = $entity->getLinkCollection($relation);
} else {
if (
$template && $entity->getRelationType($relation, ['hasMany', 'manyMany', 'hasChildren']) &&
mb_stripos($template, '{{#each '.$relation.'}}') !== false
) {
$limit = 100;
if ($this->config) {
$limit = $this->config->get('htmlizerLinkLimit') ?? $limit;
}
$collection = $entity->getLinkCollection($relation, ['limit' => $limit]);
}
}
if ($collection) {
$data[$relation] = $collection;
}
}
}
foreach ($data as $key => $value) {
if ($value instanceof \Espo\ORM\EntityCollection) {
if ($value instanceof \Espo\ORM\ICollection) {
$skipAttributeList[] = $key;
$collection = $value;
$list = [];
@@ -280,7 +305,7 @@ class Htmlizer
}
}
return;
}
},
],
'hbhelpers' => [
'ifEqual' => function () {
@@ -331,7 +356,7 @@ class Htmlizer
$this->fileManager->removeFile($fileName);
}
$data = $this->getDataFromEntity($entity, $skipLinks);
$data = $this->getDataFromEntity($entity, $skipLinks, 0, $template);
if (!array_key_exists('today', $data)) {
$data['today'] = $this->dateTime->getTodayString();

View File

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

View File

@@ -212,56 +212,70 @@ class Importer
if ($parser->checkMessageAttribute($message, 'in-Reply-To') && $parser->getMessageAttribute($message, 'in-Reply-To')) {
$arr = explode(' ', $parser->getMessageAttribute($message, 'in-Reply-To'));
$inReplyTo = $arr[0];
$replied = $this->getEntityManager()->getRepository('Email')->where(array(
'messageId' => $inReplyTo
))->findOne();
if ($replied) {
$email->set('repliedId', $replied->id);
$repliedTeamIdList = $replied->getLinkMultipleIdList('teams');
foreach ($repliedTeamIdList as $repliedTeamId) {
$email->addLinkMultipleId('teams', $repliedTeamId);
if ($inReplyTo) {
if ($inReplyTo[0] !== '<') $inReplyTo = '<' . $inReplyTo . '>';
$replied = $this->getEntityManager()->getRepository('Email')->where(array(
'messageId' => $inReplyTo
))->findOne();
if ($replied) {
$email->set('repliedId', $replied->id);
$repliedTeamIdList = $replied->getLinkMultipleIdList('teams');
foreach ($repliedTeamIdList as $repliedTeamId) {
$email->addLinkMultipleId('teams', $repliedTeamId);
}
}
}
}
if ($parser->checkMessageAttribute($message, 'references') && $parser->getMessageAttribute($message, 'references')) {
$arr = explode(' ', $parser->getMessageAttribute($message, 'references'));
$reference = $arr[0];
$reference = str_replace(array('/', '@'), " ", trim($reference, '<>'));
$parentType = $parentId = null;
$emailSent = PHP_INT_MAX;
$number = null;
$n = sscanf($reference, '%s %s %d %d espo', $parentType, $parentId, $emailSent, $number);
if ($n != 4) {
$n = sscanf($reference, '%s %s %d %d espo-system', $parentType, $parentId, $emailSent, $number);
$references = $parser->getMessageAttribute($message, 'references');
$delimiter = ' ';
if (strpos($references, '>,')) {
$delimiter = ',';
}
if ($n == 4 && $emailSent < time()) {
if (!empty($parentType) && !empty($parentId)) {
if ($parentType == 'Lead') {
$parent = $this->getEntityManager()->getEntity('Lead', $parentId);
if ($parent && $parent->get('status') == 'Converted') {
if ($parent->get('createdAccountId')) {
$account = $this->getEntityManager()->getEntity('Account', $parent->get('createdAccountId'));
if ($account) {
$parentType = 'Account';
$parentId = $account->id;
}
} else {
if ($this->getConfig()->get('b2cMode')) {
if ($parent->get('createdContactId')) {
$contact = $this->getEntityManager()->getEntity('Contact', $parent->get('createdContactId'));
if ($contact) {
$parentType = 'Contact';
$parentId = $contact->id;
$arr = explode($delimiter, $references);
foreach ($arr as $reference) {
$reference = trim($reference);
$reference = str_replace(['/', '@'], " ", trim($reference, '<>'));
$parentType = $parentId = null;
$emailSent = PHP_INT_MAX;
$number = null;
$n = sscanf($reference, '%s %s %d %d espo', $parentType, $parentId, $emailSent, $number);
if ($n != 4) {
$n = sscanf($reference, '%s %s %d %d espo-system', $parentType, $parentId, $emailSent, $number);
}
if ($n == 4 && $emailSent < time()) {
if (!empty($parentType) && !empty($parentId)) {
if ($parentType == 'Lead') {
$parent = $this->getEntityManager()->getEntity('Lead', $parentId);
if ($parent && $parent->get('status') == 'Converted') {
if ($parent->get('createdAccountId')) {
$account = $this->getEntityManager()->getEntity('Account', $parent->get('createdAccountId'));
if ($account) {
$parentType = 'Account';
$parentId = $account->id;
}
} else {
if ($this->getConfig()->get('b2cMode')) {
if ($parent->get('createdContactId')) {
$contact = $this->getEntityManager()->getEntity('Contact', $parent->get('createdContactId'));
if ($contact) {
$parentType = 'Contact';
$parentId = $contact->id;
}
}
}
}
}
}
$email->set('parentType', $parentType);
$email->set('parentId', $parentId);
$parentFound = true;
}
$email->set('parentType', $parentType);
$email->set('parentId', $parentId);
$parentFound = true;
}
}
}

View File

@@ -91,7 +91,13 @@ class MailMimeParser
public function getMessageMessageId($message)
{
return $this->getMessageAttribute($message, 'Message-ID');
$messageId = $this->getMessageAttribute($message, 'Message-ID');
if ($messageId && strlen($messageId) && $messageId[0] !== '<') {
$messageId = '<' . $messageId . '>';
}
return $messageId;
}
public function getAddressNameMap($message)

View File

@@ -74,22 +74,33 @@ class Entity extends \Espo\ORM\Entity
}
}
public function getLinkMultipleCollection($field)
public function getLinkCollection(string $link, ?array $selectParams = null)
{
if (!$this->hasLinkMultipleField($field)) return;
if (!$selectParams) $selectParams = [];
$defs = $this->getRelationSelectParams($field);
$relSelectParams = $this->getRelationSelectParams($link);
$columnAttribute = $field . 'Columns';
$selectParams = array_merge($selectParams, $relSelectParams);
$selectParams['returnSthCollection'] = true;
$columnAttribute = $link . 'Columns';
if ($this->hasAttribute($columnAttribute) && $this->getAttributeParam($columnAttribute, 'columns')) {
$defs['additionalColumns'] = $this->getAttributeParam($columnAttribute, 'columns');
$selectParams['additionalColumns'] = $this->getAttributeParam($columnAttribute, 'columns');
}
$collection = $this->get($field, $defs);
$collection = $this->get($link, $selectParams);
return $collection;
}
public function getLinkMultipleCollection(string $link, ?array $selectParams = null)
{
if (!$this->hasLinkMultipleField($link)) return;
return $this->getLinkCollection($link, $selectParams);
}
protected function getRelationSelectParams($link)
{
$field = $link;

View File

@@ -240,4 +240,33 @@ class Tcpdf extends \TCPDF
$this->_out($out);
}
public function Output($name = 'doc.pdf', $dest = 'I')
{
if ($dest === 'I' && !$this->sign && php_sapi_name() != 'cli') {
if ($this->state < 3) {
$this->Close();
}
$name = preg_replace('/[\s]+/', '_', $name);
$name = \Espo\Core\Utils\Util::sanitizeFileName($name);
if (ob_get_contents()) {
$this->Error('Some data has already been output, can\'t send PDF file');
}
header('Content-Type: application/pdf');
if (headers_sent()) {
$this->Error('Some data has already been output to browser, can\'t send PDF file');
}
header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0, max-age=1');
header('Pragma: public');
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Content-Disposition: inline; filename="'.$name.'"');
TCPDF_STATIC::sendOutputData($this->getBuffer(), $this->bufferlen);
return '';
}
return parent::Output($name, $dest);
}
}

View File

@@ -70,12 +70,19 @@ class Base
protected $selectAttributesDependancyMap = [];
protected $fullTextSearchForceOrderOnlyByRelevance = false;
protected $fullTextOrderType = self::FT_ORDER_COMBINTED;
protected $fullTextRelevanceThreshold = null;
const FT_ORDER_COMBINTED = 0;
const FT_ORDER_RELEVANCE = 1;
const FT_ORDER_ORIGINAL = 3;
const MIN_LENGTH_FOR_CONTENT_SEARCH = 4;
const MIN_LENGTH_FOR_FULL_TEXT_SEARCH = 4;
protected $fullTextOrderRelevanceDivider = 5;
protected $fullTextSearchDataCacheHash = [];
public function __construct(EntityManager $entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Metadata $metadata, Config $config, FieldManagerUtil $fieldManagerUtil, InjectableFactory $injectableFactory)
@@ -186,6 +193,12 @@ class Base
$result['orderBy'] = 'LIST:' . $sortBy . ':' . implode(',', $list);
return;
}
} else {
if (strpos($sortBy, '.') === false && strpos($sortBy, ':') === false) {
if (!$this->getSeed()->hasAttribute($sortBy)) {
throw new Error("Order by non-existing field '{$sortBy}'.");
}
}
}
}
if (!$desc) {
@@ -1732,6 +1745,7 @@ class Base
case 'arrayNoneOf':
case 'arrayIsEmpty':
case 'arrayIsNotEmpty':
case 'arrayAllOf':
if (!$result) break;
$arrayValueAlias = 'arrayFilter' . strval(rand(10000, 99999));
@@ -1763,6 +1777,8 @@ class Base
$arrayValueAlias . '.attribute' => $arrayAttribute
]], $result);
$part[$arrayValueAlias . '.value'] = $value;
$this->setDistinct(true, $result);
} else if ($type === 'arrayNoneOf') {
if (is_null($value) || !$value && !is_array($value)) break;
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
@@ -1772,6 +1788,8 @@ class Base
$arrayValueAlias . '.value=' => $value
]], $result);
$part[$arrayValueAlias . '.id'] = null;
$this->setDistinct(true, $result);
} else if ($type === 'arrayIsEmpty') {
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
$arrayValueAlias . '.entityId:' => $idPart,
@@ -1779,6 +1797,8 @@ class Base
$arrayValueAlias . '.attribute' => $arrayAttribute
]], $result);
$part[$arrayValueAlias . '.id'] = null;
$this->setDistinct(true, $result);
} else if ($type === 'arrayIsNotEmpty') {
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
$arrayValueAlias . '.entityId:' => $idPart,
@@ -1786,9 +1806,31 @@ class Base
$arrayValueAlias . '.attribute' => $arrayAttribute
]], $result);
$part[$arrayValueAlias . '.id!='] = null;
}
$this->setDistinct(true, $result);
$this->setDistinct(true, $result);
} else if ($type === 'arrayAllOf') {
if (is_null($value) || !$value && !is_array($value)) break;
if (!is_array($value)) {
$value = [$value];
}
foreach ($value as $arrayValue) {
$part[] = [
$idPart .'=s' => [
'entityType' => 'ArrayValue',
'selectParams' => [
'select' => ['entityId'],
'whereClause' => [
'value' => $arrayValue,
'attribute' => $arrayAttribute,
'entityType' => $arrayEntityType,
],
],
]
];
}
}
}
}
@@ -2137,7 +2179,7 @@ class Base
$fullTextSearchColumnSanitizedList[$i] = $query->sanitize($query->toDb($field));
}
$where = $function . ':' . implode(',', $fullTextSearchColumnSanitizedList) . ':' . $textFilter;
$where = $function . ':(' . implode(',', $fullTextSearchColumnSanitizedList) . ',' . $textFilter . ')';
$result = [
'where' => $where,
@@ -2204,23 +2246,35 @@ class Base
$fullTextSearchFieldList = [];
if ($fullTextSearchData) {
$fullTextGroup[] = $fullTextSearchData['where'];
if ($this->fullTextRelevanceThreshold) {
$fullTextGroup[] = [$fullTextSearchData['where'] . '>=' => $this->fullTextRelevanceThreshold];
} else {
$fullTextGroup[] = $fullTextSearchData['where'];
}
$fullTextSearchFieldList = $fullTextSearchData['fieldList'];
if (isset($result['orderBy']) && !$this->fullTextSearchForceOrderOnlyByRelevance) {
if (is_string($result['orderBy'])) {
$result['orderBy'] = [
[$fullTextSearchData['where'], 'desc'],
[$result['orderBy'], $result['order'] ?? 'asc'],
];
}
} else {
$result['orderBy'] = [[$fullTextSearchData['where'], 'desc']];
$relevanceExpression = $fullTextSearchData['where'];
if (!isset($result['orderBy']) || $this->fullTextOrderType === self::FT_ORDER_RELEVANCE) {
$result['orderBy'] = [[$relevanceExpression, 'desc']];
$result['order'] = null;
} else {
if ($this->fullTextOrderType === self::FT_ORDER_COMBINTED) {
$relevanceExpression =
'ROUND:(DIV:(' . $fullTextSearchData['where'] . ','.$this->fullTextOrderRelevanceDivider.'))';
if (is_string($result['orderBy'])) {
$result['orderBy'] = [
[$relevanceExpression, 'desc'],
[$result['orderBy'], $result['order'] ?? 'asc'],
];
}
}
}
$result['additionalSelect'] = $result['additionalSelect'] ?? [];
$result['additionalSelect'][] = $fullTextSearchData['where'];
$result['additionalSelect'][] = $relevanceExpression;
$result['hasFullTextSearch'] = true;
}

View File

@@ -72,6 +72,12 @@
"entity": "Task",
"foreign": "parent",
"layoutRelationshipsDisabled": true
},
"emails": {
"type": "hasChildren",
"entity": "Email",
"foreign": "parent",
"layoutRelationshipsDisabled": true
}
},
"collection": {

View File

@@ -245,7 +245,7 @@ abstract class Base
$isInRange = false;
try {
$isInRange = Semver::satisfies($currentVersion, $version);
} catch (\Exception $e) {
} catch (\Throwable $e) {
$GLOBALS['log']->error('SemVer: Version identification error: '.$e->getMessage().'.');
}
@@ -313,7 +313,7 @@ abstract class Base
try {
$script->run($this->getContainer(), $this->scriptParams);
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->throwErrorAndRemovePackage($e->getMessage());
}
}
@@ -494,7 +494,7 @@ abstract class Base
{
try {
$res = $this->getFileManager()->copy($sourcePath, $destPath, $recursively, $fileList, $copyOnlyFiles);
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->throwErrorAndRemovePackage($e->getMessage());
}
@@ -706,7 +706,7 @@ abstract class Base
{
try {
return $this->getContainer()->get('dataManager')->rebuild();
} catch (\Exception $e) {
} catch (\Throwable $e) {
$GLOBALS['log']->error('Database rebuild failure, details: '.$e->getMessage().'.');
}

View File

@@ -171,7 +171,7 @@ class Install extends \Espo\Core\Upgrades\Actions\Base
if (!isset($data['skipSystemRebuild']) || !$data['skipSystemRebuild']) {
if (!$this->systemRebuild()) {
$this->throwErrorAndRemovePackage('Error occurred while EspoCRM rebuild.');
$this->throwErrorAndRemovePackage('Error occurred while EspoCRM rebuild. Please see the log for more detail.');
}
}

View File

@@ -76,7 +76,7 @@ class Uninstall extends \Espo\Core\Upgrades\Actions\Base
if (!isset($data['skipSystemRebuild']) || !$data['skipSystemRebuild']) {
if (!$this->systemRebuild()) {
$this->throwErrorAndRemovePackage('Error occurred while EspoCRM rebuild.');
$this->throwErrorAndRemovePackage('Error occurred while EspoCRM rebuild. Please see the log for more detail.');
}
}

View File

@@ -77,20 +77,7 @@ class Auth
protected function getAuthenticationImpl(string $method) : \Espo\Core\Utils\Authentication\Base
{
$className = $this->getMetadata()->get([
'authenticationMethods', $method, 'implementationClassName'
]);
if (!$className) {
$sanitizedName = preg_replace('/[^a-zA-Z0-9]+/', '', $method);
$className = "\\Espo\\Custom\\Core\\Utils\\Authentication\\" . $sanitizedName;
if (!class_exists($className)) {
$className = "\\Espo\\Core\\Utils\\Authentication\\" . $sanitizedName;
}
}
return new $className($this->getConfig(), $this->getEntityManager(), $this, $this->getContainer());
return $this->getContainer()->get('authenticationFactory')->create($method);
}
protected function get2FAImpl(string $method) : \Espo\Core\Utils\Authentication\TwoFA\Base

View File

@@ -29,11 +29,13 @@
namespace Espo\Core\Utils\Authentication;
use \Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Error;
use Espo\Entities\AuthToken;
class ApiKey extends Base
{
public function login($username, $password, $authToken = null, $params = [], $request)
public function login(string $username, $password, ?AuthToken $authToken = null, array $params = [], $request = null)
{
$apiKey = $username;
@@ -41,7 +43,7 @@ class ApiKey extends Base
'whereClause' => [
'type' => 'api',
'apiKey' => $apiKey,
'authMethod' => 'ApiKey'
'authMethod' => 'ApiKey',
]
]);

View File

@@ -35,19 +35,16 @@ use \Espo\Core\Utils\Auth;
abstract class Base
{
private $config;
protected $config;
private $entityManager;
private $auth;
protected $entityManager;
private $passwordHash;
public function __construct(Config $config, EntityManager $entityManager, Auth $auth)
public function __construct(Config $config, EntityManager $entityManager)
{
$this->config = $config;
$this->entityManager = $entityManager;
$this->auth = $auth;
}
protected function getConfig()
@@ -60,11 +57,6 @@ abstract class Base
return $this->entityManager;
}
protected function getAuth()
{
return $this->auth;
}
protected function getPasswordHash()
{
if (!isset($this->passwordHash)) {
@@ -74,4 +66,3 @@ abstract class Base
return $this->passwordHash;
}
}

View File

@@ -29,11 +29,13 @@
namespace Espo\Core\Utils\Authentication;
use \Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Error;
use Espo\Entities\AuthToken;
class Espo extends Base
{
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null, $params = [], $request)
public function login(string $username, $password, ?AuthToken $authToken = null, array $params = [], $request = null)
{
if (!$password) return;
@@ -47,7 +49,7 @@ class Espo extends Base
'whereClause' => [
'userName' => $username,
'password' => $hash,
'type!=' => ['api', 'system']
'type!=' => ['api', 'system'],
]
]);

View File

@@ -29,11 +29,13 @@
namespace Espo\Core\Utils\Authentication;
use \Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Error;
use Espo\Entities\AuthToken;
class Hmac extends Base
{
public function login($username, $password, $authToken = null, $params = [], $request)
public function login(string $username, $password, ?AuthToken $authToken = null, array $params = [], $request)
{
$apiKey = $username;
$hash = $password;

View File

@@ -33,6 +33,7 @@ use Espo\Core\Exceptions\Error;
use Espo\Core\Utils\Config;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Utils\Auth;
use Espo\Entities\AuthToken;
class LDAP extends Espo
{
@@ -74,9 +75,9 @@ class LDAP extends Espo
'portalRolesIds' => 'portalUserRolesIds',
);
public function __construct(Config $config, EntityManager $entityManager, Auth $auth)
public function __construct(Config $config, EntityManager $entityManager)
{
parent::__construct($config, $entityManager, $auth);
parent::__construct($config, $entityManager);
$this->utils = new LDAP\Utils($config);
}
@@ -101,16 +102,7 @@ class LDAP extends Espo
return $this->ldapClient;
}
/**
* LDAP login
*
* @param string $username
* @param string $password
* @param \Espo\Entities\AuthToken $authToken
*
* @return \Espo\Entities\User | null
*/
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null, $params = [], $request)
public function login(string $username, $password, ?AuthToken $authToken = null, array $params = [], $request = null)
{
if (!$password) return;
@@ -279,9 +271,17 @@ class LDAP extends Espo
$data[$fieldName] = $fieldValue;
}
$this->getAuth()->useNoAuth();
$entityManager = $this->entityManager;
$user = $this->getEntityManager()->getEntity('User');
$systemUser = $entityManager->getRepository('User')->get('system');
if (!$systemUser) {
throw new Error("System user is not found");
}
$systemUser->set('isAdmin', true);
$systemUser->set('ipAddress', $_SERVER['REMOTE_ADDR']);
$entityManager->setUser($systemUser);
$user = $entityManager->getEntity('User');
$user->set($data);
$this->getEntityManager()->saveEntity($user);

View File

@@ -0,0 +1,70 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2019 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Authentication\Utils;
use Espo\Core\Container;
class AuthenticationFactory
{
protected $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function create(string $method) : \Espo\Core\Utils\Authentication\Base
{
$metadata = $this->container->get('metadata');
$className = $metadata->get(['authenticationMethods', $method, 'implementationClassName']);
$dependencyList = $metadata->get(['authenticationMethods', $method, 'dependencyList']) ?? [];
if (!$className) {
$sanitizedName = preg_replace('/[^a-zA-Z0-9]+/', '', $method);
$className = "\\Espo\\Custom\\Core\\Utils\\Authentication\\" . $sanitizedName;
if (!class_exists($className)) {
$className = "\\Espo\\Core\\Utils\\Authentication\\" . $sanitizedName;
}
}
$config = $this->container->get('config');
$entityManager = $this->container->get('entityManager');
$impl = new $className($config, $entityManager);
foreach ($dependencyList as $item) {
$impl->inject($item, $this->container->get($item));
}
return $impl;
}
}

View File

@@ -87,7 +87,7 @@ class Autoload
return $this->data;
}
return Utill::getValueByKey($this->data, $key, $returns);
return Util::getValueByKey($this->data, $key, $returns);
}
public function getAll()

View File

@@ -30,6 +30,9 @@
namespace Espo\Core\Utils\Database\DBAL\Schema;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Schema\ColumnDiff;
class Comparator extends \Doctrine\DBAL\Schema\Comparator
{
@@ -136,4 +139,130 @@ class Comparator extends \Doctrine\DBAL\Schema\Comparator
return $changedProperties;
}
public function diffTable(Table $table1, Table $table2)
{
$changes = 0;
$tableDifferences = new TableDiff($table1->getName());
$tableDifferences->fromTable = $table1;
$table1Columns = $table1->getColumns();
$table2Columns = $table2->getColumns();
/* See if all the fields in table 1 exist in table 2 */
foreach ( $table2Columns as $columnName => $column ) {
if ( !$table1->hasColumn($columnName) ) {
$tableDifferences->addedColumns[$columnName] = $column;
$changes++;
}
}
/* See if there are any removed fields in table 2 */
foreach ( $table1Columns as $columnName => $column ) {
if ( !$table2->hasColumn($columnName) ) {
$tableDifferences->removedColumns[$columnName] = $column;
$changes++;
}
}
foreach ( $table1Columns as $columnName => $column ) {
if ( $table2->hasColumn($columnName) ) {
$changedProperties = $this->diffColumn( $column, $table2->getColumn($columnName) );
if (count($changedProperties) ) {
$columnDiff = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties);
$columnDiff->fromColumn = $column;
$tableDifferences->changedColumns[$column->getName()] = $columnDiff;
$changes++;
}
}
}
$this->detectColumnRenamings($tableDifferences);
$table1Indexes = $table1->getIndexes();
$table2Indexes = $table2->getIndexes();
foreach ($table2Indexes as $index2Name => $index2Definition) {
foreach ($table1Indexes as $index1Name => $index1Definition) {
if ($this->diffIndex($index1Definition, $index2Definition) === false) {
unset($table1Indexes[$index1Name]);
unset($table2Indexes[$index2Name]);
} else {
if ($index1Name == $index2Name) {
/*espo*/ if (isset($table2Indexes[$index2Name])) { /*espo*/
$tableDifferences->changedIndexes[$index2Name] = $table2Indexes[$index2Name];
unset($table1Indexes[$index1Name]);
unset($table2Indexes[$index2Name]);
$changes++;
/*espo*/ } /*espo*/
}
}
}
}
foreach ($table1Indexes as $index1Name => $index1Definition) {
$tableDifferences->removedIndexes[$index1Name] = $index1Definition;
$changes++;
}
foreach ($table2Indexes as $index2Name => $index2Definition) {
$tableDifferences->addedIndexes[$index2Name] = $index2Definition;
$changes++;
}
$fromFkeys = $table1->getForeignKeys();
$toFkeys = $table2->getForeignKeys();
foreach ($fromFkeys as $key1 => $constraint1) {
foreach ($toFkeys as $key2 => $constraint2) {
if($this->diffForeignKey($constraint1, $constraint2) === false) {
unset($fromFkeys[$key1]);
unset($toFkeys[$key2]);
} else {
if (strtolower($constraint1->getName()) == strtolower($constraint2->getName())) {
$tableDifferences->changedForeignKeys[] = $constraint2;
$changes++;
unset($fromFkeys[$key1]);
unset($toFkeys[$key2]);
}
}
}
}
foreach ($fromFkeys as $constraint1) {
$tableDifferences->removedForeignKeys[] = $constraint1;
$changes++;
}
foreach ($toFkeys as $constraint2) {
$tableDifferences->addedForeignKeys[] = $constraint2;
$changes++;
}
return $changes ? $tableDifferences : false;
}
private function detectColumnRenamings(TableDiff $tableDifferences)
{
$renameCandidates = array();
foreach ($tableDifferences->addedColumns as $addedColumnName => $addedColumn) {
foreach ($tableDifferences->removedColumns as $removedColumn) {
if (count($this->diffColumn($addedColumn, $removedColumn)) == 0) {
$renameCandidates[$addedColumn->getName()][] = array($removedColumn, $addedColumn, $addedColumnName);
}
}
}
foreach ($renameCandidates as $candidateColumns) {
if (count($candidateColumns) == 1) {
list($removedColumn, $addedColumn) = $candidateColumns[0];
$removedColumnName = strtolower($removedColumn->getName());
$addedColumnName = strtolower($addedColumn->getName());
if ( ! isset($tableDifferences->renamedColumns[$removedColumnName])) {
$tableDifferences->renamedColumns[$removedColumnName] = $addedColumn;
unset($tableDifferences->addedColumns[$addedColumnName]);
unset($tableDifferences->removedColumns[$removedColumnName]);
}
}
}
}
}

View File

@@ -332,19 +332,6 @@ class Converter
$table->setPrimaryKey(array("id"));
//add unique indexes
if (!empty($relationParams['conditions'])) {
foreach ($relationParams['conditions'] as $fieldName => $fieldParams) {
$uniqueIndex[] = Util::toUnderScore($fieldName);
}
}
if (!empty($uniqueIndex)) {
$uniqueIndexName = implode('_', $uniqueIndex);
$table->addUniqueIndex($uniqueIndex, SchemaUtils::generateIndexName($uniqueIndexName, 'unique'));
}
//END: add unique indexes
//add defined indexes
if (!empty($relationParams['indexes'])) {
$normalizedIndexes = SchemaUtils::getIndexList([
@@ -358,6 +345,19 @@ class Converter
}
//END: add defined indexes
//add unique indexes
if (!empty($relationParams['conditions'])) {
foreach ($relationParams['conditions'] as $fieldName => $fieldParams) {
$uniqueIndex[] = Util::toUnderScore($fieldName);
}
}
if (!empty($uniqueIndex)) {
$uniqueIndexName = implode('_', $uniqueIndex);
$table->addUniqueIndex($uniqueIndex, SchemaUtils::generateIndexName($uniqueIndexName, 'unique'));
}
//END: add unique indexes
return $table;
}

View File

@@ -54,7 +54,18 @@ class EntityManager
private $linkForbiddenNameList = ['posts', 'stream', 'subscription', 'followers', 'action', 'null', 'false', 'true'];
private $forbiddenEntityTypeNameList = ['Common', 'PortalUser', 'ApiUser', 'Timeline', 'About', 'Admin', 'Null', 'False', 'True'];
private $forbiddenEntityTypeNameList = [
'Common',
'PortalUser',
'ApiUser',
'Timeline',
'About',
'Admin',
'Null',
'False',
'True',
'Base',
];
public function __construct(Metadata $metadata, Language $language, File\Manager $fileManager, Config $config, Container $container = null)
{

View File

@@ -427,7 +427,11 @@ class FieldManager
protected function getFieldDefs($scope, $name, $default = null)
{
return $this->getMetadata()->get('entityDefs'.'.'.$scope.'.fields.'.$name, $default);
$defs = $this->getMetadata()->getObjects(['entityDefs', $scope, 'fields', $name], $default);
if (is_object($defs)) {
return get_object_vars($defs);
}
return $defs;
}
protected function getCustomFieldDefs($scope, $name, $default = null)
@@ -532,7 +536,7 @@ class FieldManager
}
$actualCustomFieldDefs = $this->getCustomFieldDefs($scope, $name, []);
$actualFieldDefs = $this->getFieldDefs($scope, $name, []);
$actualFieldDefs = $this->getFieldDefs($scope, $name, (object) []);
$permittedParamList = array_keys($params);
$filteredFieldDefs = !empty($actualCustomFieldDefs) ? $actualCustomFieldDefs : [];

View File

@@ -44,7 +44,6 @@ class Manager
if (isset($config)) {
$params = array(
'defaultPermissions' => $config->get('defaultPermissions'),
'permissionMap' => $config->get('permissionMap'),
);
}
@@ -381,11 +380,11 @@ class Manager
{
$fullPath = $this->concatPaths($path);
if (file_exists($fullPath) && is_dir($path)) {
if (file_exists($fullPath) && is_dir($fullPath)) {
return true;
}
$defaultPermissions = $this->getPermissionUtils()->getDefaultPermissions();
$defaultPermissions = $this->getPermissionUtils()->getRequiredPermissions($fullPath);
if (!isset($permission)) {
$permission = (string) $defaultPermissions['dir'];
@@ -393,7 +392,11 @@ class Manager
}
try {
$umask = @umask(0);
$result = mkdir($fullPath, $permission, true);
if ($umask) {
@umask($umask);
}
if (!empty($defaultPermissions['user'])) {
$this->getPermissionUtils()->chown($fullPath);
@@ -484,11 +487,11 @@ class Manager
*/
public function checkCreateFile($filePath)
{
$defaultPermissions = $this->getPermissionUtils()->getDefaultPermissions();
$defaultPermissions = $this->getPermissionUtils()->getRequiredPermissions($filePath);
if (file_exists($filePath)) {
if (!is_writable($filePath) && !in_array($this->getPermissionUtils()->getCurrentPermission($filePath), array($defaultPermissions['file'], $defaultPermissions['dir']))) {
return $this->getPermissionUtils()->setDefaultPermissions($filePath, true);
return $this->getPermissionUtils()->setDefaultPermissions($filePath);
}
return true;
}
@@ -504,7 +507,7 @@ class Manager
}
if (touch($filePath)) {
return $this->getPermissionUtils()->setDefaultPermissions($filePath, true);
return $this->getPermissionUtils()->setDefaultPermissions($filePath);
}
return false;

View File

@@ -28,8 +28,9 @@
************************************************************************/
namespace Espo\Core\Utils\File;
use Espo\Core\Utils,
Espo\Core\Exceptions\Error;
use Espo\Core\Utils;
use Espo\Core\Exceptions\Error;
class Permission
{
@@ -44,47 +45,45 @@ class Permission
protected $permissionErrorRules = null;
protected $params = array(
'defaultPermissions' => array (
'dir' => '0775',
'file' => '0664',
'user' => '',
'group' => '',
),
'permissionMap' => array(
protected $writableMap = [
'data' => [
'recursive' => true,
],
'application/Espo/Modules' => [
'recursive' => false,
],
'client/modules' => [
'recursive' => false,
],
'custom/Espo/Custom' => [
'recursive' => true,
],
];
/** array('0664', '0775') */
'writable' => array(
'data',
'custom',
),
/** array('0644', '0755') */
'readable' => array(
'api',
'application',
'client',
'vendor',
'index.php',
'cron.php',
'rebuild.php',
'main.html',
'reset.html',
),
),
);
protected $permissionRules = array(
'writable' => array('0664', '0775'),
'readable' => array('0644', '0755'),
);
protected $defaultPermissions = [
'dir' => '0755',
'file' => '0644',
'user' => null,
'group' => null,
];
protected $writablePermissions = [
'file' => '0664',
'dir' => '0775',
];
public function __construct(Manager $fileManager, array $params = null)
{
$this->fileManager = $fileManager;
if (isset($params)) {
$this->params = $params;
if ($params) {
foreach ($params as $paramName => $paramValue) {
switch ($paramName) {
case 'defaultPermissions':
$this->defaultPermissions = array_merge($this->defaultPermissions, $paramValue);
break;
}
}
}
}
@@ -93,11 +92,6 @@ class Permission
return $this->fileManager;
}
protected function getParams()
{
return $this->params;
}
/**
* Get default settings
*
@@ -105,14 +99,34 @@ class Permission
*/
public function getDefaultPermissions()
{
$params = $this->getParams();
return $params['defaultPermissions'];
return $this->defaultPermissions;
}
public function getPermissionRules()
public function getWritableMap()
{
return $this->permissionRules;
return $this->writableMap;
}
public function getWritableList()
{
return array_keys($this->writableMap);
}
public function getRequiredPermissions($path)
{
$permission = $this->getDefaultPermissions();
foreach ($this->getWritableMap() as $writablePath => $writableOptions) {
if (!$writableOptions['recursive'] && $path == $writablePath) {
return array_merge($permission, $this->writablePermissions);
}
if ($writableOptions['recursive'] && substr($path, 0, strlen($writablePath)) == $writablePath) {
return array_merge($permission, $this->writablePermissions);
}
}
return $permission;
}
/**
@@ -129,7 +143,7 @@ class Permission
return false;
}
$permission = $this->getDefaultPermissions();
$permission = $this->getRequiredPermissions($path);
$result = $this->chmod($path, array($permission['file'], $permission['dir']), $recurse);
if (!empty($permission['user'])) {
@@ -342,8 +356,8 @@ class Permission
*
* @return bool
*/
protected function chgrpRecurse($path, $group) {
protected function chgrpRecurse($path, $group)
{
if (!file_exists($path)) {
return false;
}
@@ -461,60 +475,38 @@ class Permission
*
* @return bool
*/
public function setMapPermission($mode = null)
public function setMapPermission()
{
$this->permissionError = array();
$this->permissionErrorRules = array();
$params = $this->getParams();
$permissionRules = $this->permissionRules;
if (isset($mode)) {
foreach ($permissionRules as &$value) {
$value = $mode;
}
}
$result = true;
foreach ($params['permissionMap'] as $type => $items) {
$permission = $permissionRules[$type];
foreach ($this->getWritableMap() as $path => $options) {
if (!file_exists($path)) continue;
foreach ($items as $item) {
try {
$this->chmod($path, $this->writablePermissions, $options['recursive']);
} catch (\Throwable $e) {}
if (file_exists($item)) {
/** check is writable */
$res = is_writable($path);
try {
$this->chmod($item, $permission, true);
} catch (\Exception $e) {
}
$res = is_readable($item);
/** check is wtitable */
if ($type == 'writable') {
$res &= is_writable($item);
if (is_dir($item)) {
$name = uniqid();
try {
$res &= $this->getFileManager()->putContents(array($item, $name), 'test');
$res &= $this->getFileManager()->removeFile($name, $item);
} catch (\Exception $e) {
$res = false;
}
}
}
if (!$res) {
$result = false;
$this->permissionError[] = $item;
$this->permissionErrorRules[$item] = $permission;
}
if (is_dir($path)) {
try {
$name = uniqid();
$res &= $this->getFileManager()->putContents([$path, $name], 'test');
$res &= $this->getFileManager()->removeFile($name, $path);
} catch (\Throwable $e) {
$res = false;
}
}
if (!$res) {
$result = false;
$this->permissionError[] = $path;
$this->permissionErrorRules[$path] = $this->writablePermissions;
}
}
return $result;
@@ -607,4 +599,3 @@ class Permission
}
}

View File

@@ -159,11 +159,12 @@ class SystemRequirements
{
return $this->getRequiredList('permissionRequirements', [
'permissionMap.writable',
'permissionMap.readable',
], $additionalData);
], $additionalData, [
'permissionMap.writable' => $this->getFileManager()->getPermissionUtils()->getWritableList(),
]);
}
protected function getRequiredList($type, $checkList, array $additionalData = null)
protected function getRequiredList($type, $checkList, array $additionalData = null, array $predefinedData = [])
{
$config = $this->getConfig();
@@ -172,7 +173,8 @@ class SystemRequirements
foreach ($checkList as $itemName) {
$methodName = 'check' . ucfirst($type);
if (method_exists($this, $methodName)) {
$result = $this->$methodName($itemName, $config->get($itemName), $additionalData);
$itemValue = isset($predefinedData[$itemName]) ? $predefinedData[$itemName] : $config->get($itemName);
$result = $this->$methodName($itemName, $itemValue, $additionalData);
$list = array_merge($list, $result);
}
}

View File

@@ -29,31 +29,11 @@
return [
'defaultPermissions' => [
'dir' => '0775',
'file' => '0664',
'dir' => '0755',
'file' => '0644',
'user' => '',
'group' => ''
],
'permissionMap' => [
/** array('0664', '0775') */
'writable' => [
'data',
'custom',
'application/Espo/Modules',
'client/modules'
],
/** array('0644', '0755') */
'readable' => [
'api',
'application',
'client',
'vendor',
'index.php',
'cron.php',
'rebuild.php',
'clear_cache.php'
],
],
'jobMaxPortion' => 15, /** Max number of jobs per one execution. */
'jobPeriod' => 7800, /** Max execution time (in seconds) allocated for a sinle job. If exceeded then set to Failed.*/
'jobPeriodForActiveProcess' => 36000, /** Max execution time (in seconds) allocated for a sinle job with active process. If exceeded then set to Failed.*/
@@ -89,10 +69,8 @@ return [
'crud',
'logger',
'isInstalled',
'defaultPermissions',
'systemUser',
'permissionMap',
'permissionRules',
'defaultPermissions',
'passwordSalt',
'cryptKey',
'apiSecretKeys',
@@ -127,6 +105,8 @@ return [
'webSocketUseSecureServer',
'webSocketPort',
'aclStrictMode',
'actualDatabaseType',
'actualDatabaseVersion',
],
'adminItems' => [
'devMode',

View File

@@ -31,7 +31,7 @@ namespace Espo\Entities;
class Integration extends \Espo\Core\ORM\Entity
{
public function get($name, $params = [])
public function get(string $name, $params = [])
{
if ($name == 'id') {
return $this->id;
@@ -54,7 +54,7 @@ class Integration extends \Espo\Core\ORM\Entity
return null;
}
public function clear($name = null)
public function clear(?string $name = null)
{
parent::clear($name);

View File

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

View File

@@ -39,7 +39,6 @@ class OauthCallback extends \Espo\Core\EntryPoints\Base
public function run()
{
echo "EspoCRM rocks !!!";
echo "EspoCRM rocks! If this window is not closed automatically, it's probable that URL you use to access EspoCRM doesn't match URL specified at Administration > Settings > Site URL.";
}
}

View File

@@ -371,48 +371,13 @@ class Stream extends \Espo\Core\Hooks\Base
if (
$this->getMetadata()->get(['entityDefs', $entityType, 'links', $link, 'audited'])
) {
$n = $this->getEntityManager()->getRepository('Note')->where(array(
'type' => 'Relate',
'parentId' => $entity->id,
'parentType' => $entityType,
'relatedId' => $foreignEntity->id,
'relatedType' => $foreignEntity->getEntityType()
))->findOne();
if (!$n) {
$note = $this->getEntityManager()->getEntity('Note');
$note->set(array(
'type' => 'Relate',
'parentId' => $entity->id,
'parentType' => $entityType,
'relatedId' => $foreignEntity->id,
'relatedType' => $foreignEntity->getEntityType()
));
$this->getEntityManager()->saveEntity($note);
}
$this->getStreamService()->noteRelate($foreignEntity, $entityType, $entity->id);
}
$foreignLink = $entity->getRelationParam($link, 'foreign');
if ($this->getMetadata()->get(['entityDefs', $foreignEntity->getEntityType(), 'links', $foreignLink, 'audited'])) {
$n = $this->getEntityManager()->getRepository('Note')->where(array(
'type' => 'Relate',
'parentId' => $foreignEntity->id,
'parentType' => $foreignEntity->getEntityType(),
'relatedId' => $entity->id,
'relatedType' => $entityType
))->findOne();
if (!$n) {
$note = $this->getEntityManager()->getEntity('Note');
$note->set(array(
'type' => 'Relate',
'parentId' => $foreignEntity->id,
'parentType' => $foreignEntity->getEntityType(),
'relatedId' => $entity->id,
'relatedType' => $entityType
));
$this->getEntityManager()->saveEntity($note);
}
$this->getStreamService()->noteRelate($entity, $foreignEntity->getEntityType(), $foreignEntity->id);
}
}
}

View File

@@ -32,39 +32,41 @@ namespace Espo\Modules\Crm\Business\Distribution\CaseObj;
class LeastBusy
{
protected $entityManager;
protected $metadata;
public function __construct($entityManager)
public function __construct($entityManager, $metadata)
{
$this->entityManager = $entityManager;
}
protected function getEntityManager()
{
return $this->entityManager;
$this->metadata = $metadata;
}
public function getUser($team, $targetUserPosition = null)
{
$params = array();
$selectParams = [
'whereClause' => ['isActive' => true],
'orderBy' => 'id',
];
if (!empty($targetUserPosition)) {
$params['additionalColumnsConditions'] = array(
'role' => $targetUserPosition
);
$selectParams['additionalColumnsConditions'] = ['role' => $targetUserPosition];
}
$userList = $team->get('users', $params);
$userList = $team->get('users', $selectParams);
if (count($userList) == 0) {
return false;
}
$countHash = array();
$countHash = [];
$notActualStatusList =
$this->metadata->get(['entityDefs', 'Case', 'fields', 'status', 'notActualOptions']) ?? [];
foreach ($userList as $user) {
$count = $this->getEntityManager()->getRepository('Case')->where(array(
$count = $this->entityManager->getRepository('Case')->where([
'assignedUserId' => $user->id,
'status<>' => ['Closed', 'Rejected', 'Duplicated']
))->count();
'status!=' => $notActualStatusList,
])->count();
$countHash[$user->id] = $count;
}
@@ -83,8 +85,7 @@ class LeastBusy
}
if ($foundUserId !== false) {
return $this->getEntityManager()->getEntity('User', $foundUserId);
return $this->entityManager->getEntity('User', $foundUserId);
}
}
}

View File

@@ -45,29 +45,30 @@ class RoundRobin
public function getUser($team, $targetUserPosition = null)
{
$params = array();
$selectParams = [
'whereClause' => ['isActive' => true],
'orderBy' => 'id',
];
if (!empty($targetUserPosition)) {
$params['additionalColumnsConditions'] = array(
'role' => $targetUserPosition
);
$selectParams['additionalColumnsConditions'] = ['role' => $targetUserPosition];
}
$userList = $team->get('users', $params);
$userList = $team->get('users', $selectParams);
if (count($userList) == 0) {
return false;
}
$userIdList = array();
$userIdList = [];
foreach ($userList as $user) {
$userIdList[] = $user->id;
}
$case = $this->getEntityManager()->getRepository('Case')->where(array(
$case = $this->getEntityManager()->getRepository('Case')->where([
'assignedUserId' => $userIdList,
))->order('createdAt', 'DESC')->findOne();
])->order('number', 'DESC')->findOne();
if (empty($case)) {
$num = 0;
@@ -80,7 +81,8 @@ class RoundRobin
}
}
return $this->getEntityManager()->getEntity('User', $userIdList[$num]);
$id = $userIdList[$num];
return $this->getEntityManager()->getEntity('User', $id);
}
}

View File

@@ -0,0 +1,8 @@
{
"aclDependencies": {
"Lead.options.source": {
"scope": "Opportunity",
"field": "leadSource"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"aclDependencies": {
"entityDefs.Lead.fields.source.options": {
"scope": "Opportunity",
"field": "leadSource"
}
}
}

View File

@@ -310,8 +310,7 @@
"campaign": {
"type": "belongsTo",
"entity": "Campaign",
"foreign": "accounts",
"noJoin": true
"foreign": "accounts"
},
"campaignLogRecords": {
"type": "hasChildren",

View File

@@ -268,26 +268,22 @@
"createdAccount": {
"type": "belongsTo",
"entity": "Account",
"noJoin": true,
"foreign": "originalLead"
},
"createdContact": {
"type": "belongsTo",
"entity": "Contact",
"noJoin": true,
"foreign": "originalLead"
},
"createdOpportunity": {
"type": "belongsTo",
"entity": "Opportunity",
"noJoin": true,
"foreign": "originalLead"
},
"campaign": {
"type": "belongsTo",
"entity": "Campaign",
"foreign": "leads",
"noJoin": true
"foreign": "leads"
},
"campaignLogRecords": {
"type": "hasChildren",

View File

@@ -334,8 +334,7 @@
"campaign": {
"type": "belongsTo",
"entity": "Campaign",
"foreign": "opportunities",
"noJoin": true
"foreign": "opportunities"
},
"originalLead": {
"type": "hasOne",

View File

@@ -754,6 +754,13 @@ class Activities extends \Espo\Core\Services\Base
$service = $this->getServiceFactory()->create($entityType);
$selectManager = $this->getSelectManagerFactory()->create($entityType);
if ($entityType === 'Email') {
if ($params['orderBy'] ?? null === 'dateStart') {
$params['orderBy'] = 'dateSent';
}
}
$selectParams = $selectManager->getSelectParams($params, false, true);
$selectAttributeList = $service->getSelectAttributeList($params);
@@ -778,13 +785,6 @@ class Activities extends \Espo\Core\Services\Base
unset($selectParams['order']);
unset($selectParams['orderBy']);
if ($entityType === 'Email') {
if ($orderBy === 'dateStart') {
$orderBy = 'dateSent';
$order = 'desc';
}
}
$sql = $this->getActivitiesQuery($entity, $entityType, $statusList, $isHistory, $selectParams);
$query = $this->getEntityManager()->getQuery();

View File

@@ -630,16 +630,31 @@ abstract class Base
throw new \Exception("Empty MATCH parameters.");
}
$delimiterPosition = strpos($rest, ':');
if ($delimiterPosition === false) {
throw new \Exception("Bad MATCH usage.");
if (substr($rest, 0, 1) === '(' && substr($rest, -1) === ')') {
$rest = substr($rest, 1, -1);
$argumentList = self::parseArgumentListFromFunctionContent($rest);
if (count($argumentList) < 2) {
throw new \Exception("Bad MATCH usage.");
}
$columnList = [];
for ($i = 0; $i < count($argumentList) - 1; $i++) {
$columnList[] = $argumentList[$i];
}
$query = $argumentList[count($argumentList) - 1];
} else {
$delimiterPosition = strpos($rest, ':');
if ($delimiterPosition === false) {
throw new \Exception("Bad MATCH usage.");
}
$columns = substr($rest, 0, $delimiterPosition);
$query = mb_substr($rest, $delimiterPosition + 1);
$columnList = explode(',', $columns);
}
$columns = substr($rest, 0, $delimiterPosition);
$query = mb_substr($rest, $delimiterPosition + 1);
$columnList = explode(',', $columns);
$tableName = $this->toDb($entity->getEntityType());
foreach ($columnList as $i => $column) {

View File

@@ -81,7 +81,7 @@ abstract class Entity implements IEntity
}
}
public function clear($name = null)
public function clear(?string $name = null)
{
if (is_null($name)) {
$this->reset();
@@ -126,7 +126,7 @@ abstract class Entity implements IEntity
}
}
public function get($name, $params = [])
public function get(string $name, $params = [])
{
if ($name == 'id') {
return $this->id;
@@ -148,7 +148,7 @@ abstract class Entity implements IEntity
return null;
}
public function has($name)
public function has(string $name)
{
if ($name == 'id') {
return !!$this->id;
@@ -448,6 +448,15 @@ abstract class Entity implements IEntity
public function setFetched($name, $value)
{
if ($value) {
$type = $this->getAttributeType($name);
if ($type === self::JSON_OBJECT) {
$value = self::cloneObject($value);
} else if ($type === self::JSON_ARRAY) {
$value = self::cloneArray($value);
}
}
$this->fetchedValuesContainer[$name] = $value;
}
@@ -478,12 +487,16 @@ abstract class Entity implements IEntity
public function updateFetchedValues()
{
$this->fetchedValuesContainer = $this->valuesContainer;
foreach ($this->fetchedValuesContainer as $attribute => $value) {
$this->setFetched($attribute, $value);
}
}
public function setAsFetched()
{
$this->isFetched = true;
$this->fetchedValuesContainer = $this->valuesContainer;
$this->updateFetchedValues();
}
public function setAsNotFetched()
@@ -520,4 +533,45 @@ abstract class Entity implements IEntity
{
return $this->entityManager;
}
protected function cloneArray($value)
{
if (is_array($value)) {
$copy = [];
foreach ($value as $v) {
if (is_object($v)) {
$v = clone $v;
}
$copy[] = $v;
}
return $copy;
}
return $value;
}
protected function cloneObject($value)
{
if (is_array($value)) {
$copy = [];
foreach ($value as $v) {
$copy[] = self::cloneObject($v);
}
return $copy;
}
if (is_object($value)) {
$copy = (object) [];
foreach (get_object_vars($value) as $k => $v) {
$key = $k;
if (!is_string($key)) {
$key = strval($key);
}
$copy->$key = self::cloneObject($v);
}
return $copy;
}
return $value;
}
}

View File

@@ -29,7 +29,7 @@
namespace Espo\ORM;
class EntityCollection implements \Iterator, \Countable, \ArrayAccess, \SeekableIterator
class EntityCollection implements \Iterator, \Countable, \ArrayAccess, \SeekableIterator, ICollection
{
private $entityFactory = null;

View File

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

View File

@@ -56,37 +56,27 @@ interface IEntity
const HAS_CHILDREN = 'hasChildren';
/**
* Push values from the array.
* E.g. insert values into the bean from a request data.
* @param array $arr Array of field - value pairs
*/
function populateFromArray(array $arr);
/**
* Resets all fields in the current model.
* Resets all attributes in the current model.
*/
function reset();
/**
* Set field.
* Set attribute.
*/
function set($name, $value);
/**
* Get field.
* Get attribute.
*/
function get($name);
function get(string $name);
/**
* Check field is set.
* Check attribute is set.
*/
function has($name);
function has(string $name);
/**
* Clear field.
* Clear attribute.
*/
function clear($name);
function clear(?string $name);
}

View File

@@ -58,6 +58,8 @@ class RDB extends \Espo\ORM\Repository
*/
protected $listParams = [];
private $isTableLocked = false;
public function __construct($entityType, EntityManager $entityManager, EntityFactory $entityFactory)
{
$this->entityType = $entityType;
@@ -643,9 +645,27 @@ class RDB extends \Espo\ORM\Repository
return $params;
}
protected function getPDO()
{
return $this->getEntityManager()->getPDO();
}
protected function lockTable()
{
$tableName = $this->getEntityManager()->getQuery()->toDb($this->entityType);
$this->getPDO()->query("LOCK TABLES `{$tableName}` WRITE");
$this->isTableLocked = true;
}
protected function unlockTable()
{
$this->getPDO()->query("UNLOCK TABLES");
$this->isTableLocked = false;
}
protected function isTableLocked()
{
return $this->isTableLocked;
}
}

View File

@@ -65,4 +65,3 @@ class RepositoryFactory
$this->defaultRepositoryClassName = $defaultRepositoryClassName;
}
}

View File

@@ -29,7 +29,7 @@
namespace Espo\ORM;
class SthCollection implements \IteratorAggregate
class SthCollection implements \IteratorAggregate, ICollection
{
protected $entityManager = null;

View File

@@ -87,9 +87,21 @@ class Email extends \Espo\Core\ORM\Repositories\RDB
{
if ($entity->get('fromEmailAddressName')) {
$entity->set('from', $entity->get('fromEmailAddressName'));
} else {
$entity->set('from', null);
return;
}
if ($entity->get('fromEmailAddressId')) {
$ea = $this->getEntityManager()->getRepository('EmailAddress')->get($entity->get('fromEmailAddressId'));
if ($ea) {
$entity->set('from', $ea->get('name'));
return;
}
}
if (!$entity->has('fromEmailAddressId')) {
return;
}
$entity->set('from', null);
}
public function loadToField(Entity $entity)

View File

@@ -85,36 +85,6 @@ class User extends \Espo\Core\ORM\Repositories\RDB
parent::beforeSave($entity, $options);
if ($entity->isNew()) {
$userName = $entity->get('userName');
if (empty($userName)) {
throw new Error();
}
$user = $this->where([
'userName' => $userName
])->findOne();
if ($user) {
throw new Conflict(json_encode(['reason' => 'userNameExists']));
}
} else {
if ($entity->isAttributeChanged('userName')) {
$userName = $entity->get('userName');
if (empty($userName)) {
throw new Error();
}
$user = $this->where(array(
'userName' => $userName,
'id!=' => $entity->id
))->findOne();
if ($user) {
throw new Conflict(json_encode(['reason' => 'userNameExists']));
}
}
}
if ($entity->has('type') && !$entity->isPortal()) {
$entity->set('portalRolesIds', []);
$entity->set('portalRolesNames', (object)[]);
@@ -130,10 +100,51 @@ class User extends \Espo\Core\ORM\Repositories\RDB
$entity->set('defaultTeamId', null);
$entity->set('defaultTeamName', null);
}
if ($entity->isNew()) {
$userName = $entity->get('userName');
if (empty($userName)) {
throw new Error("Username can't be empty.");
}
$this->lockTable();
$user = $this->select(['id'])->where([
'userName' => $userName
])->findOne();
if ($user) {
$this->unlockTable();
throw new Conflict(json_encode(['reason' => 'userNameExists']));
}
} else {
if ($entity->isAttributeChanged('userName')) {
$userName = $entity->get('userName');
if (empty($userName)) {
throw new Error("Username can't be empty.");
}
$this->lockTable();
$user = $this->select(['id'])->where(array(
'userName' => $userName,
'id!=' => $entity->id
))->findOne();
if ($user) {
$this->unlockTable();
throw new Conflict(json_encode(['reason' => 'userNameExists']));
}
}
}
}
protected function afterSave(Entity $entity, array $options = [])
{
if ($this->isTableLocked()) {
$this->unlockTable();
}
parent::afterSave($entity, $options);
if ($entity->isApi()) {

View File

@@ -0,0 +1,168 @@
{
"names": {
"AED":"United Arab Emirates Dirham",
"AFN":"Afghan Afghani",
"ALL":"Albanian Lek",
"AMD":"Armenian Dram",
"ANG":"Netherlands Antillean Guilder",
"AOA":"Angolan Kwanza",
"ARS":"Argentine Peso",
"AUD":"Australian Dollar",
"AWG":"Aruban Florin",
"AZN":"Azerbaijani Manat",
"BAM":"Bosnia-Herzegovina Convertible Mark",
"BBD":"Barbadian Dollar",
"BDT":"Bangladeshi Taka",
"BGN":"Bulgarian Lev",
"BHD":"Bahraini Dinar",
"BIF":"Burundian Franc",
"BMD":"Bermudan Dollar",
"BND":"Brunei Dollar",
"BOB":"Bolivian Boliviano",
"BOV":"Bolivian Mvdol",
"BRL":"Brazilian Real",
"BSD":"Bahamian Dollar",
"BTN":"Bhutanese Ngultrum",
"BWP":"Botswanan Pula",
"BYN":"Belarusian Ruble",
"BZD":"Belize Dollar",
"CAD":"Canadian Dollar",
"CDF":"Congolese Franc",
"CHE":"WIR Euro",
"CHF":"Swiss Franc",
"CHW":"WIR Franc",
"CLF":"Chilean Unit of Account (UF)",
"CLP":"Chilean Peso",
"CNH":"Chinese Yuan (offshore)",
"CNY":"Chinese Yuan",
"COP":"Colombian Peso",
"COU":"Colombian Real Value Unit",
"CRC":"Costa Rican Colón",
"CUC":"Cuban Convertible Peso",
"CUP":"Cuban Peso",
"CVE":"Cape Verdean Escudo",
"CZK":"Czech Koruna",
"DJF":"Djiboutian Franc",
"DKK":"Danish Krone",
"DOP":"Dominican Peso",
"DZD":"Algerian Dinar",
"EGP":"Egyptian Pound",
"ERN":"Eritrean Nakfa",
"ETB":"Ethiopian Birr",
"EUR":"Euro",
"FJD":"Fijian Dollar",
"FKP":"Falkland Islands Pound",
"GBP":"British Pound",
"GEL":"Georgian Lari",
"GHS":"Ghanaian Cedi",
"GIP":"Gibraltar Pound",
"GMD":"Gambian Dalasi",
"GNF":"Guinean Franc",
"GTQ":"Guatemalan Quetzal",
"GYD":"Guyanaese Dollar",
"HKD":"Hong Kong Dollar",
"HNL":"Honduran Lempira",
"HRK":"Croatian Kuna",
"HTG":"Haitian Gourde",
"HUF":"Hungarian Forint",
"IDR":"Indonesian Rupiah",
"ILS":"Israeli New Shekel",
"INR":"Indian Rupee",
"IQD":"Iraqi Dinar",
"IRR":"Iranian Rial",
"ISK":"Icelandic Króna",
"JMD":"Jamaican Dollar",
"JOD":"Jordanian Dinar",
"JPY":"Japanese Yen",
"KES":"Kenyan Shilling",
"KGS":"Kyrgystani Som",
"KHR":"Cambodian Riel",
"KMF":"Comorian Franc",
"KPW":"North Korean Won",
"KRW":"South Korean Won",
"KWD":"Kuwaiti Dinar",
"KYD":"Cayman Islands Dollar",
"KZT":"Kazakhstani Tenge",
"LAK":"Laotian Kip",
"LBP":"Lebanese Pound",
"LKR":"Sri Lankan Rupee",
"LRD":"Liberian Dollar",
"LSL":"Lesotho Loti",
"LYD":"Libyan Dinar",
"MAD":"Moroccan Dirham",
"MDL":"Moldovan Leu",
"MGA":"Malagasy Ariary",
"MKD":"Macedonian Denar",
"MMK":"Myanmar Kyat",
"MNT":"Mongolian Tugrik",
"MOP":"Macanese Pataca",
"MRO":"Mauritanian Ouguiya",
"MUR":"Mauritian Rupee",
"MWK":"Malawian Kwacha",
"MXN":"Mexican Peso",
"MXV":"Mexican Investment Unit",
"MYR":"Malaysian Ringgit",
"MZN":"Mozambican Metical",
"NAD":"Namibian Dollar",
"NGN":"Nigerian Naira",
"NIO":"Nicaraguan Córdoba",
"NOK":"Norwegian Krone",
"NPR":"Nepalese Rupee",
"NZD":"New Zealand Dollar",
"OMR":"Omani Rial",
"PAB":"Panamanian Balboa",
"PEN":"Peruvian Sol",
"PGK":"Papua New Guinean Kina",
"PHP":"Philippine Piso",
"PKR":"Pakistani Rupee",
"PLN":"Polish Zloty",
"PYG":"Paraguayan Guarani",
"QAR":"Qatari Rial",
"RON":"Romanian Leu",
"RSD":"Serbian Dinar",
"RUB":"Russian Ruble",
"RWF":"Rwandan Franc",
"SAR":"Saudi Riyal",
"SBD":"Solomon Islands Dollar",
"SCR":"Seychellois Rupee",
"SDG":"Sudanese Pound",
"SEK":"Swedish Krona",
"SGD":"Singapore Dollar",
"SHP":"St. Helena Pound",
"SLL":"Sierra Leonean Leone",
"SOS":"Somali Shilling",
"SRD":"Surinamese Dollar",
"SSP":"South Sudanese Pound",
"STN":"São Tomé & Príncipe Dobra (2018)",
"SYP":"Syrian Pound",
"SZL":"Swazi Lilangeni",
"SVC": "Salvadoran Colón",
"THB":"Thai Baht",
"TJS":"Tajikistani Somoni",
"TND":"Tunisian Dinar",
"TOP":"Tongan Paʻanga",
"TRY":"Turkish Lira",
"TTD":"Trinidad & Tobago Dollar",
"TWD":"New Taiwan Dollar",
"TZS":"Tanzanian Shilling",
"UAH":"Ukrainian Hryvnia",
"UGX":"Ugandan Shilling",
"USD":"US Dollar",
"USN":"US Dollar (Next day)",
"UYI":"Uruguayan Peso (Indexed Units)",
"UYU":"Uruguayan Peso",
"UZS":"Uzbekistani Som",
"VEF":"Venezuelan Bolívar",
"VND":"Vietnamese Dong",
"VUV":"Vanuatu Vatu",
"WST":"Samoan Tala",
"XAF":"Central African CFA Franc",
"XCD":"East Caribbean Dollar",
"XOF":"West African CFA Franc",
"XPF":"CFP Franc",
"YER":"Yemeni Rial",
"ZAR":"South African Rand",
"ZMW":"Zambian Kwacha",
"ZWL": "Zimbabwe Dollar"
}
}

View File

@@ -107,7 +107,7 @@
"View Users": "View Users"
},
"messages": {
"noSmtpSetup": "No SMTP setup. {link}.",
"noSmtpSetup": "SMTP is not configured: {link}",
"testEmailSent": "Test email has been sent",
"emailSent": "Email has been sent",
"savedAsDraft": "Saved as draft",

View File

@@ -8,7 +8,8 @@
"attachments": "Attachments",
"insertField": "Insert Field",
"oneOff": "One-off",
"category": "Category"
"category": "Category",
"insertField": "Placeholders"
},
"links": {
},

View File

@@ -608,7 +608,9 @@
"isNot": "Is Not",
"isNotOneOf": "None Of",
"anyOf": "Any Of",
"noneOf": "None Of"
"allOf": "All Of",
"noneOf": "None Of",
"any": "Any"
},
"varcharSearchRanges": {
"equals": "Equals",

View File

@@ -65,7 +65,7 @@
"removeDuplicates": "This will permanently remove all imported records that were recognized as duplicates.",
"confirmRevert": "This will remove all imported records permanently. Are you sure?",
"confirmRemoveDuplicates": "This will permanently remove all imported records that were recognized as duplicates. Are you sure?",
"confirmRemoveImportLog" : "This will remove the import log. All imported records will be kept. You wan't be able to revert import results. Are you sure you?",
"confirmRemoveImportLog" : "This will remove the import log. All imported records will be kept. You won't be able to revert import results. Are you sure you?",
"removeImportLog": "This will remove the import log. All imported records will be kept. Use it if you are sure that import is fine."
},
"fields": {

View File

@@ -2,8 +2,9 @@
"account",
"dateSent",
"emailAddress",
"from",
"to",
"isNotRead",
"isImportant",
"isNotReplied",
"status",
"parent",

View File

@@ -21,7 +21,6 @@
{
"name":"insertField",
"view": "views/email-template/fields/insert-field",
"customLabel": "",
"fullWidth":true
}
],

View File

@@ -9,7 +9,6 @@
{
"name":"insertField",
"view": "views/email-template/fields/insert-field",
"customLabel": "",
"fullWidth":true
}
],

View File

@@ -163,5 +163,102 @@
"ZAR":"R",
"ZWD":"Z$",
"BTC":"฿"
}
},
"list": [
"AFN",
"AED",
"ALL",
"ANG",
"AOA",
"ARS",
"AUD",
"BAM",
"BGN",
"BHD",
"BND",
"BOB",
"BRL",
"BWP",
"CAD",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CVE",
"CZK",
"DKK",
"DOP",
"DZD",
"EGP",
"EUR",
"FJD",
"GBP",
"GNF",
"HKD",
"HNL",
"HRK",
"HUF",
"IDR",
"ILS",
"INR",
"IRR",
"JMD",
"JOD",
"JPY",
"KES",
"KRW",
"KWD",
"KYD",
"KZT",
"LBP",
"LKR",
"MAD",
"MDL",
"MKD",
"MMK",
"MUR",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"SAR",
"SCR",
"SEK",
"SGD",
"SLL",
"SVC",
"THB",
"TND",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VND",
"YER",
"ZAR",
"ZMW",
"ZWL"
]
}

View File

@@ -7,6 +7,7 @@
["app", "fileStorage", "implementationClassNameMap"],
["app", "emailNotifications", "handlerClassNameMap"],
["app", "client"],
["app", "language"],
["app", "auth2FAMethods", "__ANY__", "implementationClassName"],
["app", "auth2FAMethods", "__ANY__", "implementationUserClassName"],
["authenticationMethods", "__ANY__", "implementationClassName"]

View File

@@ -34,6 +34,39 @@
],
"dynamicLogic": {
"fields": {
"ldapHost": {
"required": {
"conditionGroup": [
{
"type": "equals",
"attribute": "authenticationMethod",
"value": "LDAP"
}
]
}
},
"ldapUserNameAttribute": {
"required": {
"conditionGroup": [
{
"type": "equals",
"attribute": "authenticationMethod",
"value": "LDAP"
}
]
}
},
"ldapUserObjectClass": {
"required": {
"conditionGroup": [
{
"type": "equals",
"attribute": "authenticationMethod",
"value": "LDAP"
}
]
}
},
"ldapUsername": {
"visible": {
"conditionGroup": [
@@ -42,6 +75,14 @@
"attribute": "ldapAuth"
}
]
},
"required": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "ldapAuth"
}
]
}
},
"ldapPassword": {
@@ -94,6 +135,14 @@
"attribute": "ldapCreateEspoUser"
}
]
},
"required": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "ldapCreateEspoUser"
}
]
}
},
"ldapUserFirstNameAttribute": {
@@ -104,6 +153,14 @@
"attribute": "ldapCreateEspoUser"
}
]
},
"required": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "ldapCreateEspoUser"
}
]
}
},
"ldapUserLastNameAttribute": {
@@ -114,6 +171,14 @@
"attribute": "ldapCreateEspoUser"
}
]
},
"required": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "ldapCreateEspoUser"
}
]
}
},
"ldapUserEmailAddressAttribute": {
@@ -124,6 +189,14 @@
"attribute": "ldapCreateEspoUser"
}
]
},
"required": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "ldapCreateEspoUser"
}
]
}
},
"ldapUserPhoneNumberAttribute": {
@@ -134,6 +207,14 @@
"attribute": "ldapCreateEspoUser"
}
]
},
"required": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "ldapCreateEspoUser"
}
]
}
},
"ldapUserTeams": {

View File

@@ -0,0 +1,3 @@
{
"languageAclDisabled": true
}

View File

@@ -60,8 +60,8 @@
"currencyList": {
"type": "multiEnum",
"default": ["USD", "EUR"],
"options": ["AED","ANG","ARS","AUD","BAM", "BGN","BHD","BND","BOB","BRL","BWP","CAD","CHF","CLP","CNY","COP","CRC","CZK","DKK","DOP","DZD","EEK","EGP","EUR","FJD","GBP","GNF","HKD","HNL","HRK","HUF","IDR","ILS","INR","IRR","JMD","JOD","JPY","KES","KRW","KWD","KYD","KZT","LBP","LKR","LTL","LVL","MAD","MDL","MKD","MMK","MUR","MXN","MYR","NAD","NGN","NIO","NOK","NPR","NZD","OMR","PEN","PGK","PHP","PKR","PLN","PYG","QAR","RON","RSD","RUB","SAR","SCR","SEK","SGD","SKK","SLL","SVC","THB","TND","TRY","TTD","TWD","TZS","UAH","UGX","USD","UYU","UZS","VND","YER","ZAR","ZMK"],
"required": true
"required": true,
"view": "views/settings/fields/currency-list"
},
"defaultCurrency": {
"type": "enum",
@@ -101,12 +101,10 @@
"type": "int",
"min": 0,
"max": 9999,
"required": true,
"default": 587
},
"smtpAuth": {
"type": "bool",
"default": true
"type": "bool"
},
"smtpSecurity": {
"type": "enum",
@@ -114,8 +112,7 @@
"options": ["", "SSL", "TLS"]
},
"smtpUsername": {
"type": "varchar",
"required": true
"type": "varchar"
},
"smtpPassword": {
"type": "password"
@@ -193,8 +190,7 @@
"type": "bool"
},
"ldapHost": {
"type": "varchar",
"required": true
"type": "varchar"
},
"ldapPort": {
"type": "varchar",
@@ -210,7 +206,6 @@
},
"ldapUsername": {
"type": "varchar",
"required": true,
"tooltip": true
},
"ldapPassword": {
@@ -265,37 +260,30 @@
},
"ldapUserNameAttribute": {
"type": "varchar",
"required": true,
"tooltip": true
},
"ldapUserObjectClass": {
"type": "varchar",
"required": true,
"tooltip": true
},
"ldapUserFirstNameAttribute": {
"type": "varchar",
"required": true,
"tooltip": true
},
"ldapUserLastNameAttribute": {
"type": "varchar",
"required": true,
"tooltip": true
},
"ldapUserTitleAttribute": {
"type": "varchar",
"required": true,
"tooltip": true
},
"ldapUserEmailAddressAttribute": {
"type": "varchar",
"required": true,
"tooltip": true
},
"ldapUserPhoneNumberAttribute": {
"type": "varchar",
"required": true,
"tooltip": true
},
"ldapUserDefaultTeam": {

View File

@@ -25,6 +25,10 @@
"type": "bool",
"default": false
},
{
"name": "displayAsList",
"type":"bool"
},
{
"name": "maxCount",
"type": "int",
@@ -40,9 +44,13 @@
}
],
"validationList": [
"array",
"required",
"maxCount"
],
"mandatoryValidationList": [
"array"
],
"filter": true,
"notCreatable": false,
"notSortable": true,

View File

@@ -28,6 +28,12 @@
"type":"bool"
}
],
"validationList": [
"array"
],
"mandatoryValidationList": [
"array"
],
"filter": true,
"notCreatable": true,
"fieldDefs":{

View File

@@ -36,9 +36,13 @@
}
],
"validationList": [
"array",
"required",
"maxCount"
],
"mandatoryValidationList": [
"array"
],
"filter": true,
"notCreatable": false,
"notSortable": true,

View File

@@ -44,7 +44,7 @@
"url": "views/fields/foreign-url",
"date": "views/fields/foreign-date",
"datetime": "views/fields/foreign-datetime",
"text": "views/fields/views/fields/foreign-text",
"text": "views/fields/foreign-text",
"number": "views/fields/foreign-varchar",
"bool": "views/fields/foreign-bool",
"email": "views/fields/foreign-email",

View File

@@ -14,6 +14,11 @@
{
"name":"readOnly",
"type":"bool"
},
{
"name": "default",
"type": "linkMultiple",
"view": "views/admin/field-manager/fields/link-multiple/default"
}
],
"actualFields":[

View File

@@ -51,9 +51,13 @@
}
],
"validationList": [
"array",
"required",
"maxCount"
],
"mandatoryValidationList": [
"array"
],
"filter": true,
"notCreatable": false,
"notSortable": true,

View File

@@ -33,7 +33,7 @@ class Email extends \Espo\Core\SelectManagers\Base
{
protected $textFilterUseContainsAttributeList = ['name'];
protected $fullTextSearchForceOrderOnlyByRelevance = true;
protected $fullTextOrderType = self::FT_ORDER_ORIGINAL;
protected $selectAttributesDependancyMap = [
'subject' => ['name'],
@@ -74,6 +74,17 @@ class Email extends \Espo\Core\SelectManagers\Base
if ($folderId === 'important' || $folderId === 'drafts') {
$skipIndex = true;
}
$actualDatabaseType = $this->getConfig()->get('actualDatabaseType');
$actualDatabaseVersion = $this->getConfig()->get('actualDatabaseVersion');
if (
!$skipIndex &&
($actualDatabaseType !== 'mysql' || version_compare($actualDatabaseVersion, '8.0.0') < 0) &&
$this->hasLinkJoined('teams', $result)
) {
$skipIndex = true;
}
if (!$skipIndex) {
$result['useIndex'] = 'dateSent';
}
@@ -385,27 +396,82 @@ class Email extends \Espo\Core\SelectManagers\Base
], $result);
}
public function whereEmailAddress(string $value, array &$result)
protected function getWherePartEmailAddressEquals($value, array &$result)
{
$orItem = [];
if (!$value) {
return ['id' => null];
}
$emailAddressId = $this->getEmailAddressIdByValue($value);
if ($emailAddressId) {
$this->leftJoinEmailAddress($result);
$orItem['fromEmailAddressId'] = $emailAddressId;
$orItem['emailEmailAddress.emailAddressId'] = $emailAddressId;
$result['whereClause'][] = [
'OR' => $orItem
];
} else {
if (empty($result['customWhere'])) {
$result['customWhere'] = '';
}
$result['customWhere'] .= ' AND 0';
if (!$emailAddressId) {
return ['id' => null];
}
$this->setDistinct(true, $result);
$alias = 'emailEmailAddress' . strval(rand(10000, 99999));
$this->addLeftJoin([
'EmailEmailAddress',
$alias,
[
'emailId:' => 'id',
'deleted' => false,
]
], $result);
return [
'OR' => [
'fromEmailAddressId' => $emailAddressId,
$alias . '.emailAddressId' => $emailAddressId,
],
];
}
protected function getWherePartFromEquals($value, array &$result)
{
if (!$value) {
return ['id' => null];
}
$emailAddressId = $this->getEmailAddressIdByValue($value);
if (!$emailAddressId) {
return ['id' => null];
}
return [
'fromEmailAddressId' => $emailAddressId,
];
}
protected function getWherePartToEquals($value, array &$result)
{
if (!$value) {
return ['id' => null];
}
$emailAddressId = $this->getEmailAddressIdByValue($value);
if (!$emailAddressId) {
return ['id' => null];
}
$alias = 'emailEmailAddress' . strval(rand(10000, 99999));
$this->addLeftJoin([
'EmailEmailAddress',
$alias,
[
'emailId:' => 'id',
'deleted' => false,
]
], $result);
return [
$alias . '.emailAddressId' => $emailAddressId,
$alias . '.addressType' => 'to',
];
}
protected function getWherePartIsNotRepliedIsTrue()

View File

@@ -44,7 +44,7 @@ class EmailFilter extends \Espo\Core\SelectManagers\Base
'assignedUserId' => $this->getUser()->id
])->find();
foreach ($emailAccountList as $emailAccount) {
$idList = $emailAccount->id;
$idList[] = $emailAccount->id;
}
if (count($idList)) {

View File

@@ -125,6 +125,16 @@ class User extends \Espo\Core\SelectManagers\Base
protected function accessOnlyOwn(&$result)
{
if ($this->getAcl()->get('portalPermission') == 'yes') {
$result['whereClause'][] = [
'OR' => [
'id' => $this->getUser()->id,
'type' => 'portal',
],
];
return;
}
$result['whereClause'][] = [
'id' => $this->getUser()->id
];
@@ -141,11 +151,18 @@ class User extends \Espo\Core\SelectManagers\Base
{
$this->setDistinct(true, $result);
$this->addLeftJoin(['teams', 'teamsAccess'], $result);
$or = [
'teamsAccess.id' => $this->getUser()->getLinkMultipleIdList('teams'),
'id' => $this->getUser()->id,
];
if ($this->getAcl()->get('portalPermission') == 'yes') {
$or['type'] = 'portal';
}
$result['whereClause'][] = [
'OR' => [
'teamsAccess.id' => $this->getUser()->getLinkMultipleIdList('teams'),
'id' => $this->getUser()->id
]
'OR' => $or,
];
}
}

View File

@@ -198,8 +198,8 @@ class Email extends Record
}
if ($smtpParams) {
if ($emailAddress) {
$this->applySmtpHandler($this->getUser()->id, $emailAddress, $smtpParams);
if ($fromAddress) {
$this->applySmtpHandler($this->getUser()->id, $fromAddress, $smtpParams);
}
$emailSender->useSmtp($smtpParams);
}
@@ -380,6 +380,12 @@ class Email extends Record
}
$this->loadAdditionalFields($entity);
if (!isset($data->from) && !isset($data->to) && !isset($data->cc)) {
$entity->clear('nameHash');
$entity->clear('idHash');
$entity->clear('typeHash');
}
}
public function loadFromField(Entity $entity)
@@ -775,31 +781,6 @@ class Email extends Record
$this->getEntityManager()->getRepository('Email')->loadNameHash($entity, $fieldList);
}
protected function getSelectParams($params)
{
$searchByEmailAddress = false;
if (!empty($params['where']) && is_array($params['where'])) {
foreach ($params['where'] as $i => $p) {
if (!empty($p['attribute']) && $p['attribute'] == 'emailAddress') {
$searchByEmailAddress = true;
$emailAddress = $p['value'];
unset($params['where'][$i]);
}
}
}
$selectManager = $this->getSelectManager($this->getEntityType());
$selectParams = $selectManager->getSelectParams($params, true);
if ($searchByEmailAddress) {
$selectManager->whereEmailAddress($emailAddress, $selectParams);
}
return $selectParams;
}
public function copyAttachments($emailId, $parentType, $parentId)
{
return $this->getCopiedAttachments($emailId, $parentType, $parentId);

View File

@@ -550,7 +550,7 @@ class InboundEmail extends \Espo\Services\Record
$className = '\\Espo\\Modules\\Crm\\Business\\Distribution\\CaseObj\\LeastBusy';
}
$distribution = new $className($this->getEntityManager());
$distribution = new $className($this->getEntityManager(), $this->getMetadata());
$user = $distribution->getUser($team, $targetUserPosition);

View File

@@ -97,6 +97,7 @@ class Language extends \Espo\Core\Services\Base
$scopeList = array_keys($this->getMetadata()->get(['scopes'], []));
foreach ($scopeList as $scope) {
if (!$this->getMetadata()->get(['scopes', $scope, 'entity'])) continue;
if ($this->getMetadata()->get(['entityAcl', $scope, 'languageAclDisabled'])) continue;
if (!$this->getAcl()->check($scope)) {
@@ -133,12 +134,39 @@ class Language extends \Espo\Core\Services\Base
'userHasNoEmailAddress' => $languageObj->translate('userHasNoEmailAddress', 'messages', 'Admin'),
],
];
}
$data['User']['fields']['password'] = $languageObj->translate('password', 'fields', 'User');
$data['User']['fields']['passwordConfirm'] = $languageObj->translate('passwordConfirm', 'fields', 'User');
foreach (($this->getMetadata()->get(['app', 'language', 'aclDependencies']) ?? []) as $target => $item) {
$targetArr = explode('.', $target);
$aclScope = $item['scope'] ?? null;;
$aclField = $item['field'] ?? null;
if (!$aclScope) continue;
if (!$this->getAcl()->check($aclScope)) continue;
if ($aclField && in_array($aclField, $this->getAcl()->getScopeForbiddenFieldList($aclScope))) continue;
$pointer =& $data;
foreach ($targetArr as $i => $k) {
if ($i === count($targetArr) - 1) {
$pointer[$k] = $languageObj->get($targetArr);
break;
}
if (!isset($pointer[$k])) {
$pointer[$k] = [];
}
$pointer =& $pointer[$k];
}
}
}
}
$data['User']['fields'] = $data['User']['fields'] ?? [];
$data['User']['fields']['password'] = $languageObj->translate('password', 'fields', 'User');
$data['User']['fields']['passwordConfirm'] = $languageObj->translate('passwordConfirm', 'fields', 'User');
$data['User']['fields']['newPassword'] = $languageObj->translate('newPassword', 'fields', 'User');
$data['User']['fields']['newPasswordConfirm'] = $languageObj->translate('newPasswordConfirm', 'fields', 'User');
return $data;
}
}

View File

@@ -60,7 +60,9 @@ class Layout extends \Espo\Core\Services\Base
if ($name === 'relationships') {
$data = json_decode($dataString);
if (is_array($data)) {
foreach ($data as $i => $link) {
foreach ($data as $i => $item) {
$link = $item;
if (is_object($item)) $link = $item->name ?? null;
$foreignEntityType = $this->getMetadata()->get(['entityDefs', $scope, 'links', $link, 'entity']);
if ($foreignEntityType) {
if (!$this->getAcl()->check($foreignEntityType)) {

View File

@@ -64,6 +64,7 @@ class Metadata extends \Espo\Core\Services\Base
if (!$this->getUser()->isAdmin()) {
$scopeList = array_keys($this->getMetadata()->get(['scopes'], []));
foreach ($scopeList as $scope) {
if (!$this->getMetadata()->get(['scopes', $scope, 'entity'])) continue;
if (in_array($scope, ['Reminder'])) continue;
if (!$this->getAcl()->check($scope)) {
unset($data->entityDefs->$scope);
@@ -143,6 +144,39 @@ class Metadata extends \Espo\Core\Services\Base
unset($data->authenticationMethods);
unset($data->formula);
foreach (($this->getMetadata()->get(['app', 'metadata', 'aclDependencies']) ?? []) as $target => $item) {
$targetArr = explode('.', $target);
if (is_string($item)) {
$depArr = explode('.', $item);
$pointer = $data;
foreach ($depArr as $k) {
if (!isset($pointer->$k)) {
continue 2;
}
$pointer = $pointer->$k;
}
} else if (is_array($item)) {
$aclScope = $item['scope'] ?? null;;
$aclField = $item['field'] ?? null;
if (!$aclScope) continue;
if (!$this->getAcl()->check($aclScope)) continue;
if ($aclField && in_array($aclField, $this->getAcl()->getScopeForbiddenFieldList($aclScope))) continue;
}
$pointer = $data;
foreach ($targetArr as $i => $k) {
if ($i === count($targetArr) - 1) {
$pointer->$k = $this->getMetadata()->get($targetArr);
break;
}
if (!isset($pointer->$k)) {
$pointer->$k = (object) [];
}
$pointer = $pointer->$k;
}
}
}
return $data;

View File

@@ -307,7 +307,8 @@ class Pdf extends \Espo\Core\Services\Base
$this->getAcl(),
$this->getInjection('entityManager'),
$this->getInjection('metadata'),
$this->getInjection('defaultLanguage')
$this->getInjection('defaultLanguage'),
$this->getInjection('config')
);
}
}

View File

@@ -29,8 +29,9 @@
namespace Espo\Services;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\BadRequest;
use Espo\ORM\Entity;
@@ -190,6 +191,10 @@ class Settings extends \Espo\Core\Services\Base
unset($data->$item);
}
$entity = $this->getEntityManager()->getEntity('Settings');
$entity->set($data);
$this->processValidation($entity, $data);
if (
(isset($data->useCache) && $data->useCache !== $this->getConfig()->get('useCache'))
||
@@ -297,4 +302,44 @@ class Settings extends \Espo\Core\Services\Base
return $itemList;
}
protected function processValidation(Entity $entity, $data)
{
$fieldList = $this->getFieldManagerUtil()->getEntityTypeFieldList('Settings');
foreach ($fieldList as $field) {
if (!$this->isFieldSetInData($data, $field)) continue;
$this->processValidationField($entity, $field, $data);
}
}
protected function processValidationField(Entity $entity, string $field, $data)
{
$fieldType = $this->getFieldManagerUtil()->getEntityTypeFieldParam('Settings', $field, 'type');
$validationList = $this->getMetadata()->get(['fields', $fieldType, 'validationList'], []);
$mandatoryValidationList = $this->getMetadata()->get(['fields', $fieldType, 'mandatoryValidationList'], []);
$fieldValidatorManager = $this->getInjection('container')->get('fieldValidatorManager');
foreach ($validationList as $type) {
$value = $this->getFieldManagerUtil()->getEntityTypeFieldParam('Settings', $field, $type);
if (is_null($value) && !in_array($type, $mandatoryValidationList)) continue;
if (!$fieldValidatorManager->check($entity, $field, $type, $data)) {
throw new BadRequest("Not valid data. Field: '{$field}', type: {$type}.");
}
}
}
protected function isFieldSetInData($data, $field)
{
$attributeList = $this->getFieldManagerUtil()->getActualAttributeList('Settings', $field);
$isSet = false;
foreach ($attributeList as $attribute) {
if (property_exists($data, $attribute)) {
$isSet = true;
break;
}
}
return $isSet;
}
}

View File

@@ -276,6 +276,16 @@ class Stream extends \Espo\Core\Services\Base
return false;
}
$user = $this->getEntityManager()->getRepository('User')
->select(['id', 'type', 'isActive'])
->where([
'id' => $userId,
'isActive' => true,
])->findOne();
if (!$user) return false;
if (!$this->getAclManager()->check($user, $entity, 'stream')) return false;
$pdo = $this->getEntityManager()->getPDO();
if (!$this->checkIsFollowed($entity, $userId)) {
@@ -564,6 +574,7 @@ class Stream extends \Espo\Core\Services\Base
'orderBy' => 'number',
'order' => 'DESC',
'limit' => $sqLimit,
'_name' => 'selfPost',
];
$selectParamsList[] = [
@@ -593,6 +604,7 @@ class Stream extends \Espo\Core\Services\Base
'orderBy' => 'number',
'order' => 'DESC',
'limit' => $sqLimit,
'_name' => 'globalPost',
];
}
@@ -679,18 +691,22 @@ class Stream extends \Espo\Core\Services\Base
}
}
if ($skipOwn) {
$whereClause[] = [
'createdById!=' => $this->getUser()->id,
];
}
$sqlPartList = [];
foreach ($selectParamsList as $i => $selectParams) {
if (empty($selectParams['whereClause'])) {
$selectParams['whereClause'] = [];
}
$selectParams['whereClause'][] = $whereClause;
if ($skipOwn) {
$itemName = $selectParams['_name'] ?? null;
if ($itemName !== 'selfPost' && $itemName !== 'globalPost') {
$selectParams['whereClause'][] = [
'createdById!=' => $this->getUser()->id,
];
}
}
$sqlPartList[] = "(\n" . $this->getEntityManager()->getQuery()->createSelectQuery('Note', $selectParams) . "\n)";
}
@@ -1305,6 +1321,39 @@ class Stream extends \Espo\Core\Services\Base
$this->getEntityManager()->saveEntity($note, $o);
}
public function noteRelate(Entity $entity, $parentType, $parentId, array $options = [])
{
$entityType = $entity->getEntityType();
$existing = $this->getEntityManager()->getRepository('Note')->select(['id'])->where([
'type' => 'Relate',
'parentId' => $parentId,
'parentType' => $parentType,
'relatedId' => $entity->id,
'relatedType' => $entityType,
])->findOne();
if ($existing) return false;
$note = $this->getEntityManager()->getEntity('Note');
$note->set([
'type' => 'Relate',
'parentId' => $parentId,
'parentType' => $parentType,
'relatedType' => $entityType,
'relatedId' => $entity->id,
]);
$this->processNoteTeamsUsers($note, $entity);
$o = [];
if (!empty($options['createdById'])) {
$o['createdById'] = $options['createdById'];
}
$this->getEntityManager()->saveEntity($note, $o);
}
public function noteAssign(Entity $entity, array $options = [])
{
$note = $this->getEntityManager()->getEntity('Note');

View File

@@ -45,6 +45,7 @@ class UserSecurity extends \Espo\Core\Services\Base
$this->addDependency('metadata');
$this->addDependency('totp');
$this->addDependency('config');
$this->addDependency('container');
}
protected function getUser()
@@ -87,7 +88,7 @@ class UserSecurity extends \Espo\Core\Services\Base
if (!$password) throw new Forbidden('Passport required.');
if (!$this->getUser()->isAdmin() || $this->getUser()->id === $id) {
$this->checkPassport($id, $password);
$this->checkPassword($id, $password);
}
$userData = $this->getEntityManager()->getRepository('UserData')->getByUserId($id);
@@ -136,7 +137,7 @@ class UserSecurity extends \Espo\Core\Services\Base
if (!$password) throw new Forbidden('Passport required.');
if (!$this->getUser()->isAdmin() || $this->getUser()->id === $id) {
$this->checkPassport($id, $password);
$this->checkPassword($id, $password);
}
foreach (get_object_vars($data) as $attribute => $v) {
@@ -222,15 +223,21 @@ class UserSecurity extends \Espo\Core\Services\Base
];
}
protected function checkPassport(string $id, string $password)
protected function checkPassword(string $id, string $password)
{
$passwordHash = new \Espo\Core\Utils\PasswordHash($this->getConfig());
$method = $this->getConfig()->get('authenticationMethod', 'Espo');
$auth = $this->getInjection('container')->get('authenticationFactory')->create($method);
$user = $this->getEntityManager()->getRepository('User')->where([
'id' => $id,
'password' => $passwordHash->hash($password),
])->findOne();
if (!$user) {
throw new Forbidden('Passport is incorrect.');
if (!$user) throw new Forbidden('User is not found.');
if (!$auth->login($user->get('userName'), $password)) {
throw new Forbidden('Password is incorrect.');
}
return true;
}
}

View File

@@ -999,8 +999,16 @@ var Bull = Bull || {};
this.$el = $(el).eq(0);
this.el = this.$el[0];
}
},
propagateEvent: function () {
this.trigger.apply(this, arguments);
for (var key in this.nestedViews) {
var view = this.nestedViews[key];
view.propagateEvent.apply(view, arguments);
}
},
});
}).call(this, Bull, Backbone, _);

View File

@@ -77,7 +77,7 @@
<div class="row">
<div class="col-md-12">
<div class="pull-right">
<a target="_blank" href="https://www.espocrm.com/documentation/administration/server-configuration/" style="font-weight:bold;">Configuration Instructions</a>
<a target="_blank" href="https://www.espocrm.com/documentation/administration/server-configuration/" style="font-weight:600;">Configuration Instructions</a>
</div>
</div>
</div>

View File

@@ -1,11 +1,11 @@
<div class="row">
<div class="col-sm-3 col-xs-6">
<div class="col-sm-4 col-xs-6">
<select class="form-control" data-name="entityType"></select>
</div>
<div class="col-sm-3 col-xs-6">
<div class="col-sm-5 col-xs-6">
<select class="form-control" data-name="field"></select>
</div>
<div class="col-sm-4 col-xs-6">
<div class="col-sm-3 col-xs-6">
<button class="btn btn-default" type="button" data-action="insert">{{translate 'Insert'}}</button>
</div>
</div>

View File

@@ -1 +1,5 @@
<input type="checkbox"{{#ifEqual searchParams.type 'isTrue'}} checked{{/ifEqual}} data-name="{{name}}" class="main-element">
<select data-name="{{name}}" class="main-element form-control input-sm">
<option value="isTrue" {{#ifEqual searchType 'isTrue'}} selected{{/ifEqual}}>{{translate 'Yes'}}</option>
<option value="isFalse" {{#ifEqual searchType 'isFalse'}} selected{{/ifEqual}}>{{translate 'No'}}</option>
<option value="any" {{#ifEqual searchType 'any'}} selected{{/ifEqual}}>{{translateOption 'any' field='searchRanges'}}</option>
</select>

View File

@@ -26,13 +26,23 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('acl/user', 'acl', function (Dep) {
define('acl/user', 'acl', function (Dep) {
return Dep.extend({
checkModelRead: function (model, data, precise) {
if (model.isPortal()) {
if (this.get('portalPermission') === 'yes') {
return true;
}
}
return Dep.prototype.checkModelRead.call(this, model, data, precise);
},
checkIsOwner: function (model) {
return this.getUser().id === model.id;
}
},
});
});

View File

@@ -313,7 +313,7 @@ define(
controller.doAction(params.action, params.options);
this.trigger('action:done');
} catch (e) {
console.log(e);
console.error(e);
switch (e.name) {
case 'AccessDenied':
this.baseController.error403();

View File

@@ -157,7 +157,13 @@ define('controller', [], function () {
storeMainView: function (key, view) {
this.set('storedMainView-' + key, view);
view.once('remove', function () {
this.listenTo(view, 'remove', function (o) {
o = o || {};
if (o.ignoreCleaning) return;
this.stopListening(view, 'remove');
this.clearStoredMainView(key);
}, this);
},
@@ -278,6 +284,10 @@ define('controller', [], function () {
if (master.currentViewKey) {
this.set('storedScrollTop-' + master.currentViewKey, $(window).scrollTop());
if (this.hasStoredMainView(master.currentViewKey)) {
var mainView = master.getView('main');
if (mainView) {
mainView.propagateEvent('remove', {ignoreCleaning: true});
}
master.unchainView('main');
}
}

View File

@@ -57,7 +57,15 @@ define('controllers/portal-user', 'controllers/record', function (Dep) {
options.attributes = options.attributes || {};
options.attributes.type = 'portal';
Dep.prototype.actionCreate.call(this, options);
}
},
checkAccess: function (action) {
if (this.getAcl().get('portalPermission') === 'yes')
return true;
return false;
},
});
});

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