Compare commits

...

106 Commits
5.9.0 ... 5.9.4

Author SHA1 Message Date
Yuri Kuznetsov
235d8f2264 package lock version 2020-09-23 11:14:29 +03:00
Yuri Kuznetsov
9cfab5a7ea upgrade fix 2020-09-23 10:17:07 +03:00
Yuri Kuznetsov
86a85ba177 Merge branch 'hotfix/5.9.4' of github.com:espocrm/espocrm into hotfix/5.9.4 2020-09-16 13:45:25 +03:00
Yuri Kuznetsov
36a4cb2451 fix formula int parsing 2020-09-16 13:44:56 +03:00
Eymen Elkum
6f60a73c62 minor fix (#1785) 2020-09-15 14:33:53 +03:00
Yuri Kuznetsov
72fd9184f4 fix link manager ui 2020-09-08 17:42:19 +03:00
Yuri Kuznetsov
0a5ae621b4 fix complex text 2020-08-25 18:11:38 +03:00
Yuri Kuznetsov
4e588f6d0e cs fix 2020-08-25 18:08:06 +03:00
Yuri Kuznetsov
f60c5e29de Merge branch 'hotfix/5.9.4' of https://github.com/espocrm/espocrm into hotfix/5.9.4 2020-08-25 18:04:36 +03:00
Yuri Kuznetsov
1662cfd97d notification form fix 2020-08-25 18:01:15 +03:00
Yuri Kuznetsov
0fd8f9d29f email index change 2020-08-10 13:42:03 +03:00
Yuri Kuznetsov
bf033c31af date fix 2020-08-08 10:33:20 +03:00
Yuri Kuznetsov
aa788a17d9 fix date field issue 2020-08-07 18:00:17 +03:00
Yuri Kuznetsov
036ad99ba6 fix deprecation 2020-08-02 13:45:32 +03:00
Yuri Kuznetsov
7fda7abbe6 attachments fixes 2020-07-23 17:46:00 +03:00
Yuri Kuznetsov
fa0cf9c9fd cleanup 2020-07-18 15:02:25 +03:00
Yuri Kuznetsov
3c651f8633 lang fixes 2020-07-18 14:47:02 +03:00
Yuri Kuznetsov
2b5f1c28e8 lang.js refactoring 2020-07-18 14:41:48 +03:00
Yuri Kuznetsov
7fea881d17 refactor po 2020-07-18 14:21:05 +03:00
Yuri Kuznetsov
ada64bba0c fix dayOfWeek 2020-07-18 12:22:43 +03:00
Yuri Kuznetsov
8fe96b140a tcpdf header 2020-07-16 20:14:48 +03:00
Yuri Kuznetsov
3b25a1a001 error message 4 seconds 2020-07-16 19:59:46 +03:00
Yuri Kuznetsov
f4d98f177c fix typos 2020-07-16 19:57:04 +03:00
Yuri Kuznetsov
9f4f38bb0d webhook verify peer 2020-07-16 19:50:53 +03:00
Yuri Kuznetsov
e76490880a v 2020-07-13 10:48:13 +03:00
Yuri Kuznetsov
e12bb1320c fix cookies 2020-07-13 10:47:57 +03:00
Yuri Kuznetsov
c84abfb542 do not display auth error message 2020-07-08 16:15:30 +03:00
Yuri Kuznetsov
656f66f567 fix menu items 2020-07-08 16:07:27 +03:00
Yuri Kuznetsov
eca5bdea71 fix formula where 2020-07-01 08:38:14 +03:00
Yuri Kuznetsov
2ab4173e85 oauth client fix 2020-06-30 08:51:29 +03:00
Yuri Kuznetsov
195db973dc get image url use ssl verify peer 2020-06-30 08:50:09 +03:00
Yuri Kuznetsov
cd5537cc26 fix user generate new possword hidden if group email account used 2020-06-27 12:36:56 +03:00
Yuri Kuznetsov
94008f5a53 fix metadata service null acl check 2020-06-25 08:35:47 +03:00
Yuri Kuznetsov
bf410b2258 cleanup 2020-06-24 09:02:23 +03:00
Yuri Kuznetsov
0f2a26b744 formula comments fix 2020-06-22 21:26:51 +03:00
Yuri Kuznetsov
c11fd843d0 Merge branch 'hotfix/5.9.3' of github.com:espocrm/espocrm into hotfix/5.9.3 2020-06-22 13:03:18 +03:00
Yuri Kuznetsov
70a374004d fix navbar verical scroll issue 2020-06-22 13:02:08 +03:00
Yuri Kuznetsov
0b218833af fix q0 queue 2020-06-16 15:44:32 +03:00
Yuri Kuznetsov
4a1e5c974c fix import 2020-06-15 16:23:58 +03:00
Yuri Kuznetsov
b08083173a reminder 7 days 2020-06-15 11:07:26 +03:00
Yuri Kuznetsov
c24e7a6939 version 2020-06-15 10:47:00 +03:00
Yuri Kuznetsov
ce99671583 layout set error msg 2020-06-15 10:39:28 +03:00
Yuri Kuznetsov
d36e5cc0c1 fix select create related 2020-06-12 14:44:21 +03:00
Yuri Kuznetsov
13ae2d27c8 fix external account client manager 2020-06-12 13:43:47 +03:00
Yuri Kuznetsov
ea5c76f012 oauth changes 2020-06-11 12:13:39 +03:00
Yuri Kuznetsov
acb9b50d14 stream internal post button red color 2020-06-10 11:06:51 +03:00
Yuri Kuznetsov
968fc7ad30 fix email account form 2020-06-10 10:32:59 +03:00
Yuri Kuznetsov
bb6d7598a1 email account layout change 2020-06-10 10:27:42 +03:00
Yuri Kuznetsov
cd9481670a oauth fix 2020-06-09 15:33:42 +03:00
Yuri Kuznetsov
505e9e278b oauth race condition fix 2020-06-09 13:50:25 +03:00
Yuri Kuznetsov
7e59888fb2 oauth fix 2020-06-06 18:37:34 +03:00
Yuri Kuznetsov
74f2d7f1ea oauth: handle token expiration; factory in client manager 2020-06-06 18:12:39 +03:00
Yuri Kuznetsov
f319be61d2 fix select related 2020-06-06 10:04:02 +03:00
Yuri Kuznetsov
2cc4c8f974 fix mail sender 2020-06-04 14:49:50 +03:00
Yuri Kuznetsov
a3aa74013a oauth storing new refresh token 2020-06-03 19:26:57 +03:00
Yuri Kuznetsov
509b3affd1 email account: imap handler 2020-06-03 10:10:34 +03:00
Yuri Kuznetsov
ad9b2b54dd scheduled job log panel fix 2020-06-02 23:01:50 +03:00
Yuri Kuznetsov
247eb00963 fix field-manager.js 2020-06-02 20:07:49 +03:00
Yuri Kuznetsov
c0b59a49bf fix horizontal navbar 2020-06-02 20:06:18 +03:00
Yuri Kuznetsov
b525afe154 less re-grouping 2 2020-06-02 19:59:57 +03:00
Yuri Kuznetsov
b449263854 less re-grouping 2020-06-02 15:43:03 +03:00
Yuri Kuznetsov
12550c3d0a password recovery improvements 2020-05-31 18:50:54 +03:00
Yuri Kuznetsov
803a4ffb7f scheduling text fix 2020-05-28 13:41:11 +03:00
Yuri Kuznetsov
57bfea4d70 fix email body plain 2020-05-27 16:38:33 +03:00
Yuri Kuznetsov
86f460866a fix tests 2020-05-27 16:16:55 +03:00
Yuri Kuznetsov
8515157916 relationship panel: create from select modal 2020-05-27 12:01:14 +03:00
Yuri Kuznetsov
5bd18dee49 cs fix 2020-05-27 10:46:33 +03:00
Yuri Kuznetsov
88ea5e7d6c mail parser body plain change 2020-05-27 10:45:40 +03:00
Yuri Kuznetsov
3839335508 fix email body plain 2020-05-27 10:45:19 +03:00
Yuri Kuznetsov
b8e94b52aa detail layout panel label changes 2020-05-26 20:08:27 +03:00
Yuri Kuznetsov
eeda450405 pdf: ifMultipleOf 2020-05-26 13:20:22 +03:00
Yuri Kuznetsov
d051bd7df2 googleMapsImage helper name 2020-05-26 12:57:53 +03:00
Yuri Kuznetsov
690a265450 date-picker change 2020-05-26 10:36:12 +03:00
Yuri Kuznetsov
8bec0a3caf change 1 2020-05-25 16:04:53 +03:00
Yuri Kuznetsov
a89463367e v 2020-05-25 12:20:55 +03:00
Yuri Kuznetsov
05dbcbd917 field view fixes 2020-05-25 12:17:21 +03:00
Yuri Kuznetsov
774bde3e20 websocket update fixes 2020-05-25 11:46:51 +03:00
Yuri Kuznetsov
90589e0d26 scheduling color 2020-05-25 11:25:03 +03:00
Yuri Kuznetsov
be199235f1 Merge branch 'hotfix/5.9.2' of https://github.com/espocrm/espocrm into hotfix/5.9.2 2020-05-25 09:47:55 +03:00
Yuri Kuznetsov
1b2d67b027 massEmailSiteUrl admin only 2020-05-25 09:41:09 +03:00
Eymen Elkum
7bc56fc864 add missing import (#1720) 2020-05-24 11:10:23 +03:00
Yuri Kuznetsov
c706ddc809 person name field: fix typo 2020-05-22 21:55:56 +03:00
Yuri Kuznetsov
1b6b2ea140 pdf: google maps 2020-05-22 15:45:06 +03:00
Yuri Kuznetsov
c1db037fc2 htmlzier additions 2020-05-22 14:31:29 +03:00
Yuri Kuznetsov
838e9ea773 post preview: links in new tab 2020-05-22 13:41:46 +03:00
Yuri Kuznetsov
10efe3513c scheduled job: expression validation 2020-05-22 11:27:46 +03:00
Yuri Kuznetsov
6d301092b2 Merge branch 'hotfix/5.9.2' of https://github.com/espocrm/espocrm into hotfix/5.9.2 2020-05-22 11:13:14 +03:00
Yuri Kuznetsov
5acb5da8fa scheduled job: cron description 2020-05-22 11:12:30 +03:00
Eymen Elkum
3ad343b274 add missing use in settings.php service (#1711) 2020-05-22 09:45:34 +03:00
Yuri Kuznetsov
c5fd749b21 formula while 2020-05-21 14:35:06 +03:00
Yuri Kuznetsov
1f8e0f16c7 formula randomInt function 2020-05-21 13:37:17 +03:00
Yuri Kuznetsov
92babe7fbc fix gruntfile 2020-05-21 13:34:43 +03:00
Yuri Kuznetsov
fb684837f8 role table ui improvement 2020-05-21 11:40:59 +03:00
Yuri Kuznetsov
669413b184 mass email site url param 2020-05-20 16:44:10 +03:00
Yuri Kuznetsov
a945a01767 password request send support group email account smtp 2020-05-20 13:04:41 +03:00
Yuri Kuznetsov
b4664eafa5 fix password recovery disabled if smtpServer is empty 2020-05-20 12:53:07 +03:00
Yuri Kuznetsov
34b06b83aa Merge branch 'hotfix/5.9.1' of github.com:espocrm/espocrm into hotfix/5.9.1 2020-05-19 11:55:41 +03:00
Yuri Kuznetsov
da14681077 fix global search 2020-05-19 08:32:39 +03:00
Yuri Kuznetsov
53e91ad683 fix mail sender 2020-05-19 00:47:14 +03:00
Yuri Kuznetsov
06434bef99 fix panel container 2020-05-18 19:27:01 +03:00
Yuri Kuznetsov
16fab0ced0 v 2020-05-18 11:21:49 +03:00
Yuri Kuznetsov
bc3e531447 multi enum match any word prop 2020-05-18 11:19:40 +03:00
Yuri Kuznetsov
af038e3306 record controller forbidden messages 2020-05-18 11:08:46 +03:00
Yuri Kuznetsov
1a06b83d9d external account changes 2020-05-18 09:43:17 +03:00
Yuri Kuznetsov
bcc3cfd143 fix wrong response content type 2020-05-15 15:40:21 +03:00
Yuri Kuznetsov
b73123c137 fix array validation 2020-05-14 22:28:48 +03:00
115 changed files with 4553 additions and 2868 deletions

View File

@@ -313,11 +313,11 @@ module.exports = function (grunt) {
});
grunt.registerTask("unit-tests-run", function() {
cp.execSync("vendor/bin/phpunit --bootstrap=vendor/autoload.php tests/unit", {stdio: 'inherit'});
cp.execSync("vendor/bin/phpunit --bootstrap=./vendor/autoload.php tests/unit", {stdio: 'inherit'});
});
grunt.registerTask("integration-tests-run", function() {
cp.execSync("vendor/bin/phpunit --bootstrap=vendor/autoload.php tests/integration", {stdio: 'inherit'});
cp.execSync("vendor/bin/phpunit --bootstrap=./vendor/autoload.php tests/integration", {stdio: 'inherit'});
});
grunt.registerTask("zip", function() {

View File

@@ -31,6 +31,7 @@ namespace Espo\Controllers;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
class EmailAccount extends \Espo\Core\Controllers\Record
{

View File

@@ -93,12 +93,7 @@ class ExternalAccount extends \Espo\Core\Controllers\Record
{
list($integration, $userId) = explode('__', $params['id']);
if ($this->getUser()->id != $userId && !$this->getUser()->isAdmin()) {
throw new Forbidden();
}
$entity = $this->getEntityManager()->getEntity('ExternalAccount', $params['id']);
return $entity->toArray();
return $this->getRecordService()->read($params['id'])->getValueMap();
}
public function actionUpdate($params, $data, $request)

View File

@@ -69,29 +69,7 @@ class User extends \Espo\Core\Controllers\Record
throw new BadRequest();
}
if ($this->getConfig()->get('passwordRecoveryDisabled')) {
throw new Forbidden("Password recovery disabled");
}
$request = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where([
'requestId' => $data->requestId
])->findOne();
if (!$request) {
throw new Forbidden();
}
$userId = $request->get('userId');
if (!$userId) {
throw new Error();
}
if ($this->getService('User')->changePassword($userId, $data->password)) {
$this->getEntityManager()->removeEntity($request);
return [
'url' => $request->get('url')
];
}
return $this->getService('User')->changePasswordByRequest($data->requestId, $data->password);
}
public function postActionPasswordChangeRequest($params, $data, $request)

View File

@@ -260,6 +260,9 @@ class Application
$route = $slim->router()->getCurrentRoute();
$conditions = $route->getConditions();
$response = $slim->response();
$response->headers->set('Content-Type', 'application/json');
if (isset($conditions['useController']) && $conditions['useController'] == false) {
return;
}

View File

@@ -58,6 +58,8 @@ class SetPassword extends Base
$password = $this->ask();
$password = trim($password);
if (!$password) {
$this->out("Password can not be empty.\n");
die;

View File

@@ -140,8 +140,7 @@ class Upgrade extends Base
fwrite(\STDOUT, "\n");
$app = new \Espo\Core\Application();
$currentVerison = $app->getContainer()->get('config')->get('version');
$currentVerison = $this->getCurrentVersion();
fwrite(\STDOUT, "Upgrade is complete. Current version is {$currentVerison}.\n");
@@ -211,8 +210,7 @@ class Upgrade extends Base
return;
}
$app = new \Espo\Core\Application();
$currentVerison = $app->getContainer()->get('config')->get('version');
$currentVerison = $this->getCurrentVersion();
fwrite(\STDOUT, "Upgrade is complete. Current version is {$currentVerison}.\n");
@@ -406,4 +404,15 @@ class Upgrade extends Base
return true;
}
protected function getCurrentVersion()
{
$configData = include "data/config.php";
if (!$configData) {
return null;
}
return $configData['version'] ?? null;
}
}

View File

@@ -29,11 +29,12 @@
namespace Espo\Core\Controllers;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\NotFound;
use \Espo\Core\Exceptions\BadRequest;
use \Espo\Core\Utils\Util;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Utils\Util;
use Espo\Core\Exceptions\ForbiddenSilent;
class Record extends Base
{
@@ -88,7 +89,7 @@ class Record extends Base
}
if (!$this->getAcl()->check($this->name, 'create')) {
throw new Forbidden();
throw new Forbidden("No create access for {$this->name}.");
}
$service = $this->getRecordService();
@@ -109,7 +110,7 @@ class Record extends Base
}
if (!$this->getAcl()->check($this->name, 'edit')) {
throw new Forbidden();
throw new Forbidden("No edit access for {$this->name}.");
}
$id = $params['id'];
@@ -124,7 +125,7 @@ class Record extends Base
public function actionList($params, $data, $request)
{
if (!$this->getAcl()->check($this->name, 'read')) {
throw new Forbidden();
throw new Forbidden("No read access for {$this->name}.");
}
$params = [];
@@ -156,7 +157,7 @@ class Record extends Base
public function getActionListKanban($params, $data, $request)
{
if (!$this->getAcl()->check($this->name, 'read')) {
throw new Forbidden();
throw new Forbidden("No read access for {$this->name}.");
}
$params = [];
@@ -286,14 +287,14 @@ class Record extends Base
}
if (!$this->getAcl()->check($this->name, 'edit')) {
throw new Forbidden();
throw new Forbidden("No edit access for {$this->name}.");
}
if (empty($data->attributes)) {
throw new BadRequest();
}
if ($this->getAcl()->get('massUpdatePermission') !== 'yes') {
throw new Forbidden();
throw new Forbidden("No massUpdatePermission.");
}
$actionParams = $this->getMassActionParamsFromData($data);
@@ -313,7 +314,7 @@ class Record extends Base
if (array_key_exists('where', $actionParams)) {
if ($this->getAcl()->get('massUpdatePermission') !== 'yes') {
throw new Forbidden();
throw new Forbidden("No massUpdatePermission.");
}
}
@@ -412,7 +413,7 @@ class Record extends Base
throw new BadRequest();
}
if (!$this->getAcl()->check($this->name, 'stream')) {
throw new Forbidden();
throw new Forbidden("No stream access for {$this->name}.");
}
$id = $params['id'];
return $this->getRecordService()->follow($id);
@@ -424,7 +425,7 @@ class Record extends Base
throw new BadRequest();
}
if (!$this->getAcl()->check($this->name, 'read')) {
throw new Forbidden();
throw new Forbidden("No read access for {$this->name}.");
}
$id = $params['id'];
return $this->getRecordService()->unfollow($id);
@@ -444,7 +445,7 @@ class Record extends Base
$attributes = $data->attributes;
if (!$this->getAcl()->check($this->name, 'edit')) {
throw new Forbidden();
throw new Forbidden("No edit access for {$this->name}.");
}
return $this->getRecordService()->merge($targetId, $sourceIds, $attributes);
@@ -480,7 +481,7 @@ class Record extends Base
public function postActionMassUnfollow($params, $data, $request)
{
if (!$this->getAcl()->check($this->name, 'stream')) {
throw new Forbidden();
throw new Forbidden("No stream access for {$this->name}.");
}
$actionParams = $this->getMassActionParamsFromData($data);

View File

@@ -29,9 +29,11 @@
namespace Espo\Core\ExternalAccount;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\InjectableFactory;
class ClientManager
{
@@ -39,13 +41,17 @@ class ClientManager
protected $metadata;
protected $clientMap = array();
protected $clientMap = [];
public function __construct($entityManager, $metadata, $config)
protected $injectableFactory = null;
public function __construct(
$entityManager, $metadata, $config, ?InjectableFactory $injectableFactory = null)
{
$this->entityManager = $entityManager;
$this->metadata = $metadata;
$this->config = $config;
$this->injectableFactory = $injectableFactory;
}
protected function getMetadata()
@@ -69,24 +75,68 @@ class ClientManager
$externalAccountEntity = $this->clientMap[$hash]['externalAccountEntity'];
$externalAccountEntity->set('accessToken', $data['accessToken']);
$externalAccountEntity->set('tokenType', $data['tokenType']);
$externalAccountEntity->set('expiresAt', $data['expiresAt'] ?? null);
if ($data['refreshToken'] ?? null) {
$externalAccountEntity->set('refreshToken', $data['refreshToken']);
}
$copy = $this->getEntityManager()->getEntity('ExternalAccount', $externalAccountEntity->id);
if ($copy) {
if (!$copy->get('enabled')) {
throw new Error("External Account Client Manager: Account got disabled.");
}
$copy->set('accessToken', $data['accessToken']);
$copy->set('tokenType', $data['tokenType']);
$this->getEntityManager()->saveEntity($copy, ['isTokenRenewal' => true]);
$copy->set('expiresAt', $data['expiresAt'] ?? null);
if ($data['refreshToken'] ?? null) {
$copy->set('refreshToken', $data['refreshToken'] ?? null);
}
$this->getEntityManager()->saveEntity($copy, ['isTokenRenewal' => true, 'skipHooks' => true]);
}
}
}
public function create($integration, $userId)
public function create(string $integration, string $userId)
{
$authMethod = $this->getMetadata()->get("integrations.{$integration}.authMethod");
$methodName = 'create' . ucfirst($authMethod);
return $this->$methodName($integration, $userId);
if (method_exists($this, $methodName)) {
return $this->$methodName($integration, $userId);
}
if (!$this->injectableFactory) {
throw new Error();
}
$integrationEntity = $this->getEntityManager()->getEntity('Integration', $integration);
$externalAccountEntity = $this->getEntityManager()->getEntity('ExternalAccount', $integration . '__' . $userId);
if (!$externalAccountEntity) {
throw new Error("External Account {$integration} not found for {$userId}");
}
if (!$integrationEntity->get('enabled')) return null;
if (!$externalAccountEntity->get('enabled')) return null;
$className = $this->getMetadata()->get("integrations.{$integration}.clientClassName");
$client = $this->injectableFactory->createByClassName($className);
$client->setup(
$userId,
$integrationEntity,
$externalAccountEntity,
$this
);
$this->addToClientMap($client, $integrationEntity, $externalAccountEntity, $userId);
return $client;
}
protected function createOAuth2($integration, $userId)
protected function createOAuth2(string $integration, string $userId)
{
$integrationEntity = $this->getEntityManager()->getEntity('Integration', $integration);
$externalAccountEntity = $this->getEntityManager()->getEntity('ExternalAccount', $integration . '__' . $userId);
@@ -113,7 +163,7 @@ class ClientManager
$oauth2Client = new \Espo\Core\ExternalAccount\OAuth2\Client();
$client = new $className($oauth2Client, array(
$params = [
'endpoint' => $this->getMetadata()->get("integrations.{$integration}.params.endpoint"),
'tokenEndpoint' => $this->getMetadata()->get("integrations.{$integration}.params.tokenEndpoint"),
'clientId' => $integrationEntity->get('clientId'),
@@ -122,7 +172,16 @@ class ClientManager
'accessToken' => $externalAccountEntity->get('accessToken'),
'refreshToken' => $externalAccountEntity->get('refreshToken'),
'tokenType' => $externalAccountEntity->get('tokenType'),
), $this);
'expiresAt' => $externalAccountEntity->get('expiresAt'),
];
foreach (get_object_vars($integrationEntity->getValueMap()) as $k => $v) {
if (array_key_exists($k, $params)) continue;
if ($integrationEntity->hasAttribute($k)) continue;
$params[$k] = $v;
}
$client = new $className($oauth2Client, $params, $this);
$this->addToClientMap($client, $integrationEntity, $externalAccountEntity, $userId);
@@ -139,5 +198,82 @@ class ClientManager
'externalAccountEntity' => $externalAccountEntity,
);
}
}
protected function getClientRecord($client) : \Espo\ORM\Entity
{
$data = $this->clientMap[spl_object_hash($client)];
if (!$data) {
throw new Error("External Account Client Manager: Client not found in hash.");
}
return $data['externalAccountEntity'];
}
public function isClientLocked($client) : bool
{
$externalAccountEntity = $this->getClientRecord($client);
$id = $externalAccountEntity->id;
$e = $this->getEntityManager()->getRepository('ExternalAccount')
->select(['id', 'isLocked'])->where(['id' => $id])->findOne();
if (!$e) {
throw new Error("External Account Client Manager: Client {$id} not found in DB.");
}
return $e->get('isLocked');
}
public function lockClient($client)
{
$externalAccountEntity = $this->getClientRecord($client);
$id = $externalAccountEntity->id;
$e = $this->getEntityManager()->getRepository('ExternalAccount')
->select(['id', 'isLocked'])->where(['id' => $id])->findOne();
if (!$e) {
throw new Error("External Account Client Manager: Client {$id} not found in DB.");
}
$e->set('isLocked', true);
$this->getEntityManager()->saveEntity($e, ['skipHooks' => true, 'silent' => true]);
}
public function unlockClient($client)
{
$externalAccountEntity = $this->getClientRecord($client);
$id = $externalAccountEntity->id;
$e = $this->getEntityManager()->getRepository('ExternalAccount')
->select(['id', 'isLocked'])->where(['id' => $id])->findOne();
if (!$e) {
throw new Error("External Account Client Manager: Client {$id} not found in DB.");
}
$e->set('isLocked', false);
$this->getEntityManager()->saveEntity($e, ['skipHooks' => true, 'silent' => true]);
}
public function reFetchClient($client)
{
$externalAccountEntity = $this->getClientRecord($client);
$id = $externalAccountEntity->id;
$e = $this->getEntityManager()->getEntity('ExternalAccount', $id);
if (!$e) {
throw new Error("External Account Client Manager: Client {$id} not found in DB.");
}
$data = $e->getValueMap();
$externalAccountEntity->set($data);
$client->setParams(get_object_vars($data));
}
}

View File

@@ -29,9 +29,9 @@
namespace Espo\Core\ExternalAccount\Clients;
use \Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Error;
use \Espo\Core\ExternalAccount\OAuth2\Client;
use Espo\Core\ExternalAccount\OAuth2\Client;
abstract class OAuth2Abstract implements IClient
{
@@ -39,7 +39,7 @@ abstract class OAuth2Abstract implements IClient
protected $manager = null;
protected $paramList = array(
protected $paramList = [
'endpoint',
'tokenEndpoint',
'clientId',
@@ -48,7 +48,8 @@ abstract class OAuth2Abstract implements IClient
'accessToken',
'refreshToken',
'redirectUri',
);
'expiresAt',
];
protected $clientId = null;
@@ -60,7 +61,15 @@ abstract class OAuth2Abstract implements IClient
protected $redirectUri = null;
public function __construct($client, array $params = array(), $manager = null)
protected $expiresAt = null;
const ACCESS_TOKEN_EXPIRATION_MARGIN = '20 seconds';
const LOCK_TIMEOUT = 5;
const LOCK_CHECK_STEP = 0.5;
public function __construct($client, array $params = [], $manager = null)
{
$this->client = $client;
@@ -90,7 +99,7 @@ abstract class OAuth2Abstract implements IClient
public function setParams(array $params)
{
foreach ($this->paramList as $name) {
if (!empty($params[$name])) {
if (array_key_exists($name, $params)) {
$this->setParam($name, $params[$name]);
}
}
@@ -103,6 +112,28 @@ abstract class OAuth2Abstract implements IClient
}
}
protected function getAccessTokenDataFromResponseResult($result)
{
$data = [];
$data['accessToken'] = $result['access_token'];
$data['tokenType'] = $result['token_type'];
$data['expiresAt'] = null;
if (isset($result['refresh_token']) && $result['refresh_token'] !== $this->refreshToken) {
$data['refreshToken'] = $result['refresh_token'];
}
if (isset($result['expires_in']) && is_numeric($result['expires_in'])) {
$data['expiresAt'] = (new \DateTime())
->modify('+' . $result['expires_in'] . ' seconds')
->format('Y-m-d H:i:s');
}
return $data;
}
public function getAccessTokenFromAuthorizationCode($code)
{
$r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_AUTHORIZATION_CODE, [
@@ -111,15 +142,16 @@ abstract class OAuth2Abstract implements IClient
]);
if ($r['code'] == 200) {
$data = [];
if (!empty($r['result'])) {
$data['accessToken'] = $r['result']['access_token'];
$data['tokenType'] = $r['result']['token_type'];
$data = $this->getAccessTokenDataFromResponseResult($r['result']);
$data['refreshToken'] = $r['result']['refresh_token'];
return $data;
} else {
$GLOBALS['log']->debug("OAuth getAccessTokenFromAuthorizationCode; Response: " . json_encode($r));
return null;
}
return $data;
} else {
$GLOBALS['log']->debug("OAuth getAccessTokenFromAuthorizationCode; Response: " . json_encode($r));
}
@@ -144,9 +176,69 @@ abstract class OAuth2Abstract implements IClient
}
}
public function handleAccessTokenActuality()
{
if ($this->getParam('expiresAt')) {
try {
$dt = new \DateTime($this->getParam('expiresAt'));
$dt->modify('-' . $this::ACCESS_TOKEN_EXPIRATION_MARGIN);
} catch (\Exception $e) {
return;
}
if ($dt->format('U') <= (new \DateTime())->format('U')) {
$GLOBALS['log']->debug("Oauth: Refreshing expired token for client {$this->clientId}.");
$until = microtime(true) + $this::LOCK_TIMEOUT;
if ($this->isLocked()) {
while (true) {
usleep($this::LOCK_CHECK_STEP * 1000000);
if (!$this->isLocked()) {
$GLOBALS['log']->debug("Oauth: Waited until unlocked for client {$this->clientId}.");
$this->reFetch();
return;
}
if (microtime(true) > $until) {
$GLOBALS['log']->debug("Oauth: Waited until unlocked but timed out for client {$this->clientId}.");
$this->unlock();
break;
}
}
}
$this->refreshToken();
}
}
}
protected function isLocked()
{
return $this->manager->isClientLocked($this);
}
protected function lock()
{
$this->manager->lockClient($this);
}
protected function unlock()
{
$this->manager->unlockClient($this);
}
protected function reFetch()
{
$this->manager->reFetchClient($this);
}
public function request($url, $params = null, $httpMethod = Client::HTTP_METHOD_GET, $contentType = null, $allowRenew = true)
{
$httpHeaders = array();
$this->handleAccessTokenActuality();
$httpHeaders = [];
if (!empty($contentType)) {
$httpHeaders['Content-Type'] = $contentType;
switch ($contentType) {
@@ -192,24 +284,43 @@ abstract class OAuth2Abstract implements IClient
protected function refreshToken()
{
if (!empty($this->refreshToken)) {
$r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_REFRESH_TOKEN, array(
'refresh_token' => $this->refreshToken,
));
if ($r['code'] == 200) {
if (is_array($r['result'])) {
if (!empty($r['result']['access_token'])) {
$data = array();
$data['accessToken'] = $r['result']['access_token'];
$data['tokenType'] = $r['result']['token_type'];
if (empty($this->refreshToken)) {
throw new Error(
"Oauth: Could not refresh token for client {$this->clientId}, because refreshToken is empty."
);
}
$this->setParams($data);
$this->afterTokenRefreshed($data);
return true;
}
$this->lock();
try {
$r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_REFRESH_TOKEN, [
'refresh_token' => $this->refreshToken,
]);
} catch (\Exception $e) {
$this->unlock();
throw new Error("Oauth: Error while refreshing token: " . $e->getMessage());
}
if ($r['code'] == 200) {
if (is_array($r['result'])) {
if (!empty($r['result']['access_token'])) {
$data = $this->getAccessTokenDataFromResponseResult($r['result']);
$this->setParams($data);
$this->afterTokenRefreshed($data);
$this->unlock();
return true;
}
}
}
$this->unlock();
$GLOBALS['log']->error("Oauth: Refreshing token failed for client {$this->clientId}: " . json_encode($r));
return false;
}
protected function handleErrorResponse($r)
@@ -217,21 +328,20 @@ abstract class OAuth2Abstract implements IClient
if ($r['code'] == 401 && !empty($r['result'])) {
$result = $r['result'];
if (strpos($r['header'], 'error=invalid_token') !== false) {
return array(
return [
'action' => 'refreshToken'
);
];
} else {
return array(
return [
'action' => 'renew'
);
];
}
} else if ($r['code'] == 400 && !empty($r['result'])) {
if ($r['result']['error'] == 'invalid_token') {
return array(
return [
'action' => 'refreshToken'
);
];
}
}
}
}

View File

@@ -62,6 +62,8 @@ class Client
protected $accessToken = null;
protected $expiresAt = null;
protected $authType = self::AUTH_TYPE_URI;
protected $tokenType = self::TOKEN_TYPE_URI;
@@ -72,9 +74,9 @@ class Client
protected $certificateFile = null;
protected $curlOptions = array();
protected $curlOptions = [];
public function __construct(array $params = array())
public function __construct(array $params = [])
{
if (!extension_loaded('curl')) {
throw new \Exception('CURL extension not found.');
@@ -121,12 +123,17 @@ class Client
$this->tokenType = $tokenType;
}
public function setExpiresAt($value)
{
$this->expiresAt = $value;
}
public function setAccessTokenSecret($accessTokenSecret)
{
$this->accessTokenSecret = $accessTokenSecret;
}
public function request($url, $params = null, $httpMethod = self::HTTP_METHOD_GET, array $httpHeaders = array())
public function request($url, $params = null, $httpMethod = self::HTTP_METHOD_GET, array $httpHeaders = [])
{
if ($this->accessToken) {
switch ($this->tokenType) {
@@ -148,7 +155,7 @@ class Client
return $this->execute($url, $params, $httpMethod, $httpHeaders);
}
private function execute($url, $params = null, $httpMethod, array $httpHeaders = array())
private function execute($url, $params = null, $httpMethod, array $httpHeaders = [])
{
$curlOptions = array(
CURLOPT_RETURNTRANSFER => true,
@@ -200,13 +207,11 @@ class Client
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
if (!empty($this->certificateFile)) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_CAINFO, $this->certificateFile);
} else {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
}
if (!empty($this->curlOptions)) {
@@ -243,7 +248,7 @@ class Client
{
$params['grant_type'] = $grantType;
$httpHeaders = array();
$httpHeaders = [];
switch ($this->tokenType) {
case self::AUTH_TYPE_URI:
case self::AUTH_TYPE_FORM:
@@ -261,4 +266,3 @@ class Client
return $this->execute($url, $params, self::HTTP_METHOD_POST, $httpHeaders);
}
}

View File

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

View File

@@ -122,4 +122,9 @@ abstract class Base implements Injectable
return $eArgs;
}
}
protected function fetchRawArguments(\StdClass $item)
{
return $item->value ?? [];
}
}

View File

@@ -62,9 +62,9 @@ class DayOfWeekType extends \Espo\Core\Formula\Functions\Base
if (empty($value)) return -1;
if (strlen($value) > 11) {
$resultString = $this->getInjection('dateTime')->convertSystemDateTime($value, $timezone, 'e');
$resultString = $this->getInjection('dateTime')->convertSystemDateTime($value, $timezone, 'd');
} else {
$resultString = $this->getInjection('dateTime')->convertSystemDate($value, 'e');
$resultString = $this->getInjection('dateTime')->convertSystemDate($value, 'd');
}
$result = intval($resultString);

View File

@@ -0,0 +1,47 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2020 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\NumberGroup;
use Espo\Core\Exceptions\Error;
class RandomIntType extends \Espo\Core\Formula\Functions\Base
{
public function process(\StdClass $item)
{
$args = $this->fetchArguments($item);
$min = $args[0] ?? 0;
$max = $args[1] ?? PHP_INT_MAX;
if (!is_int($min) || !is_int($max) ) throw new Error("Non-integer arguments passed to function number\\randomInt.");
return random_int($min, $max);
}
}

View File

@@ -82,7 +82,7 @@ class CountType extends \Espo\Core\Formula\Functions\Base
while ($i < count($item->value) - 1) {
$key = $this->evaluate($item->value[$i]);
$value = $this->evaluate($item->value[$i + 1]);
$whereClause[$key] = $value;
$whereClause[] = [$key => $value];
$i = $i + 2;
}

View File

@@ -61,7 +61,7 @@ class ExistsType extends \Espo\Core\Formula\Functions\Base
while ($i < count($item->value) - 1) {
$key = $this->evaluate($item->value[$i]);
$value = $this->evaluate($item->value[$i + 1]);
$whereClause[$key] = $value;
$whereClause[] = [$key => $value];
$i = $i + 2;
}

View File

@@ -0,0 +1,48 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2020 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;
use Espo\Core\Exceptions\Error;
class WhileType extends Base
{
public function process(\StdClass $item)
{
$args = $this->fetchRawArguments($item);
if (count($args) < 2) {
throw new Error("Function \'while\' should receieve 2 arguments.");
}
while ($this->evaluate($args[0])) {
$this->evaluate($args[1]);
}
}
}

View File

@@ -109,6 +109,8 @@ class Parser
{
$isString = false;
$isSingleQuote = false;
$isComment = false;
$isLineComment = false;
$modifiedString = $string;
@@ -116,27 +118,31 @@ class Parser
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;
if (!$isLineComment && !$isComment) {
if ($string[$i] === "'" && ($i === 0 || $string[$i - 1] !== "\\")) {
if (!$isString) {
$isString = true;
$isSingleQuote = true;
$isStringStart = true;
} else {
if ($isSingleQuote) {
$isString = false;
}
}
}
} else if ($string[$i] === "\"" && ($i === 0 || $string[$i - 1] !== "\\")) {
if (!$isString) {
$isString = true;
$isStringStart = true;
$isSingleQuote = false;
} else {
if (!$isSingleQuote) {
$isString = false;
} else if ($string[$i] === "\"" && ($i === 0 || $string[$i - 1] !== "\\")) {
if (!$isString) {
$isString = true;
$isStringStart = true;
$isSingleQuote = false;
} else {
if (!$isSingleQuote) {
$isString = false;
}
}
}
}
if ($isString) {
if ($string[$i] === '(' || $string[$i] === ')') {
$modifiedString[$i] = '_';
@@ -144,24 +150,51 @@ class Parser
$modifiedString[$i] = ' ';
}
} else {
if ($string[$i] === '(') {
$braceCounter++;
}
if ($string[$i] === ')') {
$braceCounter--;
}
if (!$isLineComment && !$isComment) {
if ($braceCounter === 0) {
if (!is_null($splitterIndexList)) {
if ($string[$i] === ';') {
$splitterIndexList[] = $i;
if (!$isComment) {
if ($i && $string[$i] === '/' && $string[$i - 1] === '/') {
$isLineComment = true;
}
}
if ($intoOneLine) {
if ($string[$i] === "\r" || $string[$i] === "\n" || $string[$i] === "\t") {
$string[$i] = ' ';
if (!$isLineComment) {
if ($i && $string[$i] === '*' && $string[$i - 1] === '/') {
$isComment = true;
}
}
if ($string[$i] === '(') {
$braceCounter++;
}
if ($string[$i] === ')') {
$braceCounter--;
}
if ($braceCounter === 0) {
if (!is_null($splitterIndexList)) {
if ($string[$i] === ';') {
$splitterIndexList[] = $i;
}
}
if ($intoOneLine) {
if ($string[$i] === "\r" || $string[$i] === "\n" || $string[$i] === "\t") {
$string[$i] = ' ';
}
}
}
}
if ($isLineComment) {
if ($string[$i] === "\n") {
$isLineComment = false;
}
}
if ($isComment) {
if ($string[$i - 1] === "*" && $string[$i] === "/") {
$isComment = false;
}
}
}
}
@@ -367,9 +400,13 @@ class Parser
}
if (is_numeric($expression)) {
$value = filter_var($expression, FILTER_VALIDATE_INT) !== false ?
(int) $expression :
(float) $expression;
return (object) [
'type' => 'value',
'value' => ($expression == (int) $expression) ? (int) $expression : (float) $expression
'value' => $value,
];
}

View File

@@ -451,6 +451,16 @@ class Htmlizer
return $context['inverse'] ? $context['inverse']() : '';
}
},
'ifMultipleOf' => function () {
$args = func_get_args();
$context = $args[count($args) - 1];
if ($args[0] % $args[1] === 0) {
return $context['fn']();
} else {
return $context['inverse'] ? $context['inverse']() : '';
}
},
'tableTag' => function () {
$args = func_get_args();
$context = $args[count($args) - 1];
@@ -567,11 +577,13 @@ class Htmlizer
$data[$k] = $value;
}
$data['__config'] = $this->config;
$data['__dateTime'] = $this->dateTime;
$data['__metadata'] = $this->metadata;
$data['__entityManager'] = $this->entityManager;
$data['__language'] = $this->language;
$data['__serviceFactory'] = $this->serviceFactory;
$data['__entityType'] = $entity->getEntityType();
$html = $renderer($data);

View File

@@ -181,7 +181,9 @@ class MailMimeParser
if ($bodyHtml) {
$email->set('isHtml', true);
$email->set('body', $bodyHtml);
$email->set('bodyPlain', $bodyPlain);
if ($bodyPlain) {
$email->set('bodyPlain', $bodyPlain);
}
} else {
$email->set('isHtml', false);
$email->set('body', $bodyPlain);

View File

@@ -62,6 +62,8 @@ class Sender
private $systemInboundEmailIsCached = false;
private $envelope = null;
public function __construct($config, $entityManager, $serviceFactory = null)
{
$this->config = $config;
@@ -84,6 +86,7 @@ class Sender
public function resetParams() : self
{
$this->params = [];
$this->envelope = null;
return $this;
}
@@ -132,7 +135,7 @@ class Sender
if ($params['auth'] ?? false) {
$authMechanism = $params['authMechanism'] ?? $params['smtpAuthMechanism'] ?? null;
if ($authMechanism) {
$authMechanism = preg_replace("([\.]{2,})", '', $params['authMechanism']);
$authMechanism = preg_replace("([\.]{2,})", '', $authMechanism);
if (in_array($authMechanism, ['login', 'crammd5', 'plain'])) {
$options['connectionClass'] = $authMechanism;
} else {
@@ -163,6 +166,10 @@ class Sender
$smtpOptions = new SmtpOptions($options);
$this->transport->setOptions($smtpOptions);
if ($this->envelope) {
$this->transport->setEnvelope($this->envelope);
}
}
protected function applyGlobal()
@@ -192,6 +199,14 @@ class Sender
]);
}
public function hasSystemSmtp()
{
if ($this->config->get('smtpServer')) return true;
if ($this->getSystemInboundEmail()) return true;
return false;
}
protected function getSystemInboundEmail()
{
$address = $this->config->get('outboundEmailFromAddress');
@@ -457,10 +472,12 @@ class Sender
$email->set('status', 'Sent');
$email->set('dateSent', date("Y-m-d H:i:s"));
} catch (\Exception $e) {
$this->resetParams();
$this->useGlobal();
throw new Error($e->getMessage(), 500);
}
$this->resetParams();
$this->useGlobal();
}
@@ -482,7 +499,8 @@ class Sender
public function setEnvelopeOptions(array $options) : self
{
$this->transport->setEnvelope(new Envelope($options));
$this->envelope = new Envelope($options);
return $this;
}
}

View File

@@ -0,0 +1,301 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2020 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\Password;
use Espo\Core\Utils\Util;
use Espo\Entities\User;
use Espo\Entities\PasswordChangeRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Error;
class Recovery implements \Espo\Core\Interfaces\Injectable
{
use \Espo\Core\Traits\Injectable;
const REQUEST_DELAY = 3000; //ms
const REQUEST_LIFETIME = '3 hours';
public function __construct()
{
$this->addDependencyList([
'entityManager',
'config',
'mailSender',
'htmlizerFactory',
'templateFileManager',
]);
}
public function getRequest(string $id) : PasswordChangeRequest
{
$config = $this->getInjection('config');
$em = $this->getInjection('entityManager');
if ($config->get('passwordRecoveryDisabled')) {
throw new Forbidden("Password recovery: Disabled.");
}
$request = $em->getRepository('PasswordChangeRequest')->where([
'requestId' => $id,
])->findOne();
if (!$request) {
throw new NotFound("Password recovery: Request not found by id.");
}
$userId = $request->get('userId');
if (!$userId) {
throw new Error();
}
return $request;
}
public function removeRequest(string $id)
{
$em = $this->getInjection('entityManager');
$request = $em->getRepository('PasswordChangeRequest')->where([
'requestId' => $id,
])->findOne();
if ($request) {
$em->removeEntity($request);
}
}
public function request(string $emailAddress, ?string $userName = null, ?string $url) : bool
{
$config = $this->getInjection('config');
$em = $this->getInjection('entityManager');
$noExposure = $config->get('passwordRecoveryNoExposure') ?? false;
if ($config->get('passwordRecoveryDisabled')) {
throw new Forbidden("Password recovery: Disabled.");
}
$user = $em->getRepository('User')->where([
'userName' => $userName,
'emailAddress' => $emailAddress,
])->findOne();
if (!$user) {
$this->fail("Password recovery: User {$emailAddress} not found.", 404);
return false;
}
if (!$user->isActive()) {
$this->fail("Password recovery: User {$user->id} is not active.");
return false;
}
if ($user->isApi() || $user->isSystem() || $user->isSuperAdmin()) {
$this->fail("Password recovery: User {$user->id} is not allowed.");
return false;
}
if ($config->get('passwordRecoveryForInternalUsersDisabled')) {
if ($user->isRegular() || $user->isAdmin()) {
$this->fail("Password recovery: User {$user->id} is not allowed, disabled for internal users.");
return false;
}
}
if ($config->get('passwordRecoveryForAdminDisabled')) {
if ($user->isAdmin()) {
$this->fail("Password recovery: User {$user->id} is not allowed, disabled for admin users.");
return false;
}
}
if (!$user->isAdmin() && $config->get('authenticationMethod', 'Espo') !== 'Espo') {
$this->fail("Password recovery: User {$user->id} is not allowed, authentication method is not 'Espo'.");
return false;
}
$passwordChangeRequest = $em->getRepository('PasswordChangeRequest')->where([
'userId' => $user->id,
])->findOne();
if ($passwordChangeRequest) {
if (!$noExposure) {
throw new Forbidden(json_encode(['reason' => 'Already-Sent']));
}
$this->fail("Password recovery: Denied for {$user->id}, already sent.");
return false;
}
$requestId = Util::generateCryptId();
$passwordChangeRequest = $em->getEntity('PasswordChangeRequest');
$passwordChangeRequest->set([
'userId' => $user->id,
'requestId' => $requestId,
'url' => $url,
]);
$microtime = microtime(true);
$this->send($requestId, $emailAddress, $user);
$em->saveEntity($passwordChangeRequest);
if (!$passwordChangeRequest->id) throw new Error();
$lifetime = $config->get('passwordRecoveryRequestLifetime') ?? self::REQUEST_LIFETIME;
$dt = new \DateTime();
$dt->modify('+' . $lifetime);
$em->createEntity('Job', [
'serviceName' => 'User',
'methodName' => 'removeChangePasswordRequestJob',
'data' => ['id' => $passwordChangeRequest->id],
'executeTime' => $dt->format('Y-m-d H:i:s'),
'queue' => 'q1',
]);
$timeDiff = $this->getDelay() - floor((microtime(true) - $microtime) / 1000);
if ($noExposure && $timeDiff > 0) {
$this->delay($timeDiff);
}
return true;
}
private function getDelay()
{
return $this->getInjection('config')->get('passwordRecoveryRequestDelay') ?? self::REQUEST_DELAY;
}
protected function delay(?int $delay = null)
{
$delay = $delay ?? $this->getDelay();
usleep($delay * 1000);
}
protected function send(string $requestId, string $emailAddress, User $user)
{
$config = $this->getInjection('config');
$em = $this->getInjection('entityManager');
$mailSender = $this->getInjection('mailSender');
$htmlizerFactory = $this->getInjection('htmlizerFactory');
$templateFileManager = $this->getInjection('templateFileManager');
if (!$emailAddress) return;
$email = $em->getEntity('Email');
if (!$mailSender->hasSystemSmtp() && !$config->get('internalSmtpServer')) {
throw new Error("Password recovery: SMTP credentials are not defined.");
}
$subjectTpl = $templateFileManager->getTemplate('passwordChangeLink', 'subject', 'User');
$bodyTpl = $templateFileManager->getTemplate('passwordChangeLink', 'body', 'User');
$siteUrl = $config->getSiteUrl();
if ($user->isPortal()) {
$portal = $em->getRepository('Portal')->distinct()->join('users')->where([
'isActive' => true,
'users.id' => $user->id,
])->findOne();
if ($portal) {
if ($portal->get('customUrl')) {
$siteUrl = $portal->get('customUrl');
}
}
}
$data = [];
$link = $siteUrl . '?entryPoint=changePassword&id=' . $requestId;
$data['link'] = $link;
$htmlizer = $htmlizerFactory->create(true);
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
$email->set([
'subject' => $subject,
'body' => $body,
'to' => $emailAddress,
'isSystem' => true,
]);
if ($mailSender->hasSystemSmtp()) {
$mailSender->useGlobal();
} else {
$mailSender->useSmtp([
'server' => $config->get('internalSmtpServer'),
'port' => $config->get('internalSmtpPort'),
'auth' => $config->get('internalSmtpAuth'),
'username' => $config->get('internalSmtpUsername'),
'password' => $config->get('internalSmtpPassword'),
'security' => $config->get('internalSmtpSecurity'),
'fromAddress' => $config->get('internalOutboundEmailFromAddress', $config->get('outboundEmailFromAddress')),
]);
}
$mailSender->send($email);
}
private function fail(?string $msg = null, int $errorCode = 403)
{
$config = $this->getInjection('config');
$noExposure = $config->get('passwordRecoveryNoExposure') ?? false;
if ($msg) {
$GLOBALS['log']->warning($msg);
}
if (!$noExposure) {
if ($errorCode === 403) {
throw new Forbidden();
} else if ($errorCode === 404) {
throw new NotFound();
} else {
throw new Error();
}
}
$this->delay();
}
}

View File

@@ -27,6 +27,11 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
/************************************************************************
* This file contains code parts copied from TCPDF software library
* that is published under GNU Lesser General Public License.
************************************************************************/
namespace Espo\Core\Pdf;
define('K_TCPDF_EXTERNAL_CONFIG', true);

View File

@@ -0,0 +1,194 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2020 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\TemplateHelpers;
class GoogleMaps
{
public static function image()
{
$args = func_get_args();
$context = $args[count($args) - 1];
$hash = $context['hash'];
$data = $context['data']['root'];
$em = $data['__entityManager'];
$metadata = $data['__metadata'];
$config = $data['__config'];
$entityType = $data['__entityType'];
$field = $hash['field'] ?? null;
$size = $hash['size'] ?? '400x400';
$zoom = $hash['zoom'] ?? null;
$language = $hash['language'] ?? $config->get('language');
if (strpos($size, 'x') === false) {
$size = $size .'x' . $size;
}
if ($field && $metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']) !== 'address') {
$GLOBALS['log']->warning("Template helper _googleMapsImage: Specified field is not of address type.");
return null;
}
if (
!$field &&
!array_key_exists('street', $hash) &&
!array_key_exists('city', $hash) &&
!array_key_exists('country', $hash) &&
!array_key_exists('state', $hash) &&
!array_key_exists('postalCode', $hash)
) {
$field = ($entityType === 'Account') ? 'billingAddress' : 'address';
}
if ($field) {
$street = $data[$field . 'Street'] ?? null;
$city = $data[$field . 'City'] ?? null;
$country = $data[$field . 'Country'] ?? null;
$state = $data[$field . 'State'] ?? null;
$postalCode = $data[$field . 'postalCode'] ?? null;
} else {
$street = $hash['street'] ?? null;
$city = $hash['city'] ?? null;
$country = $hash['country'] ?? null;
$state = $hash['state'] ?? null;
$postalCode = $hash['postalCode'] ?? null;
}
$address = '';
if ($street) {
$address .= $street;
}
if ($city) {
if ($address != '') {
$address .= ', ';
}
$address .= $city;
}
if ($state) {
if ($address != '') {
$address .= ', ';
}
$address .= $state;
}
if ($postalCode) {
if ($state || $city) {
$address .= ' ';
} else {
if ($address) {
$address .= ', ';
}
}
$address .= $postalCode;
}
if ($country) {
if ($address != '') {
$address .= ', ';
}
$address .= $country;
}
$address = urlencode($address);
$apiKey = $config->get('googleMapsApiKey');
if (!$apiKey) {
$GLOBALS['log']->error("Template helper _googleMapsImage: No Google Maps API key.");
return null;
}
if (!$address) {
$GLOBALS['log']->debug("Template helper _googleMapsImage: No address to display.");
return null;
}
$format = 'jpg;';
$url = "https://maps.googleapis.com/maps/api/staticmap?" .
'center=' . $address .
'format=' . $format .
'&size=' . $size .
'&key=' . $apiKey;
if ($zoom) {
$url .= '&zoom=' . $zoom;
}
if ($language) {
$url .= '&language=' . $language;
}
$GLOBALS['log']->debug("Template helper _googleMapsImage: URL: {$url}.");
$image = \Espo\Core\TemplateHelpers\GoogleMaps::getImage($url);
if (!$image) {
return null;
}
$filePath = tempnam(sys_get_temp_dir(), 'google_maps_image');
file_put_contents($filePath, $image);
list($width, $height) = explode('x', $size);
$tag = "<img src=\"{$filePath}\" width=\"{$width}\" height=\"{$height}\">";
return new LightnCandy\SafeString($tag);
}
public static function getImage(string $url)
{
$headers = [];
$headers[] = 'Accept: image/jpeg, image/pjpeg';
$headers[] = 'Connection: Keep-Alive';
$agent = 'Mozilla/5.0';
$c = curl_init();
curl_setopt($c, \CURLOPT_URL, $url);
curl_setopt($c, \CURLOPT_HTTPHEADER, $headers);
curl_setopt($c, \CURLOPT_HEADER, 0);
curl_setopt($c, \CURLOPT_USERAGENT, $agent);
curl_setopt($c, \CURLOPT_TIMEOUT, 10);
curl_setopt($c, \CURLOPT_RETURNTRANSFER, 1);
curl_setopt($c, \CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($c, \CURLOPT_BINARYTRANSFER, 1);
$raw = curl_exec($c);
curl_close($c);
return $raw;
}
}

View File

@@ -442,7 +442,7 @@ class Auth
if ($authToken->get('secret')) {
$sentSecret = $_COOKIE['auth-token-secret'] ?? null;
if ($sentSecret === $authToken->get('secret')) {
setcookie('auth-token-secret', null, -1, '/');
$this->setSecretInCookie(null);
}
}
return true;
@@ -487,15 +487,21 @@ class Auth
$this->getEntityManager()->saveEntity($authLogRecord);
}
protected function setSecretInCookie(string $secret)
protected function setSecretInCookie(?string $secret)
{
if (!$secret) {
$time = -1;
} else {
$time = strtotime('+1000 days');
}
if (version_compare(\PHP_VERSION, '7.3.0') < 0) {
setcookie('auth-token-secret', $secret, strtotime('+1000 days'), '/', '', false, true);
setcookie('auth-token-secret', $secret, $time, '/', '', false, true);
return;
}
setcookie('auth-token-secret', $secret, [
'expires' => strtotime('+1000 days'),
'expires' => $time,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',

View File

@@ -67,7 +67,7 @@ class Sender
$handler = curl_init($webhook->get('url'));
curl_setopt($handler, \CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, \CURLOPT_FOLLOWLOCATION, true);
curl_setopt($handler, \CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($handler, \CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($handler, \CURLOPT_HEADER, true);
curl_setopt($handler, \CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($handler, \CURLOPT_CONNECTTIMEOUT, $connectTimeout);

View File

@@ -156,6 +156,7 @@ return [
'ldapUserObjectClass',
'maxEmailAccountCount',
'massEmailMaxPerHourCount',
'massEmailSiteUrl',
'personalEmailMaxPortionSize',
'inboundEmailMaxPortionSize',
'authTokenLifetime',
@@ -181,7 +182,10 @@ return [
'authTokenPreventConcurrent',
'emailParser',
'passwordRecoveryDisabled',
'passwordRecoveryNoExposure',
'passwordRecoveryForAdminDisabled',
'passwordRecoveryForInternalUsersDisabled',
'passwordRecoveryRequestDelay',
'latestVersion',
],
'superAdminItems' => [

View File

@@ -140,21 +140,21 @@ class Email extends \Espo\Core\ORM\Entity
$body = $this->get('body');
$breaks = array("<br />","<br>","<br/>","<br />","&lt;br /&gt;","&lt;br/&gt;","&lt;br&gt;");
$breaks = ["<br />","<br>","<br/>","<br />","&lt;br /&gt;","&lt;br/&gt;","&lt;br&gt;"];
$body = str_ireplace($breaks, "\r\n", $body);
$body = strip_tags($body);
$reList = [
'/&(quot|#34);/i',
'/&(amp|#38);/i',
'/&(lt|#60);/i',
'/&(gt|#62);/i',
'/&(nbsp|#160);/i',
'/&(iexcl|#161);/i',
'/&(cent|#162);/i',
'/&(pound|#163);/i',
'/&(copy|#169);/i',
'/&(reg|#174);/i'
'&(quot|#34);',
'&(amp|#38);',
'&(lt|#60);',
'&(gt|#62);',
'&(nbsp|#160);',
'&(iexcl|#161);',
'&(cent|#162);',
'&(pound|#163);',
'&(copy|#169);',
'&(reg|#174);',
];
$replaceList = [
'',
@@ -166,10 +166,12 @@ class Email extends \Espo\Core\ORM\Entity
chr(162),
chr(163),
chr(169),
chr(174)
chr(174),
];
$body = preg_replace($reList, $replaceList, $body);
foreach ($reList as $i => $re) {
$body = mb_ereg_replace($re, $replaceList[$i], $body, 'i');
}
return $body;
}

View File

@@ -35,7 +35,7 @@ class ProcessJobQueueQ0 extends \Espo\Core\Jobs\Base
{
public function run()
{
$limit = $this->getConfig()->get('jobQ1MaxPortion', 200);
$limit = $this->getConfig()->get('jobQ0MaxPortion', 200);
$cronManager = new \Espo\Core\CronManager($this->getContainer());

View File

@@ -17,7 +17,7 @@
},
"seconds": {
"type": "enumInt",
"options": [0, 60, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 18000, 86400, 172800, 259200, 432000],
"options": [0, 60, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 18000, 86400, 172800, 259200, 432000, 604800],
"default": 0
},
"entityType": {

View File

@@ -325,14 +325,14 @@ class MassEmail extends \Espo\Services\Record
$body = $emailData['body'];
$optOutUrl = $this->getConfig()->get('siteUrl') . '?entryPoint=unsubscribe&id=' . $queueItem->id;
$optOutUrl = $this->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->id;
$optOutLink = '<a href="'.$optOutUrl.'">'.$this->getLanguage()->translate('Unsubscribe', 'labels', 'Campaign').'</a>';
$body = str_replace('{optOutUrl}', $optOutUrl, $body);
$body = str_replace('{optOutLink}', $optOutLink, $body);
foreach ($trackingUrlList as $trackingUrl) {
$url = $this->getConfig()->get('siteUrl') . '?entryPoint=campaignUrl&id=' . $trackingUrl->id . '&queueItemId=' . $queueItem->id;
$url = $this->getSiteUrl() . '?entryPoint=campaignUrl&id=' . $trackingUrl->id . '&queueItemId=' . $queueItem->id;
$body = str_replace($trackingUrl->get('urlToUse'), $url, $body);
}
@@ -346,7 +346,7 @@ class MassEmail extends \Espo\Services\Record
$trackImageAlt = $this->getLanguage()->translate('Campaign', 'scopeNames');
$trackOpenedUrl = $this->getConfig()->get('siteUrl') . '?entryPoint=campaignTrackOpened&id=' . $queueItem->id;
$trackOpenedUrl = $this->getSiteUrl() . '?entryPoint=campaignTrackOpened&id=' . $queueItem->id;
$trackOpenedHtml = '<img alt="'.$trackImageAlt.'" width="1" height="1" border="0" src="'.$trackOpenedUrl.'">';
if ($massEmail->get('campaignId') && $this->getConfig()->get('massEmailOpenTracking')) {
@@ -387,7 +387,7 @@ class MassEmail extends \Espo\Services\Record
$message->getHeaders()->addHeaderLine('Precedence', 'bulk');
if (!$this->getConfig()->get('massEmailDisableMandatoryOptOutLink')) {
$optOutUrl = $this->getConfig()->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->id;
$optOutUrl = $this->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->id;
$message->getHeaders()->addHeaderLine('List-Unsubscribe', '<' . $optOutUrl . '>');
}
@@ -589,4 +589,8 @@ class MassEmail extends \Espo\Services\Record
return $dataList;
}
protected function getSiteUrl()
{
return $this->getConfig()->get('massEmailSiteUrl') ?? $this->getConfig()->get('siteUrl');
}
}

View File

@@ -118,7 +118,7 @@ class EntityManager
public function getMapper($name)
{
if ($name{0} == '\\') {
if ($name[0] == '\\') {
$className = $name;
} else {
$className = $this->getMapperClassName($name);

View File

@@ -269,7 +269,7 @@
"systemRequirements": "System Requirements for EspoCRM.",
"apiUsers": "Separate users for integration purposes.",
"webhooks": "Manage webhooks.",
"emailAddresses": "All emailes addresses stored in the system.",
"emailAddresses": "All email addresses stored in the system.",
"phoneNumbers": "All phone numbers stored in the system.",
"dashboardTemplates": "Deploy dashboards to users.",
"layoutSets": "Collections of layouts that can be assigned to teams & portals.",

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 won'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?",
"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

@@ -9,6 +9,7 @@
"log": "Log"
},
"labels": {
"As often as possible": "As often as possible",
"Create ScheduledJob": "Create Scheduled Job"
},
"options": {

View File

@@ -126,6 +126,8 @@
"useWebSocket": "Use WebSocket",
"passwordRecoveryDisabled": "Disable password recovery",
"passwordRecoveryForAdminDisabled": "Disable password recovery for admin users",
"passwordRecoveryForInternalUsersDisabled": "Disable password recovery for internal users",
"passwordRecoveryNoExposure": "Prevent email address exposure on password recovery form",
"passwordGenerateLength": "Length of generated passwords",
"passwordStrengthLength": "Minimum password length",
"passwordStrengthLetterCount": "Number of letters required in password",
@@ -160,6 +162,8 @@
}
},
"tooltips": {
"passwordRecoveryForInternalUsersDisabled": "Only portal users will be able to recover password.",
"passwordRecoveryNoExposure": "It won't be possible to determine whether a specific email address is registered in the system.",
"emailAddressLookupEntityTypeList": "For email address autocomplete.",
"emailNotificationsDelay": "A message can be edited within the specified timeframe before the notification is sent.",
"outboundEmailFromAddress": "The system email address.",

View File

@@ -91,6 +91,7 @@
"portals": "Portals which this user has access to."
},
"messages": {
"passwordRecoverySentIfMatched": "Assuming the entered data matched any user account.",
"passwordStrengthLength": "Must be at least {length} characters long.",
"passwordStrengthLetterCount": "Must contain at least {count} letter(s).",
"passwordStrengthNumberCount": "Must contain at least {count} digit(s).",

View File

@@ -43,10 +43,11 @@
"label":"SMTP",
"rows": [
[{"name": "useSmtp"}, false],
[{"name": "smtpHost"}, {"name": "smtpPort"}],
[{"name": "smtpAuth"}, {"name": "smtpSecurity"}],
[{"name": "smtpUsername"}, {"name": "smtpAuthMechanism"}],
[{"name": "smtpPassword"}, false],
[{"name": "smtpHost"}, {"name": "smtpSecurity"}],
[{"name": "smtpPort"}, {"name": "smtpAuth"}],
[false, {"name": "smtpAuthMechanism"}],
[false, {"name": "smtpUsername"}],
[false, {"name": "smtpPassword"}],
[
{"name": "smtpTestSend", "customLabel": null, "view": "views/email-account/fields/test-send"}, false
]

View File

@@ -72,11 +72,11 @@
"rows": [
[{"name": "useSmtp"}, false],
[{"name": "smtpIsShared"}, {"name": "smtpIsForMassEmail"}],
[{"name": "smtpHost"}, {"name": "smtpPort"}],
[{"name": "smtpAuth"}, {"name": "smtpSecurity"}],
[{"name": "smtpUsername"}, {"name": "smtpAuthMechanism"}],
[{"name": "smtpPassword"}, false],
[{"name": "fromName"}, false],
[{"name": "smtpHost"}, {"name": "smtpSecurity"}],
[{"name": "smtpPort"}, {"name": "smtpAuth"}],
[{"name": "fromName"}, {"name": "smtpAuthMechanism"}],
[false, {"name": "smtpUsername"}],
[false, {"name": "smtpPassword"}],
[
{"name": "smtpTestSend", "customLabel": null, "view": "views/inbound-email/fields/test-send"}, false
]

View File

@@ -19,7 +19,8 @@
[{"name": "passwordGenerateLength"}, false],
[{"name": "passwordStrengthLength"}, {"name": "passwordStrengthLetterCount"}],
[{"name": "passwordStrengthBothCases"}, {"name": "passwordStrengthNumberCount"}],
[{"name": "passwordRecoveryDisabled"}, {"name": "passwordRecoveryForAdminDisabled"}]
[{"name": "passwordRecoveryDisabled"}, {"name": "passwordRecoveryForAdminDisabled"}],
[{"name": "passwordRecoveryNoExposure"}, {"name": "passwordRecoveryForInternalUsersDisabled"}]
]
}
]

View File

@@ -0,0 +1,3 @@
{
"googleMapsImage": "\\Espo\\Core\\TemplateHelpers\\GoogleMaps::image"
}

View File

@@ -4,6 +4,9 @@
"log": {
"readOnly": true,
"view": "views/scheduled-job/record/panels/log",
"createDisabled": true,
"selectDisabled": true,
"viewDisabled": true,
"unlinkDisabled": true
}
},

View File

@@ -9,6 +9,9 @@
},
"enabled": {
"type": "bool"
},
"isLocked": {
"type": "bool"
}
}
}

View File

@@ -175,6 +175,14 @@
"passwordRecoveryForAdminDisabled": {
"type": "bool"
},
"passwordRecoveryForInternalUsersDisabled": {
"type": "bool",
"tooltip": true
},
"passwordRecoveryNoExposure": {
"type": "bool",
"tooltip": true
},
"passwordGenerateLength": {
"type": "int",
"min": 6,

View File

@@ -75,7 +75,7 @@ class Email extends \Espo\Core\SelectManagers\Base
$skipIndex = true;
}
$actualDatabaseType = $this->getConfig()->get('actualDatabaseType');
/*$actualDatabaseType = strtolower($this->getConfig()->get('actualDatabaseType'));
$actualDatabaseVersion = $this->getConfig()->get('actualDatabaseVersion');
if (
@@ -84,7 +84,12 @@ class Email extends \Espo\Core\SelectManagers\Base
$this->hasLinkJoined('teams', $result)
) {
$skipIndex = true;
}*/
if ($this->hasLinkJoined('teams', $result)) {
$skipIndex = true;
}
if (!$skipIndex) {
$result['useIndex'] = 'dateSent';
}

View File

@@ -297,7 +297,7 @@ class Attachment extends Record
$opts[\CURLOPT_HEADER] = true;
$opts[\CURLOPT_BINARYTRANSFER] = true;
$opts[\CURLOPT_VERBOSE] = true;
$opts[\CURLOPT_SSL_VERIFYPEER] = false;
$opts[\CURLOPT_SSL_VERIFYPEER] = true;
$opts[\CURLOPT_SSL_VERIFYHOST] = 2;
$opts[\CURLOPT_RETURNTRANSFER] = true;
$opts[\CURLOPT_FOLLOWLOCATION] = true;

View File

@@ -92,6 +92,7 @@ class EmailAccount extends Record
$entity = $this->getEntityManager()->getEntity('EmailAccount', $params['id']);
if ($entity) {
$params['password'] = $this->getCrypt()->decrypt($entity->get('password'));
$params['imapHandler'] = $entity->get('imapHandler');
}
}
@@ -108,6 +109,13 @@ class EmailAccount extends Record
public function testConnection(array $params)
{
if (!empty($params['id'])) {
$account = $this->getEntityManager()->getEntity('EmailAccount', $params['id']);
if ($account) {
$params['imapHandler'] = $account->get('imapHandler');
}
}
$storage = $this->createStorage($params);
$userId = $params['userId'] ?? null;
@@ -132,7 +140,22 @@ class EmailAccount extends Record
$imapParams = null;
if ($emailAddress && $userId) {
$handlerClassName = $params['imapHandler'] ?? null;
if ($handlerClassName && !empty($params['id'])) {
try {
$handler = $this->getInjection('injectableFactory')->createByClassName($handlerClassName);
} catch (\Throwable $e) {
$GLOBALS['log']->error(
"EmailAccount: Could not create Imap Handler. Error: " . $e->getMessage()
);
}
if (method_exists($handler, 'prepareProtocol')) {
$imapParams = $handler->prepareProtocol($params['id'], $params);
}
}
if ($emailAddress && $userId && !$handlerClassName) {
$emailAddress = strtolower($emailAddress);
$userData = $this->getEntityManager()->getRepository('UserData')->getByUserId($userId);
if ($userData) {
@@ -218,6 +241,9 @@ class EmailAccount extends Record
$params['ssl'] = true;
}
$params['imapHandler'] = $emailAccount->get('imapHandler');
$params['id'] = $emailAccount->id;
$storage = $this->createStorage($params);
return $storage;

View File

@@ -29,11 +29,11 @@
namespace Espo\Services;
use \Espo\ORM\Entity;
use Espo\ORM\Entity;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Forbidden;
class ExternalAccount extends Record
{
@@ -56,7 +56,9 @@ class ExternalAccount extends Record
throw new Error("{$integration} is disabled.");
}
$factory = new \Espo\Core\ExternalAccount\ClientManager($this->getEntityManager(), $this->getMetadata(), $this->getConfig());
$factory = new \Espo\Core\ExternalAccount\ClientManager(
$this->getEntityManager(), $this->getMetadata(), $this->getConfig(), $this->getInjection('injectableFactory')
);
return $factory->create($integration, $id);
}
@@ -92,6 +94,7 @@ class ExternalAccount extends Record
$entity->clear('accessToken');
$entity->clear('refreshToken');
$entity->clear('tokenType');
$entity->clear('expiresAt');
foreach ($result as $name => $value) {
$entity->set($name, $value);
}
@@ -109,4 +112,29 @@ class ExternalAccount extends Record
throw new Error("Could not load client for {$integration}.");
}
}
public function read($id)
{
list($integration, $userId) = explode('__', $id);
if ($this->getUser()->id != $userId && !$this->getUser()->isAdmin()) {
throw new Forbidden();
}
$entity = $this->getEntityManager()->getEntity('ExternalAccount', $id);
if (!$entity) throw new NotFoundSilent("Record does not exist.");
list($integration, $id) = explode('__', $entity->id);
$externalAccountSecretAttributeList = $this->getMetadata()->get(
['integrations', $integration, 'externalAccountSecretAttributeList']) ?? [];
foreach ($externalAccountSecretAttributeList as $a) {
$entity->clear($a);
}
return $entity;
}
}

View File

@@ -29,8 +29,8 @@
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\ORM\Entity;
@@ -88,15 +88,6 @@ class GlobalSearch extends \Espo\Core\Services\Base
$fullTextSearchData = $selectManager->getFullTextSearchDataForTextFilter($query);
if ($fullTextSearchData) {
$hasFullTextSearch = true;
$selectParams['select'][] = [$fullTextSearchData['where'], '_relevance'];
$selectParams['orderBy'] = [[$fullTextSearchData['where'], 'desc'], ['name']];
} else {
$selectParams['select'][] = ['VALUE:1.1', '_relevance'];
$selectParams['orderBy'] = [['name']];
}
if ($this->getMetadata()->get(['entityDefs', $entityType, 'fields', 'name', 'type']) === 'personName') {
$selectParams['select'][] = 'firstName';
$selectParams['select'][] = 'lastName';
@@ -114,6 +105,15 @@ class GlobalSearch extends \Espo\Core\Services\Base
unset($selectParams['additionalSelect']);
if ($fullTextSearchData) {
$hasFullTextSearch = true;
$selectParams['select'][] = [$fullTextSearchData['where'], '_relevance'];
$selectParams['orderBy'] = [[$fullTextSearchData['where'], 'desc'], ['name']];
} else {
$selectParams['select'][] = ['VALUE:1.1', '_relevance'];
$selectParams['orderBy'] = [['name']];
}
$itemSql = $this->getEntityManager()->getQuery()->createSelectQuery($entityType, $selectParams);
$unionPartList[] = "(\n" . $itemSql . "\n)";

View File

@@ -299,7 +299,7 @@ class Import extends \Espo\Services\Record
if ($entity) {
$this->getEntityManager()->removeEntity($entity);
}
$this->getEntityManager()->getRepository($entity->getEntityType())->deleteFromDb($entityId);
$this->getEntityManager()->getRepository($entityType)->deleteFromDb($entityId);
}
return true;

View File

@@ -164,7 +164,7 @@ class Layout extends \Espo\Core\Services\Base
{
$em = $this->getInjection('entityManager');
$layoutSet = $em->getEntity('LayoutSet', $setId);
if (!$layoutSet) throw new NotFound();
if (!$layoutSet) throw new NotFound("LayoutSet {$setId} not found.");
$layoutList = $layoutSet->get('layoutList') ?? [];

View File

@@ -102,11 +102,14 @@ class Metadata extends \Espo\Core\Services\Base
}
$foreignEntityType = $defs['entity'] ?? null;
if ($this->getAcl()->check($foreignEntityType)) continue;
if ($this->getUser()->isPortal()) {
if ($foreignEntityType === 'Account' || $foreignEntityType === 'Contact') {
continue;
if ($foreignEntityType) {
if ($this->getAcl()->check($foreignEntityType)) continue;
if ($this->getUser()->isPortal()) {
if ($foreignEntityType === 'Account' || $foreignEntityType === 'Contact') {
continue;
}
}
}

View File

@@ -29,7 +29,25 @@
namespace Espo\Services;
use Espo\ORM\Entity;
use Espo\Core\Exceptions\BadRequest;
class ScheduledJob extends Record
{
protected $findLinkedLogCountQueryDisabled = true;
public function processValidation(Entity $entity, $data)
{
parent::processValidation($entity, $data);
$scheduling = $entity->get('scheduling');
try {
$cronExpression = \Cron\CronExpression::factory($scheduling);
$nextDate = $cronExpression->getNextRunDate()->format('Y-m-d H:i:s');
} catch (\Exception $e) {
throw new BadRequest("Not valid scheduling expression.");
}
}
}

View File

@@ -30,7 +30,7 @@
namespace Espo\Services;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\BadRequest;
use Espo\ORM\Entity;
@@ -146,7 +146,7 @@ class Settings extends \Espo\Core\Services\Base
}
if (
($this->getConfig()->get('smtpServer') || $this->getConfig()->get('internalSmtpServer'))
($this->getConfig()->get('outboundEmailFromAddress') || $this->getConfig()->get('internalSmtpServer'))
&&
!$this->getConfig()->get('passwordRecoveryDisabled')
) {

View File

@@ -29,17 +29,15 @@
namespace Espo\Services;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Utils\Util;
use \Espo\ORM\Entity;
use Espo\ORM\Entity;
class User extends Record
{
const PASSWORD_CHANGE_REQUEST_LIFETIME = 360; // minutes
protected function init()
{
parent::init();
@@ -209,83 +207,30 @@ class User extends Record
public function passwordChangeRequest($userName, $emailAddress, $url = null)
{
if ($this->getConfig()->get('passwordRecoveryDisabled')) {
throw new Forbidden("Password recovery disabled");
}
$user = $this->getEntityManager()->getRepository('User')->where([
'userName' => $userName,
'emailAddress' => $emailAddress
])->findOne();
if (empty($user)) {
throw new NotFound();
}
if (!$user->isActive()) {
throw new NotFound();
}
if ($user->isApi()) {
throw new NotFound();
}
if ($this->getConfig()->get('passwordRecoveryForAdminDisabled')) {
if ($user->isAdmin()) {
throw new NotFound();
}
}
$userId = $user->id;
$passwordChangeRequest = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where([
'userId' => $userId
])->findOne();
if ($passwordChangeRequest) {
throw new Forbidden(json_encode(['reason' => 'Already-Sent']));
}
$requestId = Util::generateCryptId();
$passwordChangeRequest = $this->getEntityManager()->getEntity('PasswordChangeRequest');
$passwordChangeRequest->set([
'userId' => $userId,
'requestId' => $requestId,
'url' => $url
]);
if (!$user->isAdmin() && $this->getConfig()->get('authenticationMethod', 'Espo') !== 'Espo') {
throw new Forbidden();
}
$this->sendChangePasswordLink($requestId, $emailAddress, $user);
$this->getEntityManager()->saveEntity($passwordChangeRequest);
if (!$passwordChangeRequest->id) {
throw new Error();
}
$dt = new \DateTime();
$dt->add(new \DateInterval('PT'. self::PASSWORD_CHANGE_REQUEST_LIFETIME . 'M'));
$job = $this->getEntityManager()->getEntity('Job');
$job->set([
'serviceName' => 'User',
'methodName' => 'removeChangePasswordRequestJob',
'data' => [
'id' => $passwordChangeRequest->id
],
'executeTime' => $dt->format('Y-m-d H:i:s'),
'queue' => 'q1'
]);
$this->getEntityManager()->saveEntity($job);
$recovery = $this->getContainer()->get('injectableFactory')->createByClassName('\\Espo\\Core\\Password\\Recovery');
$recovery->request($emailAddress, $userName, $url);
return true;
}
public function changePasswordByRequest(string $requestId, string $password)
{
$recovery = $this->getContainer()->get('injectableFactory')->createByClassName('\\Espo\\Core\\Password\\Recovery');
$request = $recovery->getRequest($requestId);
$userId = $request->get('userId');
if (!$userId) throw new Error();
$this->changePassword($userId, $password);
$recovery->removeRequest($requestId);
return (object) [
'url' => $request->get('url'),
];
}
public function removeChangePasswordRequestJob($data)
{
if (empty($data->id)) {
@@ -439,7 +384,7 @@ class User extends Record
throw new Forbidden("Generate new password: Can't process because user desn't have email address.");
}
if (!$this->getConfig()->get('smtpServer') && !$this->getConfig()->get('internalSmtpServer')) {
if (!$this->getMailSender()->hasSystemSmtp() && !$this->getConfig()->get('internalSmtpServer')) {
throw new Forbidden("Generate new password: Can't process because SMTP is not configured.");
}
@@ -585,7 +530,7 @@ class User extends Record
$email = $this->getEntityManager()->getEntity('Email');
if (!$this->getConfig()->get('smtpServer') && !$this->getConfig()->get('internalSmtpServer')) {
if (!$this->getMailSender()->hasSystemSmtp() && !$this->getConfig()->get('internalSmtpServer')) {
return;
}
@@ -643,7 +588,7 @@ class User extends Record
'to' => $emailAddress
]);
if ($this->getConfig()->get('smtpServer')) {
if ($this->getMailSender()->hasSystemSmtp()) {
$this->getMailSender()->useGlobal();
} else {
$this->getMailSender()->useSmtp(array(
@@ -659,69 +604,7 @@ class User extends Record
$this->getMailSender()->send($email);
}
protected function sendChangePasswordLink($requestId, $emailAddress, Entity $user)
{
if (empty($emailAddress)) {
return;
}
$email = $this->getEntityManager()->getEntity('Email');
if (!$this->getConfig()->get('smtpServer') && !$this->getConfig()->get('internalSmtpServer')) {
throw new Error("SMTP credentials are not defined.");
}
$templateFileManager = $this->getContainer()->get('templateFileManager');
$subjectTpl = $templateFileManager->getTemplate('passwordChangeLink', 'subject', 'User');
$bodyTpl = $templateFileManager->getTemplate('passwordChangeLink', 'body', 'User');
$siteUrl = $this->getConfig()->getSiteUrl();
if ($user->isPortal()) {
$portal = $this->getEntityManager()->getRepository('Portal')->distinct()->join('users')->where([
'isActive' => true,
'users.id' => $user->id,
])->findOne();
if ($portal) {
if ($portal->get('customUrl')) {
$siteUrl = $portal->get('customUrl');
}
}
}
$data = [];
$link = $siteUrl . '?entryPoint=changePassword&id=' . $requestId;
$data['link'] = $link;
$htmlizer = new \Espo\Core\Htmlizer\Htmlizer($this->getFileManager(), $this->getDateTime(), $this->getNumber(), null);
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
$email->set([
'subject' => $subject,
'body' => $body,
'to' => $emailAddress,
'isSystem' => true
]);
if ($this->getConfig()->get('smtpServer')) {
$this->getMailSender()->useGlobal();
} else {
$this->getMailSender()->useSmtp([
'server' => $this->getConfig()->get('internalSmtpServer'),
'port' => $this->getConfig()->get('internalSmtpPort'),
'auth' => $this->getConfig()->get('internalSmtpAuth'),
'username' => $this->getConfig()->get('internalSmtpUsername'),
'password' => $this->getConfig()->get('internalSmtpPassword'),
'security' => $this->getConfig()->get('internalSmtpSecurity'),
'fromAddress' => $this->getConfig()->get('internalOutboundEmailFromAddress', $this->getConfig()->get('outboundEmailFromAddress'))
]);
}
$this->getMailSender()->send($email);
}
public function delete($id)
{

File diff suppressed because it is too large Load Diff

1
client/lib/cronstrue-i18n.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -39,9 +39,10 @@ Espo.define('crm:views/meeting/popup-notification', 'views/popup-notification',
closeButton: true,
setup: function () {
this.wait(true);
if (this.notificationData.entityType) {
this.wait(true);
this.getModelFactory().create(this.notificationData.entityType, function (model) {
var dateAttribute = 'dateStart';

View File

@@ -3,20 +3,8 @@
<% _.each(layout, function (panel, columnNumber) { %>
<% hasHiddenPanel = panel.hidden || hasHiddenPanel; %>
<div class="panel panel-<%= panel.style %><% if (panel.name) { %>{{#if hiddenPanels.<%= panel.name %>}} hidden{{/if}}<% } %>"<% if (panel.name) print(' data-name="'+panel.name+'"') %>>
<%
var panelLabelString = null;
if ('customLabel' in panel) {
if (panel.customLabel) {
panelLabelString = panel.customLabel;
}
} else {
if (panel.label) {
panelLabelString = "{{translate \"" + panel.label + "\" scope=\""+model.name+"\"}}";
}
}
%>
<% if (panelLabelString) { %>
<div class="panel-heading"><h4 class="panel-title"><%= panelLabelString %></h4></div>
<% if (panel.label) { %>
<div class="panel-heading"><h4 class="panel-title"><%= panel.label %></h4></div>
<% } %>
<div class="panel-body panel-body-form">

View File

@@ -1,5 +1,5 @@
<header data-name="{{name}}">
<label data-is-custom="{{#if isCustomLabel}}true{{/if}}">{{label}}</label>&nbsp;
<label data-is-custom="{{#if isCustomLabel}}true{{/if}}" data-label="{{label}}">{{labelTranslated}}</label>&nbsp;
<a href="javascript:" data-action="edit-panel-label" class="edit-panel-label"><i class="fas fa-pencil-alt fa-sm"></i></a>
<a href="javascript:" style="float: right;" data-action="removePanel" class="remove-panel" data-number="{{number}}"><i class="fas fa-times"></i></a>
</header>

View File

@@ -5,7 +5,7 @@
</div>
<div class="panel-body">
<div class="no-margin">
<table class="table table-bordered no-margin">
<table class="table table-bordered no-margin scope-level">
<tr>
<th></th>
<th width="20%">{{translate 'Access' scope='Role'}}</th>
@@ -25,7 +25,12 @@
{{#each ../list}}
<td>
{{#if levelList}}
<select name="{{name}}" class="form-control{{#ifNotEqual ../../../access 'enabled'}} hidden{{/ifNotEqual}}" data-scope="{{../../name}}"{{#ifNotEqual ../../access 'enabled'}} disabled{{/ifNotEqual}} title="{{translate action scope='Role' category='actions'}}">
<select name="{{name}}"
class="form-control scope-action{{#ifNotEqual ../../../access 'enabled'}} hidden{{/ifNotEqual}}"
data-scope="{{../../name}}"
{{#ifNotEqual ../../access 'enabled'}} disabled{{/ifNotEqual}}
title="{{translate action scope='Role' category='actions'}}"
data-role-action="{{action}}">
{{options levelList level field='levelList' scope='Role'}}
</select>
{{/if}}
@@ -35,6 +40,18 @@
</tr>
{{/each}}
</table>
<div class="sticky-header-scope hidden sticky-head">
<table class="table borderless no-margin">
<tr>
<th></th>
<th width="20%">{{translate 'Access' scope='Role'}}</th>
{{#each actionList}}
<th width="11%">{{translate this scope='Role' category='actions'}}</th>
{{/each}}
</tr>
</table>
</div>
</div>
</div>
</div>
@@ -46,7 +63,7 @@
</div>
<div class="panel-body">
<div class="no-margin">
<table class="table table-bordered no-margin">
<table class="table table-bordered no-margin field-level">
<tr>
<th></th>
<th width="20%"></th>
@@ -67,7 +84,13 @@
<td><b>{{translate name category='fields' scope=../name}}</b></td>
{{#each list}}
<td>
<select name="field-{{../../name}}-{{../name}}" class="form-control" data-field="{{../name}}" data-scope="{{../../name}}" data-action="{{name}}" title="{{translate name scope='Role' category='actions'}}">{{options ../../../fieldLevelList value scope='Role' field='accessList'}}</select>
<select
name="field-{{../../name}}-{{../name}}"
class="form-control field-action"
data-field="{{../name}}"
data-scope="{{../../name}}"
data-action="{{name}}"
title="{{translate name scope='Role' category='actions'}}">{{options ../../../fieldLevelList value scope='Role' field='accessList'}}</select>
</td>
{{/each}}
<td colspan="2">
@@ -77,6 +100,19 @@
{{/each}}
{{/each}}
</table>
<div class="sticky-header-field hidden sticky-head">
<table class="table borderless no-margin">
<tr>
<th></th>
<th width="20%"></th>
{{#each fieldActionList}}
<th width="11%">{{translate this scope='Role' category='actions'}}</th>
{{/each}}
<th width="33%"></th>
</tr>
</table>
</div>
</div>
</div>
</div>

View File

@@ -25,7 +25,10 @@
{{#each ../list}}
<td>
{{#ifNotEqual ../../access 'not-set'}}
<span style="color: {{prop ../../../../colors level}};">{{translateOption level field='levelList' scope='Role'}}</span>
<span
style="color: {{prop ../../../../colors level}};"
title="{{translate action scope='Role' category='actions'}}"
>{{translateOption level field='levelList' scope='Role'}}</span>
{{/ifNotEqual}}
</td>
{{/each}}
@@ -68,7 +71,9 @@
<td><b>{{translate name category='fields' scope=../name}}</b></td>
{{#each list}}
<td>
<span style="color: {{prop ../../../colors value}};">{{translateOption value scope='Role' field='accessList'}}</span>
<span
title="{{translate name scope='Role' category='actions'}}"
style="color: {{prop ../../../colors value}};">{{translateOption value scope='Role' field='accessList'}}</span>
</td>
{{/each}}
<td colspan="3"></td>

View File

@@ -1,5 +1,5 @@
<div class="container content">
<div class="col-md-4 col-md-offset-4 col-sm-8 col-sm-offset-2">
<div class="col-md-4 col-md-offset-3 col-sm-8 col-sm-offset-2">
<div class="panel panel-default password-change">
<div class="panel-heading">
<h4 class="panel-title">{{translate 'Change Password' scope='User'}}</h4>

View File

@@ -551,13 +551,13 @@ define(
setCookieAuth: function (username, token) {
var date = new Date();
date.setTime(date.getTime() + (1000 * 24*60*60*1000));
document.cookie = 'auth-username='+username+'; expires='+date.toGMTString()+'; path=/';
document.cookie = 'auth-token='+token+'; expires='+date.toGMTString()+'; path=/';
document.cookie = 'auth-username='+username+'; SameSite=Lax; expires='+date.toGMTString()+'; path=/';
document.cookie = 'auth-token='+token+'; SameSite=Lax; expires='+date.toGMTString()+'; path=/';
},
unsetCookieAuth: function () {
document.cookie = 'auth-username' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/';
document.cookie = 'auth-token' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/';
document.cookie = 'auth-username' + '=; SameSite=Lax; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/';
document.cookie = 'auth-token' + '=; SameSite=Lax; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/';
},
initUserData: function (options, callback) {
@@ -671,7 +671,7 @@ define(
if (self.auth) {
self.logout();
} else {
Espo.Ui.error(self.language.translate('Auth error'));
console.error('Error 401: Unauthorized.');
}
}
break;

View File

@@ -184,7 +184,7 @@
},
getEntityTypeFieldParam: function (entityType, field, param) {
this.metadata.get(['entityDefs', entityType, 'fields', field, param]);
return this.metadata.get(['entityDefs', entityType, 'fields', field, param]);
},
getViewName: function (fieldType) {

View File

@@ -490,7 +490,7 @@ define('ui', [], function () {
},
error: function (message) {
Espo.Ui.notify(message, 'error', 2000);
Espo.Ui.notify(message, 'error', 4000);
},
info: function (message) {

View File

@@ -29,7 +29,6 @@
define('view-helper', ['lib!client/lib/purify.min.js'], function () {
var ViewHelper = function (options) {
this.urlRegex = /(^|[^\(])(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
this._registerHandlebarsHelpers();
this.mdBeforeList = [
@@ -125,7 +124,8 @@ define('view-helper', ['lib!client/lib/purify.min.js'], function () {
className += ' ' + additionalClassName;
}
return '<img class="'+className+'" width="'+width+'" src="'+basePath+'?entryPoint=avatar&size='+size+'&id=' + id + '&t='+t+'">';
return '<img class="'+className+'" width="'+width+'" src="'+basePath+
'?entryPoint=avatar&size='+size+'&id=' + id + '&t='+t+'">';
},
_registerHandlebarsHelpers: function () {
@@ -315,8 +315,6 @@ define('view-helper', ['lib!client/lib/purify.min.js'], function () {
transfromMarkdownText: function (text, options) {
text = text || '';
text = text.replace(this.urlRegex, '$1[$2]($2)');
text = Handlebars.Utils.escapeExpression(text).replace(/&gt;+/g, '>');
this.mdBeforeList.forEach(function (item) {

View File

@@ -55,6 +55,11 @@ define('views/admin/authentication', 'views/settings/record/edit', function (Dep
this.listenTo(this.model, 'change:auth2FA', function () {
this.manage2FAFields();
}, this);
this.managePasswordRecoveryFields();
this.listenTo(this.model, 'change:passwordRecoveryDisabled', function () {
this.managePasswordRecoveryFields();
}, this);
},
setupBeforeFinal: function () {
@@ -126,5 +131,17 @@ define('views/admin/authentication', 'views/settings/record/edit', function (Dep
}
},
managePasswordRecoveryFields: function () {
if (!this.model.get('passwordRecoveryDisabled')) {
this.showField('passwordRecoveryForAdminDisabled');
this.showField('passwordRecoveryForInternalUsersDisabled');
this.showField('passwordRecoveryNoExposure');
} else {
this.hideField('passwordRecoveryForAdminDisabled');
this.hideField('passwordRecoveryForInternalUsersDisabled');
this.hideField('passwordRecoveryNoExposure');
}
},
});
});

View File

@@ -297,10 +297,10 @@ define('views/admin/layouts/grid', ['views/admin/layouts/base', 'res!client/css/
data.isCustomLabel = false;
if (data.customLabel) {
data.label = data.customLabel;
data.labelTranslated = data.customLabel;
data.isCustomLabel = true;
} else {
data.label = this.translate(data.label, 'labels', this.scope);
data.labelTranslated = this.translate(data.label, 'labels', this.scope);
}
data.style = data.style || null;
@@ -437,7 +437,7 @@ define('views/admin/layouts/grid', ['views/admin/layouts/base', 'res!client/css/
if ($label.attr('data-is-custom')) {
o.customLabel = $label.text();
} else {
o.label = $label.text();
o.label = $label.data('label');
}
$(el).find('ul.rows > li').each(function (i, li) {
var row = [];

View File

@@ -503,7 +503,8 @@ define('views/admin/link-manager/modals/edit',
if (view) {
view.disabled = true;
}
this.$el.find('.cell[data-name=' + name+']').addClass('hidden');
this.$el.find('.cell[data-name=' + name+']').addClass('hidden-cell');
},
showField: function (name) {
@@ -511,7 +512,8 @@ define('views/admin/link-manager/modals/edit',
if (view) {
view.disabled = false;
}
this.$el.find('.cell[data-name=' + name+']').removeClass('hidden');
this.$el.find('.cell[data-name=' + name+']').removeClass('hidden-cell');
},
handleLinkTypeChange: function () {

View File

@@ -26,45 +26,45 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/admin/notifications', 'views/settings/record/edit', function (Dep) {
define('views/admin/notifications', 'views/settings/record/edit', function (Dep) {
return Dep.extend({
layoutName: 'notifications',
dependencyDefs: {
'assignmentEmailNotifications': {
map: {
true: [
{
action: 'show',
fields: ['assignmentEmailNotificationsEntityList']
}
]
dynamicLogicDefs: {
fields: {
assignmentEmailNotificationsEntityList: {
visible: {
conditionGroup: [
{
type: 'isTrue',
attribute: 'assignmentEmailNotifications',
}
],
},
},
adminNotificationsNewVersion: {
visible: {
conditionGroup: [
{
type: 'isTrue',
attribute: 'adminNotifications',
}
],
},
},
adminNotificationsNewExtensionVersion: {
visible: {
conditionGroup: [
{
type: 'isTrue',
attribute: 'adminNotifications',
}
],
},
},
default: [
{
action: 'hide',
fields: ['assignmentEmailNotificationsEntityList']
}
]
},
'adminNotifications': {
map: {
true: [
{
action: 'show',
fields: ['adminNotificationsNewVersion', 'adminNotificationsNewExtensionVersion']
}
]
},
default: [
{
action: 'hide',
fields: ['adminNotificationsNewVersion', 'adminNotificationsNewExtensionVersion']
}
]
}
},
setup: function () {
@@ -89,6 +89,4 @@ Espo.define('views/admin/notifications', 'views/settings/record/edit', function
}
});
});

View File

@@ -255,6 +255,11 @@ define('views/detail', 'views/main', function (Dep) {
view.render();
view.notify(false);
this.listenToOnce(view, 'after:save', function () {
if (data.fromSelectRelated) {
setTimeout(function () {
this.clearView('dialogSelectRelated');
}.bind(this), 25);
}
this.updateRelationshipPanel(link);
this.model.trigger('after:relate');
}, this);
@@ -287,7 +292,7 @@ define('views/detail', 'views/main', function (Dep) {
}
}
} else {
var foreignLink = this.model.defs['links'][link].foreign;
var foreignLink = (this.model.defs['links'][link] || {}).foreign;
if (foreignLink && scope) {
var foreignLinkType = this.getMetadata().get(['entityDefs', scope, 'links', foreignLink, 'type']);
var foreignLinkFieldType = this.getMetadata().get(['entityDefs', scope, 'fields', foreignLink, 'type']);
@@ -329,18 +334,27 @@ define('views/detail', 'views/main', function (Dep) {
var viewName = this.getMetadata().get('clientDefs.' + scope + '.modalViews.select') || 'views/modals/select-records';
this.notify('Loading...');
this.createView('dialog', viewName, {
this.createView('dialogSelectRelated', viewName, {
scope: scope,
multiple: true,
createButton: false,
createButton: data.createButton || false,
triggerCreateEvent: true,
filters: filters,
massRelateEnabled: massRelateEnabled,
primaryFilterName: primaryFilterName,
boolFilterList: boolFilterList
boolFilterList: boolFilterList,
}, function (dialog) {
dialog.render();
this.notify(false);
dialog.once('select', function (selectObj) {
Espo.Ui.notify(false);
this.listenTo(dialog, 'create', function () {
this.actionCreateRelated({
link: data.link,
fromSelectRelated: true,
});
}, this);
this.listenToOnce(dialog, 'select', function (selectObj) {
var data = {};
if (Object.prototype.toString.call(selectObj) === '[object Array]') {
var ids = [];
@@ -356,21 +370,21 @@ define('views/detail', 'views/main', function (Dep) {
data.id = selectObj.id;
}
}
$.ajax({
url: this.scope + '/' + this.model.id + '/' + link,
type: 'POST',
data: JSON.stringify(data),
success: function () {
Espo.Ajax.postRequest(this.scope + '/' + this.model.id + '/' + link, data)
.then(
function () {
this.notify('Linked', 'success');
this.updateRelationshipPanel(link);
this.model.trigger('after:relate');
}.bind(this),
error: function () {
}.bind(this)
)
.fail(
function () {
this.notify('Error occurred', 'error');
}.bind(this)
});
}.bind(this));
}.bind(this));
);
}, this);
});
},
actionDuplicate: function () {

View File

@@ -111,7 +111,7 @@ define('views/email-account/record/detail', 'views/record/detail', function (Dep
initSmtpFieldsControl: function () {
this.controlSmtpFields();
this.listenTo(this.model, 'change:useSmtp', this.controlSmtpFields, this);
this.listenTo(this.model, 'change:smtpAuth', this.controlSmtpAuthField, this);
this.listenTo(this.model, 'change:smtpAuth', this.controlSmtpFields, this);
},
controlSmtpFields: function () {

View File

@@ -168,6 +168,9 @@ define('views/email/detail', ['views/detail', 'email-helper'], function (Dep, Em
}, function (view) {
view.render();
view.notify(false);
this.listenTo(view, 'before:save', function () {
this.getView('record').blockUpdateWebSocket(true);
}, this);
this.listenToOnce(view, 'after:save', function () {
this.model.fetch();
this.removeMenuItem('createContact');
@@ -227,6 +230,9 @@ define('views/email/detail', ['views/detail', 'email-helper'], function (Dep, Em
this.removeMenuItem('createCase');
view.close();
}, this);
this.listenTo(view, 'before:save', function () {
this.getView('record').blockUpdateWebSocket(true);
}, this);
});
}.bind(this));
},
@@ -306,6 +312,9 @@ define('views/email/detail', ['views/detail', 'email-helper'], function (Dep, Em
this.removeMenuItem('createLead');
view.close();
}.bind(this));
this.listenTo(view, 'before:save', function () {
this.getView('record').blockUpdateWebSocket(true);
}, this);
}.bind(this));
},

View File

@@ -321,7 +321,10 @@ define('views/fields/array', ['views/fields/base', 'lib!Selectize'], function (D
highlight: false,
searchField: ['label'],
plugins: ['remove_button'],
score: function (search) {
};
if (!this.matchAnyWord) {
selectizeOptions.score = function (search) {
var score = this.getScoreFunction(search);
search = search.toLowerCase();
return function (item) {
@@ -330,8 +333,8 @@ define('views/fields/array', ['views/fields/base', 'lib!Selectize'], function (D
}
return 0;
};
}
};
};
}
if (this.allowCustomOptions) {
selectizeOptions.persist = false;

View File

@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (Dep) {
define('views/fields/attachment-multiple', 'views/fields/base', function (Dep) {
return Dep.extend({
@@ -73,6 +73,7 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D
this.deleteAttachment(id);
}
$div.parent().remove();
this.$el.find('input.file').val(null);
},
'change input.file': function (e) {
var $file = $(e.currentTarget);
@@ -204,6 +205,10 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D
$(window).off('resize.' + this.cid);
}
}.bind(this));
this.on('inline-edit-off', function () {
this.isUploading = false;
}, this);
},
setupSearch: function () {
@@ -413,30 +418,37 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D
attachment.set('file', e.target.result);
attachment.set('field', this.name);
attachment.save({}, {timeout: 0}).then(function () {
if (canceledList.indexOf(attachment.cid) === -1) {
$attachmentBox.trigger('ready');
this.pushAttachment(attachment);
$attachmentBox.attr('data-id', attachment.id);
uploadedCount++;
if (uploadedCount == totalCount && this.isUploading) {
this.isUploading = false;
this.model.trigger('attachment-uploaded:' + this.name);
this.afterAttachmentsUploaded.call(this);
}
}
}.bind(this)).fail(function () {
$attachmentBox.remove();
totalCount--;
if (!totalCount) {
this.isUploading = false;
this.$el.find('.uploading-message').remove();
}
if (uploadedCount == totalCount && this.isUploading) {
this.isUploading = false;
this.afterAttachmentsUploaded.call(this);
}
}.bind(this));
attachment
.save({}, {timeout: 0})
.then(
function () {
if (canceledList.indexOf(attachment.cid) === -1) {
$attachmentBox.trigger('ready');
this.pushAttachment(attachment);
$attachmentBox.attr('data-id', attachment.id);
uploadedCount++;
if (uploadedCount == totalCount && this.isUploading) {
this.model.trigger('attachment-uploaded:' + this.name);
this.afterAttachmentsUploaded.call(this);
this.isUploading = false;
}
}
}.bind(this)
)
.fail(
function () {
$attachmentBox.remove();
totalCount--;
if (!totalCount) {
this.isUploading = false;
this.$el.find('.uploading-message').remove();
}
if (uploadedCount == totalCount && this.isUploading) {
this.isUploading = false;
this.afterAttachmentsUploaded.call(this);
}
}.bind(this)
);
}.bind(this);
fileReader.readAsDataURL(file);
}, this);

View File

@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/fields/base', 'view', function (Dep) {
define('views/fields/base', 'view', function (Dep) {
return Dep.extend({
@@ -359,20 +359,19 @@ Espo.define('views/fields/base', 'view', function (Dep) {
}
if (this.mode != 'search') {
this.attributeList = this.getAttributeList();
this.attributeList = this.getAttributeList(); // for backward compatibility, to be removed
this.listenTo(this.model, 'change', function (model, options) {
if (this.isRendered() || this.isBeingRendered()) {
if (options.ui) {
return;
}
var changed = false;
this.attributeList.forEach(function (attribute) {
this.getAttributeList().forEach(function (attribute) {
if (model.hasChanged(attribute)) {
changed = true;
}
});
}, this);
if (changed && !options.skipReRender) {
this.reRender();

View File

@@ -41,14 +41,17 @@ define('views/fields/complex-created', 'views/fields/base', function (Dep) {
return [this.fieldAt, this.fieldBy];
},
setup: function () {
Dep.prototype.setup.call(this);
init: function () {
this.baseName = this.options.baseName || this.baseName;
this.fieldAt = this.baseName + 'At';
this.fieldBy = this.baseName + 'By';
Dep.prototype.init.call(this);
},
setup: function () {
Dep.prototype.setup.call(this);
this.createField('at');
this.createField('by');
},

View File

@@ -98,41 +98,55 @@ define('views/fields/date', 'views/fields/base', function (Dep) {
}
if (this.mode == 'list' || this.mode == 'detail' || this.mode == 'listLink') {
if (this.getConfig().get('readableDateFormatDisabled') || this.params.useNumericFormat) {
return this.getDateTime().toDisplayDate(value);
}
var d = moment.tz(value + ' OO:OO:00', this.getDateTime().internalDateTimeFormat, this.getDateTime().getTimeZone());
var today = moment().tz(this.getDateTime().getTimeZone()).startOf('day');
var dt = today.clone();
var ranges = {
'today': [dt.unix(), dt.add(1, 'days').unix()],
'tomorrow': [dt.unix(), dt.add(1, 'days').unix()],
'yesterday': [dt.add(-3, 'days').unix(), dt.add(1, 'days').unix()]
};
if (d.unix() >= ranges['today'][0] && d.unix() < ranges['today'][1]) {
return this.translate('Today');
} else if (d.unix() >= ranges['tomorrow'][0] && d.unix() < ranges['tomorrow'][1]) {
return this.translate('Tomorrow');
} else if (d.unix() >= ranges['yesterday'][0] && d.unix() < ranges['yesterday'][1]) {
return this.translate('Yesterday');
}
var readableFormat = this.getDateTime().getReadableDateFormat();
if (d.format('YYYY') == today.format('YYYY')) {
return d.format(readableFormat);
} else {
return d.format(readableFormat + ', YYYY');
}
return this.convertDateValueForDetail(value);
}
return this.getDateTime().toDisplayDate(value);
},
convertDateValueForDetail: function (value) {
if (this.getConfig().get('readableDateFormatDisabled') || this.params.useNumericFormat) {
return this.getDateTime().toDisplayDate(value);
}
var timezone = this.getDateTime().getTimeZone();
var internalDateTimeFormat = this.getDateTime().internalDateTimeFormat;
var readableFormat = this.getDateTime().getReadableDateFormat();
var valueWithTime = value + ' 00:00:00';
var today = moment().tz(timezone).startOf('day');
var dateTime = moment.tz(valueWithTime, internalDateTimeFormat, timezone);
var temp = today.clone();
var ranges = {
'today': [temp.unix(), temp.add(1, 'days').unix()],
'tomorrow': [temp.unix(), temp.add(1, 'days').unix()],
'yesterday': [temp.add(-3, 'days').unix(), temp.add(1, 'days').unix()],
};
if (dateTime.unix() >= ranges['today'][0] && dateTime.unix() < ranges['today'][1]) {
return this.translate('Today');
}
if (dateTime.unix() >= ranges['tomorrow'][0] && dateTime.unix() < ranges['tomorrow'][1]) {
return this.translate('Tomorrow');
}
if (dateTime.unix() >= ranges['yesterday'][0] && dateTime.unix() < ranges['yesterday'][1]) {
return this.translate('Yesterday');
}
// Need to use UTC, otherwise there's a DST issue with old dates.
var dateTime = moment.utc(valueWithTime, internalDateTimeFormat);
if (dateTime.format('YYYY') == today.format('YYYY')) {
return dateTime.format(readableFormat);
}
return dateTime.format(readableFormat + ', YYYY');
},
getDateStringValue: function () {
if (this.mode === 'detail' && !this.model.has(this.name)) {
return '...';
@@ -161,7 +175,7 @@ define('views/fields/date', 'views/fields/base', function (Dep) {
weekStart: this.getDateTime().weekStart,
autoclose: true,
todayHighlight: true,
keyboardNavigation: false,
keyboardNavigation: true,
todayBtn: this.getConfig().get('datepickerTodayButton') || false,
};
@@ -181,15 +195,11 @@ define('views/fields/date', 'views/fields/base', function (Dep) {
options.language = language;
var $datePicker = this.$element.datepicker(options).on('show', function (e) {
$('body > .datepicker.datepicker-dropdown').css('z-index', 1200);
}.bind(this));
var $datePicker = this.$element.datepicker(options);
if (this.mode == 'search') {
var $elAdd = this.$el.find('input.additional');
$elAdd.datepicker(options).on('show', function (e) {
$('body > .datepicker.datepicker-dropdown').css('z-index', 1200);
}.bind(this));
$elAdd.datepicker(options);
$elAdd.parent().find('button.date-picker-btn').on('click', function (e) {
$elAdd.datepicker('show');
});

View File

@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
define('views/fields/file', 'views/fields/link', function (Dep) {
return Dep.extend({
@@ -62,6 +62,7 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
var $div = $(e.currentTarget).parent();
this.deleteAttachment();
$div.parent().remove();
this.$el.find('input.file').val(null);
},
'change input.file': function (e) {
var $file = $(e.currentTarget);
@@ -179,6 +180,10 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
$(window).off('resize.' + this.cid);
}
}.bind(this));
this.on('inline-edit-off', function () {
this.isUploading = false;
}, this);
},
afterRender: function () {
@@ -340,8 +345,8 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
}
if (exceedsMaxFileSize) {
var msg = this.translate('fieldMaxFileSizeError', 'messages')
.replace('{field}', this.getLabelText())
.replace('{max}', maxFileSize);
.replace('{field}', this.getLabelText())
.replace('{max}', maxFileSize);
this.showValidationMessage(msg, '.attachment-button label');
return;
}
@@ -357,6 +362,7 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
isCanceled = true;
this.$el.find('.attachment-button').removeClass('hidden');
this.isUploading = false;
this.$el.find('input.file').val(null);
}.bind(this));
var fileReader = new FileReader();
@@ -370,18 +376,25 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
attachment.set('file', result);
attachment.set('field', this.name);
attachment.save({}, {timeout: 0}).then(function () {
this.isUploading = false;
if (!isCanceled) {
$attachmentBox.trigger('ready');
this.setAttachment(attachment);
}
}.bind(this)).fail(function () {
$attachmentBox.remove();
this.$el.find('.uploading-message').remove();
this.$el.find('.attachment-button').removeClass('hidden');
this.isUploading = false;
}.bind(this));
attachment
.save({}, {timeout: 0})
.then(
function () {
if (!isCanceled && this.isUploading) {
$attachmentBox.trigger('ready');
this.setAttachment(attachment);
this.isUploading = false;
}
}.bind(this)
)
.fail(
function () {
$attachmentBox.remove();
this.$el.find('.uploading-message').remove();
this.$el.find('.attachment-button').removeClass('hidden');
this.isUploading = false;
}.bind(this)
);
}.bind(this));
}.bind(this);
fileReader.readAsDataURL(file);

View File

@@ -38,6 +38,8 @@ define('views/fields/multi-enum', ['views/fields/array', 'lib!Selectize'], funct
editTemplate: 'fields/multi-enum/edit',
matchAnyWord: false,
events: {
},
@@ -172,7 +174,10 @@ define('views/fields/multi-enum', ['views/fields/array', 'lib!Selectize'], funct
searchField: ['label'],
plugins: pluginList,
copyClassesToDropdown: true,
score: function (search) {
};
if (!this.matchAnyWord) {
selectizeOptions.score = function (search) {
var score = this.getScoreFunction(search);
search = search.toLowerCase();
return function (item) {
@@ -181,8 +186,8 @@ define('views/fields/multi-enum', ['views/fields/array', 'lib!Selectize'], funct
}
return 0;
};
}
};
};
}
if (this.allowCustomOptions) {
selectizeOptions.persist = false;

View File

@@ -51,7 +51,7 @@ define('views/fields/person-name', 'views/fields/varchar', function (Dep) {
data.middleValue = this.model.get(this.middleField);
data.salutationOptions = this.model.getFieldParam(this.salutationField, 'options');
if (this.model === 'edit') {
if (this.mode === 'edit') {
data.firstMaxLength = this.model.getFieldParam(this.firstField, 'maxLength');
data.lastMaxLength = this.model.getFieldParam(this.lastField, 'maxLength');
data.middleMaxLength = this.model.getFieldParam(this.middleField, 'maxLength');

View File

@@ -47,7 +47,7 @@ define('views/inbound-email/record/detail', 'views/record/detail', function (Dep
this.controlSmtpFields();
this.controlSentFolderField();
this.listenTo(this.model, 'change:useSmtp', this.controlSmtpFields, this);
this.listenTo(this.model, 'change:smtpAuth', this.controlSmtpAuthField, this);
this.listenTo(this.model, 'change:smtpAuth', this.controlSmtpFields, this);
this.listenTo(this.model, 'change:storeSentEmails', this.controlSentFolderField, this);
},

View File

@@ -199,7 +199,7 @@ define('views/main', 'view', function (Dep) {
var type = false;
['actions', 'dropdown', 'buttons'].forEach(function (t) {
this.menu[t].forEach(function (item, i) {
(this.menu[t] || []).forEach(function (item, i) {
item = item || {};
if (item.name == name) {
index = i;
@@ -244,7 +244,7 @@ define('views/main', 'view', function (Dep) {
hideHeaderActionItem: function (name) {
['actions', 'dropdown', 'buttons'].forEach(function (t) {
this.menu[t].forEach(function (item, i) {
(this.menu[t] || []).forEach(function (item, i) {
item = item || {};
if (item.name == name) {
item.hidden = true;
@@ -260,7 +260,7 @@ define('views/main', 'view', function (Dep) {
showHeaderActionItem: function (name) {
['actions', 'dropdown', 'buttons'].forEach(function (t) {
this.menu[t].forEach(function (item, i) {
(this.menu[t] || []).forEach(function (item, i) {
item = item || {};
if (item.name == name) {
item.hidden = false;
@@ -276,7 +276,7 @@ define('views/main', 'view', function (Dep) {
hasMenuVisibleDropdownItems: function () {
var hasItems = false;
this.menu.dropdown.forEach(function (item) {
(this.menu.dropdown || []).forEach(function (item) {
if (!item.hidden) hasItems = true;
});
return hasItems;

View File

@@ -169,6 +169,10 @@ Espo.define('views/modals/edit', 'views/modal', function (Dep) {
this.dialog.close();
}, this);
editView.once('before:save', function () {
this.trigger('before:save', model);
}, this);
var $buttons = this.dialog.$el.find('.modal-footer button');
$buttons.addClass('disabled').attr('disabled', 'disabled');

View File

@@ -144,6 +144,8 @@ define('views/modals/password-change-request', 'views/modal', function (Dep) {
var msg = this.translate('uniqueLinkHasBeenSent', 'messages', 'User');
msg += ' ' + this.translate('passwordRecoverySentIfMatched', 'messages', 'User');
this.$el.find('.cell-userName').addClass('hidden');
this.$el.find('.cell-emailAddress').addClass('hidden');

View File

@@ -57,7 +57,7 @@ define('views/modals/select-records', ['views/modal', 'search-manager'], functio
events: {
'click button[data-action="create"]': function () {
this.create();
this.create();
},
'click .list a': function (e) {
e.preventDefault();
@@ -256,6 +256,11 @@ define('views/modals/select-records', ['views/modal', 'search-manager'], functio
},
create: function () {
if (this.options.triggerCreateEvent) {
this.trigger('create');
return;
}
var self = this;
this.notify('Loading...');
@@ -285,4 +290,3 @@ define('views/modals/select-records', ['views/modal', 'search-manager'], functio
},
});
});

View File

@@ -1487,10 +1487,19 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
for (var p in simplifiedLayout) {
var panel = {};
panel.label = simplifiedLayout[p].label || null;
if ('customLabel' in simplifiedLayout[p]) {
panel.customLabel = simplifiedLayout[p].customLabel;
panel.label = simplifiedLayout[p].customLabel;
if (panel.label) {
panel.label = this.getLanguage().translate(panel.label, 'panelCustomLabels', this.entityType);
}
} else {
panel.label = simplifiedLayout[p].label || null;
if (panel.label) {
panel.label = this.getLanguage().translate(panel.label, 'labels', this.entityType);
}
}
panel.name = simplifiedLayout[p].name || null;
panel.style = simplifiedLayout[p].style || 'default';
panel.rows = [];
@@ -1837,8 +1846,14 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
}
},
blockUpdateWebSocket: function () {
blockUpdateWebSocket: function (toUnblock) {
this.updateWebSocketIsBlocked = true;
if (toUnblock) {
setTimeout(function () {
this.unblockUpdateWebSocket();
}.bind(this), this.blockUpdateWebSocketPeriod || 500);
}
},
unblockUpdateWebSocket: function () {

View File

@@ -214,10 +214,12 @@ define('views/record/panels-container', 'view', function (Dep) {
view.disabled = false;
view.trigger('show');
var fields = view.getFieldViews();
if (fields) {
for (var i in fields) {
fields[i].reRender();
if (view.getFieldViews) {
var fields = view.getFieldViews();
if (fields) {
for (var i in fields) {
fields[i].reRender();
}
}
}
}

View File

@@ -92,6 +92,8 @@ define('views/record/panels/relationship', ['views/record/panels/bottom', 'searc
this.defs.view = false;
}
var hasCreate = false;
if (this.defs.create) {
if (this.getAcl().check(this.scope, 'create') && !~this.noCreateScopeList.indexOf(this.scope)) {
this.buttonList.push({
@@ -104,6 +106,7 @@ define('views/record/panels/relationship', ['views/record/panels/bottom', 'searc
link: this.link,
}
});
hasCreate = true;
}
}
@@ -116,6 +119,7 @@ define('views/record/panels/relationship', ['views/record/panels/bottom', 'searc
data.boolFilterList = this.defs.selectBoolFilterList;
}
data.massSelect = this.defs.massSelect;
data.createButton = hasCreate;
this.actionList.unshift({
label: 'Select',

View File

@@ -58,6 +58,10 @@ define('views/role/record/table', 'view', function (Dep) {
booleanActionList: ['create'],
defaultLevels: {
delete: 'no',
},
colors: {
yes: '#6BC924',
all: '#6BC924',
@@ -121,15 +125,37 @@ define('views/role/record/table', 'view', function (Dep) {
if (this.lowestLevelByDefault) {
$select.find('option').last().prop('selected', true);
} else {
$select.find('option').first().prop('selected', true);
var setFirst = true;
var action = $select.data('role-action');
var defaultLevel = null;
if (action) defaultLevel = this.defaultLevels[action];
if (defaultLevel) {
var $option = $select.find('option[value="'+defaultLevel+'"]');
if ($option.length) {
$option.prop('selected', true);
setFirst = false;
}
}
if (setFirst) {
$select.find('option').first().prop('selected', true);
}
}
$select.trigger('change');
this.controlSelectColor($select);
}.bind(this));
} else {
$dropdowns.attr('disabled', 'disabled');
$dropdowns.addClass('hidden');
}
}
this.controlSelectColor($(e.currentTarget));
},
'change select.scope-action': function (e) {
this.controlSelectColor($(e.currentTarget));
},
'change select.field-action': function (e) {
this.controlSelectColor($(e.currentTarget));
},
},
getTableDataList: function () {
@@ -250,6 +276,13 @@ define('views/role/record/table', 'view', function (Dep) {
if (this.mode == 'edit') {
this.template = 'role/table-edit';
}
this.once('remove', function () {
$(window).off('scroll.scope-' + this.cid);
$(window).off('resize.scope-' + this.cid);
$(window).off('scroll.field-' + this.cid);
$(window).off('resize.field-' + this.cid);
}, this);
},
setupData: function () {
@@ -427,6 +460,11 @@ define('views/role/record/table', 'view', function (Dep) {
}, this);
}, this);
this.initStickyHeader('scope');
this.initStickyHeader('field');
this.setSelectColors();
}
},
@@ -447,6 +485,8 @@ define('views/role/record/table', 'view', function (Dep) {
$o.removeAttr('disabled');
}
}.bind(this));
this.controlSelectColor($edit);
},
controlEditSelect: function (scope, value, dontChange) {
@@ -466,6 +506,8 @@ define('views/role/record/table', 'view', function (Dep) {
$o.removeAttr('disabled');
}
}.bind(this));
this.controlSelectColor($edit);
},
controlStreamSelect: function (scope, value, dontChange) {
@@ -485,6 +527,8 @@ define('views/role/record/table', 'view', function (Dep) {
$o.removeAttr('disabled');
}
}.bind(this));
this.controlSelectColor($stream);
},
controlDeleteSelect: function (scope, value, dontChange) {
@@ -504,6 +548,8 @@ define('views/role/record/table', 'view', function (Dep) {
$o.removeAttr('disabled');
}
}.bind(this));
this.controlSelectColor($delete);
},
showAddFieldModal: function (scope) {
@@ -568,5 +614,97 @@ define('views/role/record/table', 'view', function (Dep) {
}, this);
},
initStickyHeader: function (type) {
var $sticky = this.$el.find('.sticky-header-' + type);
var $window = $(window);
var screenWidthXs = this.getThemeManager().getParam('screenWidthXs');
var $buttonContainer = $('.detail-button-container');
var $table = this.$el.find('table.'+type+'-level');
if (!$buttonContainer.length) return;
var handle = function (e) {
if ($(window.document).width() < screenWidthXs) {
$sticky.addClass('hidden');
return;
}
var stickTopPosition = $buttonContainer.get(0).getBoundingClientRect().top + $buttonContainer.outerHeight();
var topEdge = $table.position().top;
topEdge += $buttonContainer.height();
topEdge += $table.find('tr > th').height();
var bottomEdge = topEdge + $table.outerHeight(true);
var scrollTop = $window.scrollTop();
var width = $table.width();
if (scrollTop > topEdge && scrollTop < bottomEdge) {
$sticky.css({
position: 'fixed',
marginTop: stickTopPosition + 'px',
top: 0,
width: width + 'px',
marginLeft: '1px',
});
$sticky.removeClass('hidden');
} else {
$sticky.addClass('hidden');
}
}.bind(this);
$window.off('scroll.' + type + '-' + this.cid);
$window.on('scroll.' + type + '-' + this.cid, handle);
$window.off('resize.' + type + '-' + this.cid);
$window.on('resize.' + type + '-' + this.cid, handle);
},
setSelectColors: function () {
this.$el.find('select[data-type="access"]').each(function (i, el) {
var $select = $(el);
this.controlSelectColor($select);
}.bind(this));
this.$el.find('select.scope-action').each(function (i, el) {
var $select = $(el);
this.controlSelectColor($select);
}.bind(this));
this.$el.find('select.field-action').each(function (i, el) {
var $select = $(el);
this.controlSelectColor($select);
}.bind(this));
},
controlSelectColor: function ($select) {
var level = $select.val();
var color = this.colors[level] || '';
if (level === 'not-set') color = '';
$select.css('color', color);
$select.children().each(function (j, el) {
var $o = $(el);
var level = $o.val();
var color = this.colors[level] || '';
if (level === 'not-set') color = '';
if ($o.attr('disabled')) {
color = '';
}
$o.css('color', color);
}.bind(this));
},
});
});

View File

@@ -25,14 +25,71 @@
* 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.
************************************************************************/
Espo.define('views/scheduled-job/fields/scheduling', 'views/fields/base', function (Dep) {
define('views/scheduled-job/fields/scheduling',
['views/fields/varchar', 'lib!client/lib/cronstrue-i18n.min.js'], function (Dep) {
return Dep.extend({
forceTrim: true,
setup: function () {
Dep.prototype.setup.call(this);
}
if (this.isEditMode() || this.isDetailMode()) {
this.listenTo(this.model, 'change:' + this.name, function () {
this.showText();
}, this);
}
},
afterRender: function () {
Dep.prototype.afterRender.call(this);
if (this.isEditMode() || this.isDetailMode()) {
var $text = this.$text = $('<div class="small text-success"/>');
this.$el.append($text);
this.showText();
}
},
showText: function () {
if (!this.$text || !this.$text.length) return;
var exp = this.model.get(this.name);
if (!exp) {
this.$text.text('');
return;
}
if (exp == '* * * * *') {
this.$text.text(this.translate('As often as possible', 'labels', 'ScheduledJob'));
return;
}
var locale = 'en';
var localeList = Object.keys(cronstrue.default.locales);
var language = this.getLanguage().name;
if (~localeList.indexOf(language)) {
locale = language;
} else if (~localeList.indexOf(language.split('_')[0])) {
locale = language.split('_')[0];
}
try {
var text = cronstrue.toString(exp, {
use24HourTimeFormat: !this.getDateTime().hasMeridian(),
locale: locale,
});
} catch (e) {
text = this.translate('Not valid');
}
this.$text.text(text);
},
});
});

View File

@@ -30,6 +30,8 @@ define('views/settings/fields/currency-list', 'views/fields/multi-enum', functio
return Dep.extend({
matchAnyWord: true,
setupOptions: function () {
this.params.options = this.getMetadata().get(['app', 'currency', 'list']) || [];

View File

@@ -334,7 +334,7 @@ define('views/site/navbar', 'view', function (Dep) {
$navbar.css('overflow', 'visible');
}
var navbarBaseWidth = this.getThemeManager().getParam('navbarBaseWidth') || 556;
var navbarBaseWidth = this.getThemeManager().getParam('navbarBaseWidth') || 565;
var tabCount = this.tabList.length;
@@ -462,6 +462,8 @@ define('views/site/navbar', 'view', function (Dep) {
updateSizeForVertical();
this.$el.find('.notifications-badge-container').insertAfter(this.$el.find('.quick-create-container'));
this.adjustBodyMinHeight();
},
getNavbarHeight: function () {
@@ -479,7 +481,7 @@ define('views/site/navbar', 'view', function (Dep) {
},
adjustBodyMinHeightVertical: function () {
var minHeight = this.$tabs.height() + this.getStaticItemsHeight();
var minHeight = this.$tabs.get(0).scrollHeight + this.getStaticItemsHeight();
var moreHeight = 0;
this.$more.find('> li:visible').each(function (i, el) {

View File

@@ -610,7 +610,7 @@ define('views/stream/panel', ['views/record/panels/relationship', 'lib!Textcompl
preview: function () {
this.createView('dialog', 'views/modal', {
templateContent: '<div class="complex-text">{{complexText viewObject.options.text}}</div>',
templateContent: '<div class="complex-text">{{complexText viewObject.options.text linksInNewTab=true}}</div>',
text: this.$textarea.val(),
headerText: this.translate('Preview'),
backdrop: true,

View File

@@ -87,10 +87,10 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
name: 'generateNewPassword',
label: 'Generate New Password',
action: 'generateNewPassword',
hidden: !this.model.get('emailAddress') || !this.getConfig().get('smtpServer'),
hidden: !this.model.get('emailAddress'),
});
if (!this.model.get('emailAddress') && this.getConfig().get('smtpServer')) {
if (!this.model.get('emailAddress')) {
this.listenTo(this.model, 'sync', function () {
if (this.model.get('emailAddress')) {
this.showActionItem('generateNewPassword');

File diff suppressed because it is too large Load Diff

View File

@@ -112,3 +112,56 @@
color: @gray-light;
}
.btn-text {
color: @gray-dark;
border-radius: 0;
.far,
.fas {
color: @gray-soft;
}
&,
&:active,
&.active,
&[disabled],
fieldset[disabled] & {
background-color: transparent;
.box-shadow(none);
}
&,
&:hover,
&:focus,
&:active {
border-color: transparent;
}
&:hover,
&:focus {
color: @gray-dark;
text-decoration: none;
background-color: transparent;
.far,
.fas {
color: @gray-dark;
}
}
&.disabled,
&[disabled],
fieldset[disabled] & {
&:hover,
&:focus {
color: @gray-light;
text-decoration: none;
}
color: @gray-light;
.far,
.fas {
color: @gray-light;
}
}
&.active {
border-bottom: 1px solid @gray-light;
}
font-weight: 400;
}

View File

@@ -0,0 +1,178 @@
.modal-header.fixed-height {
height: 40px;
max-height: 40px;
overflow: hidden;
.modal-title {
width: ~"calc(100% - 20px)";
> .modal-title-text {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
display: inline-block;
}
&.overlapped {
> .modal-title-text {
&:before {
content: " ";
position: absolute;
right: 34px;
background: linear-gradient(to right, transparent, @modal-header-bg);
height: 30px;
width: 60px;
}
}
}
}
}
.modal-header a {
color: @text-color;
}
.modal-container.overlaid .modal-content {
.box-shadow(none);
}
.modal-header {
background-color: @modal-header-bg;
}
.modal-header {
padding: @modal-title-padding @panel-padding;
border-bottom: 0 solid @modal-header-border-color;
min-height: (@modal-title-padding + @modal-title-line-height);
}
.modal-header .close {
font-size: 31px;
font-weight: normal;
margin-top: -4px;
max-height: @font-size-h3;
> span {
position: relative;
max-height: @font-size-h3;
height: @font-size-h3;
display: inline-block;
}
opacity: 0.4;
}
.modal-footer {
margin-top: @modal-title-padding;
padding: @panel-padding @panel-padding @panel-padding;
}
.modal-footer > .main-btn-group {
float: right;
}
.modal-footer > .additional-btn-group {
float: left;
}
.modal-dialog > .modal-content {
background-clip: border-box;
border-color: @default-border-color;
border-width: 0;
}
.modal-footer {
margin-top: 0;
}
.modal-header {
padding-top: 8px;
padding-bottom: 8px;
}
.modal-footer > .main-btn-group {
float: left;
}
.modal-footer > .additional-btn-group {
float: right;
}
.dialog-confirm .modal-footer > .main-btn-group {
float: right;
}
.dialog-confirm .modal-footer > .additional-btn-group {
float: left;
}
.modal-footer {
padding-top: @modal-title-padding;
padding-bottom: @modal-title-padding;
}
@media (min-width: @screen-sm-min) {
.modal-dialog {
margin: 0 0 0 auto;
}
.modal-content .note-editor > .modal > .modal-dialog {
margin: 0 0 0 -@container-padding;
}
.dialog-confirm > .modal-dialog {
margin: 0 auto;
}
}
@media (max-width: @screen-sm-min) {
.modal-dialog {
margin: 0 0 0 0;
}
}
.modal-dialog .modal-body .list-buttons-container .sticked-bar {
top: 94px !important;
width: 100% !important;
right: 0 !important;
left: unset;
padding-top: 0;
padding-left: @container-padding - 1px;
}
@media screen and (max-width: (@screen-sm-min - 1px)) {
.modal-dialog {
margin: 0 0;
}
.modal-dialog .image-container {
margin-left: -@panel-padding;
margin-right: -@panel-padding;
}
}
.dialog-confirm > .modal-dialog > .modal-content {
text-align: center;
}
@media screen and (min-width: @screen-sm-min) {
.modal-dialog {
width: 740px;
}
}
@media screen and (min-width: @screen-md-min) {
.modal-dialog,
.dialog-centered {
width: 900px;
}
.dialog-confirm > .modal-dialog,
.dialog-centered > .modal-dialog {
width: 550px;
}
}
.modal-body > .no-side-margin {
margin-left: -@panel-padding;
margin-right: -@panel-padding;
}
.modal.in .modal.in {
overflow: visible !important;
overflow-y: visible !important;
}

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