Compare commits
150 Commits
6.0.0-beta1
..
6.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
| fb9ff0b8c0 | |||
| d1a62baa12 | |||
| cdec7ccfe8 | |||
| 25bc3a40f7 | |||
| 02e5c1fc44 | |||
| 844e9dd8e3 | |||
| 39f2ec83d5 | |||
| e4d2af03a5 | |||
| 37616d69bf | |||
| 4e36f71dd8 | |||
| b5877ac32a | |||
| 92de588a6e | |||
| 700f4b5b10 | |||
| 9c0a7b6778 | |||
| 9a54c66f68 | |||
| 83332178a0 | |||
| fb33c2acf1 | |||
| 3c5914b5d9 | |||
| 8c48f72a3c | |||
| a8ce780718 | |||
| 31eff86578 | |||
| 892f9f01f6 | |||
| 860a58847d | |||
| 1ed90d2306 | |||
| aff0b350f3 | |||
| 651b0be5ed | |||
| cead33c09d | |||
| 80602c084f | |||
| b0420b8b93 | |||
| b47018cbab | |||
| ea69ab6eaf | |||
| 92887401b2 | |||
| df0bda6324 | |||
| 9d67808496 | |||
| cf33a98f20 | |||
| afbda344ec | |||
| 1079597584 | |||
| af9b411a4c | |||
| cd49d951d8 | |||
| 7d63c114aa | |||
| af6f05ba07 | |||
| 46f8eb0cdb | |||
| c543bb5a5b | |||
| d1997089e4 | |||
| 57dd0177f9 | |||
| ba76e5ee3d | |||
| 76c93b842c | |||
| e73bff5ddf | |||
| 7d5c0d754e | |||
| e08a74b129 | |||
| be07aafaf4 | |||
| e2bf27e524 | |||
| b0157adbe2 | |||
| 239593afb5 | |||
| b97d764588 | |||
| bf8e7af984 | |||
| bf68c475e5 | |||
| 29624239ba | |||
| 4cb3c4918b | |||
| 26fa0d65e1 | |||
| 958baed3df | |||
| 40d6fb7565 | |||
| a7551b7dbf | |||
| 9ed33c4dc4 | |||
| 6b18fbe8f7 | |||
| 53b9880ea8 | |||
| 62538664df | |||
| f678fb16cc | |||
| fe09aa1cd9 | |||
| 710decc610 | |||
| 5d481d3528 | |||
| 8bcd3b72c5 | |||
| 0a0da810e9 | |||
| e630c046fe | |||
| 2857b5c53d | |||
| 0965c61f55 | |||
| 9dd318b176 | |||
| f8beca720c | |||
| d8f58dcc1d | |||
| 32aaaa350e | |||
| 0f6d1c6817 | |||
| 8c5a342a5a | |||
| 257401898b | |||
| e7ff3f903a | |||
| 6f82638b5d | |||
| 0625ce51c4 | |||
| f5670436d0 | |||
| 5d20a56ac8 | |||
| 020cf9713a | |||
| fe1361fc9c | |||
| 36621be496 | |||
| 25690030d1 | |||
| fcd6be3a0c | |||
| ad8dfa487c | |||
| 7f6f54628d | |||
| 8ea0b072c6 | |||
| 0df6248226 | |||
| 26a132658e | |||
| d02529ee86 | |||
| 0cec0fef51 | |||
| e56f6ab75d | |||
| a4980aa8f2 | |||
| fd219b49be | |||
| b90579aadc | |||
| 0b39d7f6b6 | |||
| a44ce06162 | |||
| f1eb2ab5d5 | |||
| 9a965bfba4 | |||
| 7349f37786 | |||
| 3d03dfaef7 | |||
| 5bf88eaff1 | |||
| f3c4b2ba39 | |||
| 92f6fb4588 | |||
| 2d6d7fd618 | |||
| 3a1d997d5a | |||
| 5bbb259c95 | |||
| b8e44767b3 | |||
| 6b486beb5c | |||
| aab1265e4e | |||
| 676181a4be | |||
| 839e5b7c74 | |||
| aff7003ceb | |||
| 66aa60f303 | |||
| b196811cf9 | |||
| 3fdddadd53 | |||
| 7af2d1d3ac | |||
| 63670e3245 | |||
| 389931bf22 | |||
| 76a67d15a3 | |||
| 8e65f2d375 | |||
| 56737f5d67 | |||
| 20f8fae2f0 | |||
| 17b5d77d33 | |||
| cbe32c88b6 | |||
| dee291f3f6 | |||
| 0b20c8e13f | |||
| c0af0173e9 | |||
| 79f4c4b5d2 | |||
| 31c8be2ee1 | |||
| 061b237032 | |||
| d3466e201d | |||
| c69c7d2194 | |||
| 05e1624e47 | |||
| 03e32412ca | |||
| 451a2376a8 | |||
| 167c774446 | |||
| 054262a002 | |||
| ecbc3c537f | |||
| d51bbfdfab | |||
| ae343cddbe |
@@ -99,7 +99,7 @@ class FieldManager
|
||||
$fieldManagerTool->create($scope, $name, get_object_vars($data));
|
||||
|
||||
try {
|
||||
$this->dataManager->rebuild($scope);
|
||||
$this->dataManager->rebuild([$scope]);
|
||||
}
|
||||
catch (Error $e) {
|
||||
$fieldManagerTool->delete($scope, $data->name);
|
||||
@@ -131,7 +131,7 @@ class FieldManager
|
||||
$fieldManagerTool->update($scope, $name, get_object_vars($data));
|
||||
|
||||
if ($fieldManagerTool->isChanged()) {
|
||||
$this->dataManager->rebuild($scope);
|
||||
$this->dataManager->rebuild([$scope]);
|
||||
} else {
|
||||
$this->dataManager->clearCache();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ namespace Espo\Controllers;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Core\{
|
||||
Authentication\LDAP\Utils as LDAPUtils,
|
||||
Authentication\LDAP\Client as LDAPClient,
|
||||
};
|
||||
|
||||
class Settings extends \Espo\Core\Controllers\Base
|
||||
{
|
||||
protected function getConfigData()
|
||||
@@ -86,11 +91,14 @@ class Settings extends \Espo\Core\Controllers\Base
|
||||
|
||||
$data = get_object_vars($data);
|
||||
|
||||
$ldapUtils = new \Espo\Core\Utils\Authentication\LDAP\Utils();
|
||||
$ldapUtils = new LDAPUtils();
|
||||
|
||||
$options = $ldapUtils->normalizeOptions($data);
|
||||
|
||||
$ldapClient = new \Espo\Core\Utils\Authentication\LDAP\Client($options);
|
||||
$ldapClient->bind(); //an exception if no connection
|
||||
$ldapClient = new LDAPClient($options);
|
||||
|
||||
//an exception if no connection
|
||||
$ldapClient->bind();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,13 @@ class RequestWrapper implements ApiRequest
|
||||
|
||||
public function getContentType() : ?string
|
||||
{
|
||||
return $this->getHeader('Content-Type');
|
||||
if (!$this->hasHeader('Content-Type')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$contentType = $this->request->getHeader('Content-Type')[0];
|
||||
|
||||
return strtolower($contentType);
|
||||
}
|
||||
|
||||
public function getBodyContents() : ?string
|
||||
@@ -163,7 +169,7 @@ class RequestWrapper implements ApiRequest
|
||||
{
|
||||
$contents = $this->getBodyContents();
|
||||
|
||||
if (strtolower($this->getContentType()) === 'application/json' && $contents) {
|
||||
if ($this->getContentType() === 'application/json' && $contents) {
|
||||
$this->parsedBody = json_decode($contents);
|
||||
|
||||
if (is_array($this->parsedBody)) {
|
||||
|
||||
@@ -86,6 +86,7 @@ class Application
|
||||
{
|
||||
if (!$className || !class_exists($className)) {
|
||||
$this->getLog()->error("Application runner '{$className}' does not exist.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,6 +162,7 @@ class Application
|
||||
protected function initAutoloads()
|
||||
{
|
||||
$autoload = $this->getInjectableFactory()->create(Autoload::class);
|
||||
|
||||
$autoload->register();
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ use Espo\Core\{
|
||||
Api\ResponseWrapper,
|
||||
Api\RouteProcessor,
|
||||
Utils\Route,
|
||||
Utils\Log,
|
||||
};
|
||||
|
||||
use Slim\{
|
||||
@@ -63,12 +64,15 @@ class Api implements ApplicationRunner
|
||||
protected $injectableFactory;
|
||||
protected $applicationUser;
|
||||
protected $routeUtil;
|
||||
protected $log;
|
||||
|
||||
public function __construct(InjectableFactory $injectableFactory, ApplicationUser $applicationUser, Route $routeUtil)
|
||||
{
|
||||
public function __construct(
|
||||
InjectableFactory $injectableFactory, ApplicationUser $applicationUser, Route $routeUtil, Log $log
|
||||
) {
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
$this->applicationUser = $applicationUser;
|
||||
$this->routeUtil = $routeUtil;
|
||||
$this->log = $log;
|
||||
}
|
||||
|
||||
public function run()
|
||||
@@ -85,7 +89,7 @@ class Api implements ApplicationRunner
|
||||
$this->addRoute($slim, $item);
|
||||
}
|
||||
|
||||
$slim->addErrorMiddleware(false, true, true);
|
||||
$slim->addErrorMiddleware(false, true, true, $this->log);
|
||||
|
||||
$slim->run();
|
||||
}
|
||||
|
||||
@@ -29,9 +29,8 @@
|
||||
|
||||
namespace Espo\Core\ApplicationRunners;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\Error,
|
||||
InjectableFactory,
|
||||
EntryPointManager,
|
||||
ApplicationUser,
|
||||
@@ -39,7 +38,6 @@ use Espo\Core\{
|
||||
Portal\Application as PortalApplication,
|
||||
Utils\Route,
|
||||
Utils\ClientManager,
|
||||
|
||||
Authentication\Authentication,
|
||||
Api\Auth as ApiAuth,
|
||||
Api\ErrorOutput as ApiErrorOutput,
|
||||
@@ -48,18 +46,12 @@ use Espo\Core\{
|
||||
};
|
||||
|
||||
use Slim\{
|
||||
App as SlimApp,
|
||||
Factory\AppFactory as SlimAppFactory,
|
||||
};
|
||||
|
||||
use Psr\Http\{
|
||||
Message\ResponseInterface as Psr7Response,
|
||||
Message\ServerRequestInterface as Psr7Request,
|
||||
Server\RequestHandlerInterface as Psr7RequestHandler,
|
||||
ResponseEmitter,
|
||||
Factory\ServerRequestCreatorFactory,
|
||||
Psr7\Response,
|
||||
};
|
||||
|
||||
use StdClass;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
@@ -113,30 +105,19 @@ class EntryPoint implements ApplicationRunner
|
||||
}
|
||||
}
|
||||
|
||||
$slim = SlimAppFactory::create();
|
||||
$request = (ServerRequestCreatorFactory::create())->createServerRequestFromGlobals();
|
||||
|
||||
$slim->setBasePath(Route::detectBasePath());
|
||||
if ($request->getMethod() !== 'GET') {
|
||||
throw new Error("Only GET request allowed for entry points.");
|
||||
}
|
||||
|
||||
$slim->add(
|
||||
function (Psr7Request $request, Psr7RequestHandler $handler) use (
|
||||
$entryPoint, $data, $authRequired, $authNotStrict, $slim
|
||||
) : Psr7Response {
|
||||
$requestWrapped = new RequestWrapper($request, $slim->getBasePath());
|
||||
$responseWrapped = new ResponseWrapper($handler->handle($request));
|
||||
$requestWrapped = new RequestWrapper($request, Route::detectBasePath());
|
||||
|
||||
$this->processRequest($entryPoint, $requestWrapped, $responseWrapped, $data, $authRequired, $authNotStrict);
|
||||
$responseWrapped = new ResponseWrapper(new Response());
|
||||
|
||||
return $responseWrapped->getResponse();
|
||||
}
|
||||
);
|
||||
$this->processRequest($entryPoint, $requestWrapped, $responseWrapped, $data, $authRequired, $authNotStrict);
|
||||
|
||||
$route = Route::detectEntryPointRoute();
|
||||
|
||||
$slim->get($route, function (Psr7Request $request, Psr7Response $response) : Psr7Response {
|
||||
return $response;
|
||||
});
|
||||
|
||||
$slim->run();
|
||||
(new ResponseEmitter())->emit($responseWrapped->getResponse());
|
||||
}
|
||||
|
||||
protected function processRequest(
|
||||
|
||||
@@ -55,11 +55,13 @@ class ApplicationState
|
||||
}
|
||||
|
||||
/**
|
||||
* Get portal ID (if an applicaition is portal).
|
||||
* Get a portal ID (if an applicaition is portal).
|
||||
*/
|
||||
public function getPortalId() : string
|
||||
{
|
||||
if (!$this->isPortal()) throw new Error("Can't get portal ID for non-portal application.");
|
||||
if (!$this->isPortal()) {
|
||||
throw new Error("Can't get portal ID for non-portal application.");
|
||||
}
|
||||
|
||||
return $this->getPortal()->id;
|
||||
}
|
||||
@@ -69,7 +71,9 @@ class ApplicationState
|
||||
*/
|
||||
public function getPortal() : PortalEntity
|
||||
{
|
||||
if (!$this->isPortal()) throw new Error("Can't get portal for non-portal application.");
|
||||
if (!$this->isPortal()) {
|
||||
throw new Error("Can't get portal for non-portal application.");
|
||||
}
|
||||
|
||||
return $this->container->get('portal');
|
||||
}
|
||||
@@ -83,21 +87,38 @@ class ApplicationState
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current logged user. If no auth is applied, then system user will be returned.
|
||||
* Get a current logged user. If no auth is applied, then the system user will be returned.
|
||||
*/
|
||||
public function getUser() : UserEntity
|
||||
{
|
||||
if (!$this->hasUser()) throw new Error("User is not yet available.");
|
||||
if (!$this->hasUser()) {
|
||||
throw new Error("User is not yet available.");
|
||||
}
|
||||
|
||||
return $this->container->get('user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ID of a current logged user. If no auth is applied, then the system user will be returned.
|
||||
*/
|
||||
public function getUserId() : string
|
||||
{
|
||||
return $this->getUser()->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a user is logged.
|
||||
*/
|
||||
public function isLogged() : bool
|
||||
{
|
||||
if (!$this->container->has('user')) return false;
|
||||
if ($this->getUser()->isSystem()) return false;
|
||||
if (!$this->container->has('user')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getUser()->isSystem()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -106,7 +127,10 @@ class ApplicationState
|
||||
*/
|
||||
public function isAdmin() : bool
|
||||
{
|
||||
if (!$this->isLogged()) return false;
|
||||
if (!$this->isLogged()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getUser()->isAdmin();
|
||||
}
|
||||
|
||||
@@ -116,7 +140,10 @@ class ApplicationState
|
||||
*/
|
||||
public function isApi() : bool
|
||||
{
|
||||
if (!$this->isLogged()) return false;
|
||||
if (!$this->isLogged()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getUser()->isApi();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ class Authentication
|
||||
|
||||
if ($code) {
|
||||
if (!$impl->verifyCode($loggedUser, $code)) {
|
||||
Result::fail('Code not verified');
|
||||
return Result::fail('Code not verified');
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
@@ -37,10 +37,6 @@ class Utils
|
||||
|
||||
protected $options = null;
|
||||
|
||||
/**
|
||||
* Association between LDAP and Espo fields
|
||||
* @var array
|
||||
*/
|
||||
protected $fieldMap = [
|
||||
'host' => 'ldapHost',
|
||||
'port' => 'ldapPort',
|
||||
@@ -73,11 +69,6 @@ class Utils
|
||||
'portalUserRolesIds' => 'ldapPortalUserRolesIds',
|
||||
];
|
||||
|
||||
/**
|
||||
* Permitted Espo Options
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permittedEspoOptions = [
|
||||
'createEspoUser',
|
||||
'userNameAttribute',
|
||||
@@ -96,9 +87,7 @@ class Utils
|
||||
];
|
||||
|
||||
/**
|
||||
* accountCanonicalForm Map between Espo and Laminas value
|
||||
*
|
||||
* @var array
|
||||
* accountCanonicalForm Map between Espo and Laminas value.
|
||||
*/
|
||||
protected $accountCanonicalFormMap = [
|
||||
'Dn' => 1,
|
||||
@@ -107,7 +96,6 @@ class Utils
|
||||
'Principal' => 4,
|
||||
];
|
||||
|
||||
|
||||
public function __construct(Config $config = null)
|
||||
{
|
||||
if (isset($config)) {
|
||||
@@ -121,7 +109,7 @@ class Utils
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Options from espo config according to $this->fieldMap
|
||||
* Get Options from espo config according to $this->fieldMap.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@@ -131,10 +119,10 @@ class Utils
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
$options = array();
|
||||
$options = [];
|
||||
foreach ($this->fieldMap as $ldapName => $espoName) {
|
||||
|
||||
$option = $this->getConfig()->get($espoName);
|
||||
|
||||
if (isset($option)) {
|
||||
$options[$ldapName] = $option;
|
||||
}
|
||||
@@ -162,7 +150,7 @@ class Utils
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ldap option
|
||||
* Get an ldap option.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $returns Return value
|
||||
@@ -182,7 +170,7 @@ class Utils
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Laminas options for using Laminas\Ldap
|
||||
* Get Laminas options for using Laminas\Ldap.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@@ -193,5 +181,4 @@ class Utils
|
||||
|
||||
return $zendOptions;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,8 +44,12 @@ use Espo\Core\{
|
||||
Utils\Language,
|
||||
ApplicationUser,
|
||||
Authentication\Result,
|
||||
Authentication\LDAP\Utils as LDAPUtils,
|
||||
Authentication\LDAP\Client as LDAPClient,
|
||||
};
|
||||
|
||||
use Exception;
|
||||
|
||||
class LDAP extends Espo
|
||||
{
|
||||
private $utils;
|
||||
@@ -64,24 +68,21 @@ class LDAP extends Espo
|
||||
Config $config,
|
||||
EntityManager $entityManager,
|
||||
PasswordHash $passwordHash,
|
||||
Language $language,
|
||||
Language $defaultLanguage,
|
||||
ApplicationUser $applicationUser,
|
||||
bool $isPortal = false
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->passwordHash = $passwordHash;
|
||||
$this->language = $language;
|
||||
$this->language = $defaultLanguage;
|
||||
$this->applicationUser = $applicationUser;
|
||||
|
||||
$this->isPortal = $isPortal;
|
||||
|
||||
$this->utils = new LDAP\Utils($config);
|
||||
$this->utils = new LDAPUtils($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* User field name => option name (LDAP attribute).
|
||||
*/
|
||||
protected $ldapFieldMap = [
|
||||
'userName' => 'userNameAttribute',
|
||||
'firstName' => 'userFirstNameAttribute',
|
||||
@@ -91,17 +92,11 @@ class LDAP extends Espo
|
||||
'phoneNumber' => 'userPhoneNumberAttribute',
|
||||
];
|
||||
|
||||
/**
|
||||
* User field name => option name.
|
||||
*/
|
||||
protected $userFieldMap = [
|
||||
'teamsIds' => 'userTeamsIds',
|
||||
'defaultTeamId' => 'userDefaultTeamId',
|
||||
];
|
||||
|
||||
/**
|
||||
* User field name => option name.
|
||||
*/
|
||||
protected $portalUserFieldMap = [
|
||||
'portalsIds' => 'portalUserPortalsIds',
|
||||
'portalRolesIds' => 'portalUserRolesIds',
|
||||
@@ -137,7 +132,8 @@ class LDAP extends Espo
|
||||
/* Login LDAP system user (ldapUsername, ldapPassword) */
|
||||
try {
|
||||
$ldapClient->bind();
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$options = $this->utils->getLdapClientOptions();
|
||||
$GLOBALS['log']->error('LDAP: Could not connect to LDAP server ['.$options['host'].'], details: ' . $e->getMessage());
|
||||
|
||||
@@ -152,7 +148,8 @@ class LDAP extends Espo
|
||||
if (!isset($adminUser)) {
|
||||
try {
|
||||
$userDn = $this->findLdapUserDnByUsername($username);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$GLOBALS['log']->error('Error while finding DN for ['.$username.'], details: ' . $e->getMessage() . '.');
|
||||
}
|
||||
|
||||
@@ -171,8 +168,11 @@ class LDAP extends Espo
|
||||
|
||||
try {
|
||||
$ldapClient->bind($userDn, $password);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
catch (
|
||||
Exception $e) {
|
||||
$GLOBALS['log']->error('LDAP: Authentication failed for user ['.$username.'], details: ' . $e->getMessage());
|
||||
|
||||
return Result::fail();
|
||||
}
|
||||
}
|
||||
@@ -187,6 +187,7 @@ class LDAP extends Espo
|
||||
if (!isset($user)) {
|
||||
if (!$this->utils->getOption('createEspoUser')) {
|
||||
$this->useSystemUser();
|
||||
|
||||
throw new Error($this->language->translate('ldapUserInEspoNotFound', 'messages', 'User'));
|
||||
}
|
||||
|
||||
@@ -212,8 +213,9 @@ class LDAP extends Espo
|
||||
$options = $this->utils->getLdapClientOptions();
|
||||
|
||||
try {
|
||||
$this->ldapClient = new LDAP\Client($options);
|
||||
} catch (\Exception $e) {
|
||||
$this->ldapClient = new LDAPClient($options);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$GLOBALS['log']->error('LDAP error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -222,12 +224,7 @@ class LDAP extends Espo
|
||||
}
|
||||
|
||||
/**
|
||||
* Login by authorization token
|
||||
*
|
||||
* @param string $username
|
||||
* @param \Espo\Entities\AuthToken $authToken
|
||||
*
|
||||
* @return \Espo\Entities\User | null
|
||||
* Login by authorization token.
|
||||
*/
|
||||
protected function loginByToken($username, AuthToken $authToken = null)
|
||||
{
|
||||
@@ -239,11 +236,14 @@ class LDAP extends Espo
|
||||
$user = $this->entityManager->getEntity('User', $userId);
|
||||
|
||||
$tokenUsername = $user->get('userName');
|
||||
|
||||
if (strtolower($username) != strtolower($tokenUsername)) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
|
||||
$GLOBALS['log']->alert(
|
||||
'Unauthorized access attempt for user ['.$username.'] from IP ['.$ip.']'
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -257,11 +257,7 @@ class LDAP extends Espo
|
||||
}
|
||||
|
||||
/**
|
||||
* Login user with administrator rights
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return \Espo\Entities\User | null
|
||||
* Login user with administrator rights.
|
||||
*/
|
||||
protected function adminLogin($username, $password)
|
||||
{
|
||||
@@ -279,12 +275,7 @@ class LDAP extends Espo
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Espo user with data gets from LDAP server
|
||||
*
|
||||
* @param array $userData LDAP entity data
|
||||
* @param boolean $isPortal Is portal user
|
||||
*
|
||||
* @return \Espo\Entities\User
|
||||
* Create Espo user with data gets from LDAP server.
|
||||
*/
|
||||
protected function createUser(array $userData, $isPortal = false)
|
||||
{
|
||||
@@ -320,17 +311,14 @@ class LDAP extends Espo
|
||||
|
||||
$user = $this->entityManager->getEntity('User');
|
||||
$user->set($data);
|
||||
|
||||
$this->entityManager->saveEntity($user);
|
||||
|
||||
return $this->entityManager->getEntity('User', $user->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find LDAP user DN by his username
|
||||
*
|
||||
* @param string $username
|
||||
*
|
||||
* @return string | null
|
||||
* Find LDAP user DN by his username.
|
||||
*/
|
||||
protected function findLdapUserDnByUsername($username)
|
||||
{
|
||||
@@ -338,12 +326,15 @@ class LDAP extends Espo
|
||||
$options = $this->utils->getOptions();
|
||||
|
||||
$loginFilterString = '';
|
||||
|
||||
if (!empty($options['userLoginFilter'])) {
|
||||
$loginFilterString = $this->convertToFilterFormat($options['userLoginFilter']);
|
||||
}
|
||||
|
||||
$searchString = '(&(objectClass='.$options['userObjectClass'].')('.$options['userNameAttribute'].'='.$username.')'.$loginFilterString.')';
|
||||
$result = $ldapClient->search($searchString, null, LDAP\Client::SEARCH_SCOPE_SUB);
|
||||
$searchString = '(&(objectClass='.$options['userObjectClass'].')('.$options['userNameAttribute'].'='.$username.')'.
|
||||
$loginFilterString.')';
|
||||
|
||||
$result = $ldapClient->search($searchString, null, LDAPClient::SEARCH_SCOPE_SUB);
|
||||
$GLOBALS['log']->debug('LDAP: user search string: "' . $searchString . '"');
|
||||
|
||||
foreach ($result as $item) {
|
||||
@@ -352,30 +343,25 @@ class LDAP extends Espo
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and convert filter item into LDAP format
|
||||
*
|
||||
* @param string $filter E.g. "memberof=CN=externalTesters,OU=groups,DC=espo,DC=local"
|
||||
*
|
||||
* @return string
|
||||
* Check and convert filter item into LDAP format.
|
||||
*/
|
||||
protected function convertToFilterFormat($filter)
|
||||
{
|
||||
$filter = trim($filter);
|
||||
|
||||
if (substr($filter, 0, 1) != '(') {
|
||||
$filter = '(' . $filter;
|
||||
}
|
||||
|
||||
if (substr($filter, -1) != ')') {
|
||||
$filter = $filter . ')';
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load fields for a user
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return array
|
||||
* Load fields for a user.
|
||||
*/
|
||||
protected function loadFields($type)
|
||||
{
|
||||
@@ -384,6 +370,7 @@ class LDAP extends Espo
|
||||
$typeMap = $type . 'FieldMap';
|
||||
|
||||
$fields = [];
|
||||
|
||||
foreach ($this->$typeMap as $fieldName => $fieldValue) {
|
||||
if (isset($options[$fieldValue])) {
|
||||
$fields[$fieldName] = $options[$fieldValue];
|
||||
|
||||
@@ -103,7 +103,7 @@ class CommandManager
|
||||
|
||||
$skipIndex = 1;
|
||||
|
||||
if (isset($argv[0]) && $argv[0] === 'command.php') {
|
||||
if (isset($argv[0]) && preg_match('/command\.php$/', $argv[0])) {
|
||||
$skipIndex = 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,37 +48,57 @@ class AclCheck implements Command
|
||||
$id = $options['id'] ?? null;
|
||||
$action = $options['action'] ?? null;
|
||||
|
||||
if (empty($userId)) return null;
|
||||
if (empty($scope)) return null;
|
||||
if (empty($id)) return null;
|
||||
if (empty($userId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (empty($scope)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (empty($id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$container = $this->container;
|
||||
|
||||
$entityManager = $container->get('entityManager');
|
||||
|
||||
$user = $entityManager->getEntity('User', $userId);
|
||||
if (!$user) return null;
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($user->isPortal()) {
|
||||
$portalIdList = $user->getLinkMultipleIdList('portals');
|
||||
|
||||
foreach ($portalIdList as $portalId) {
|
||||
$application = new PortalApplication($portalId);
|
||||
$containerPortal = $application->getContainer();
|
||||
$entityManager = $containerPortal->get('entityManager');
|
||||
|
||||
$user = $entityManager->getEntity('User', $userId);
|
||||
if (!$user) return null;
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = $this->check($user, $scope, $id, $action, $containerPortal);
|
||||
|
||||
if ($result) {
|
||||
return 'true';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->check($user, $scope, $id, $action, $container)) {
|
||||
return 'true';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function check($user, $scope, $id, $action, $container)
|
||||
@@ -86,7 +106,10 @@ class AclCheck implements Command
|
||||
$entityManager = $container->get('entityManager');
|
||||
|
||||
$entity = $entityManager->getEntity($scope, $id);
|
||||
if (!$entity) return false;
|
||||
|
||||
if (!$entity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$aclManager = $container->get('aclManager');
|
||||
|
||||
|
||||
@@ -72,12 +72,13 @@ class Upgrade implements Command
|
||||
{
|
||||
$params = $this->normalizeParams($options, $flagList, $argumentList);
|
||||
|
||||
$versionInfo = $this->getVersionInfo();
|
||||
|
||||
$fromVersion = $this->config->get('version');
|
||||
$toVersion = $params->toVersion ?? null;
|
||||
|
||||
$versionInfo = $this->getVersionInfo($toVersion);
|
||||
|
||||
$nextVersion = $versionInfo->nextVersion ?? null;
|
||||
$lastVersion = $infoData->lastVersion ?? null;
|
||||
$lastVersion = $versionInfo->lastVersion ?? null;
|
||||
|
||||
$packageFile = $this->getPackageFile($params, $versionInfo);
|
||||
|
||||
@@ -189,10 +190,22 @@ class Upgrade implements Command
|
||||
$params->singleProcess = true;
|
||||
}
|
||||
|
||||
if (in_array('patch', $flagList)) {
|
||||
$currentVersion = $this->config->get('version');
|
||||
|
||||
if (preg_match('/^(.*)\.(.*)\..*$/', $currentVersion, $match)) {
|
||||
$options['toVersion'] = $match[1] . '.' . $match[2];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($options['step'])) {
|
||||
$params->step = $options['step'];
|
||||
}
|
||||
|
||||
if (!empty($options['toVersion'])) {
|
||||
$params->toVersion = $options['toVersion'];
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
@@ -343,12 +356,16 @@ class Upgrade implements Command
|
||||
return $phpExecutablePath;
|
||||
}
|
||||
|
||||
protected function getVersionInfo()
|
||||
protected function getVersionInfo($toVersion = null)
|
||||
{
|
||||
$url = 'https://s.espocrm.com/upgrade/next/';
|
||||
$url = $this->config->get('upgradeNextVersionUrl', $url);
|
||||
$url .= '?fromVersion=' . $this->config->get('version');
|
||||
|
||||
if ($toVersion) {
|
||||
$url .= '&toVersion=' . $toVersion;
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, \CURLOPT_SSL_VERIFYPEER, true);
|
||||
|
||||
@@ -51,6 +51,8 @@ class ContainerConfiguration
|
||||
|
||||
public function getLoaderClassName(string $name) : ?string
|
||||
{
|
||||
$className = null;
|
||||
|
||||
try {
|
||||
$className = $this->metadata->get(['app', 'containerServices', $name, 'loaderClassName']);
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ abstract class BaseFunction
|
||||
if (!$this->entity) {
|
||||
throw new NotPassedEntity('function: ' . $this->name);
|
||||
}
|
||||
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
@@ -119,12 +120,15 @@ abstract class BaseFunction
|
||||
protected function throwBadArgumentType(?int $index = null, ?string $type = null)
|
||||
{
|
||||
$msg = 'function: ' . $this->name;
|
||||
|
||||
if ($index !== null) {
|
||||
$msg .= ', index: ' . $index;
|
||||
|
||||
if ($type) {
|
||||
$msg .= ', should be: ' . $type;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadArgumentType($msg);
|
||||
}
|
||||
|
||||
@@ -134,12 +138,15 @@ abstract class BaseFunction
|
||||
protected function throwBadArgumentValue(?int $index = null, ?string $msg = null)
|
||||
{
|
||||
$string = 'function: ' . $this->name;
|
||||
|
||||
if ($index !== null) {
|
||||
$string .= ', index: ' . $index;
|
||||
|
||||
if ($msg) {
|
||||
$string .= ', ' . $msg;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadArgumentValue($string);
|
||||
}
|
||||
|
||||
@@ -147,11 +154,13 @@ abstract class BaseFunction
|
||||
* Throw Error exception.
|
||||
*/
|
||||
protected function throwError(?string $msg = null)
|
||||
|
||||
{
|
||||
$string = 'function: ' . $this->name;
|
||||
if ($msg) {
|
||||
$string .= ', ' . $msg;
|
||||
}
|
||||
|
||||
throw new Error($string);
|
||||
}
|
||||
|
||||
@@ -160,7 +169,10 @@ abstract class BaseFunction
|
||||
*/
|
||||
protected function logBadArgumentType(int $index, string $type)
|
||||
{
|
||||
if (!$this->log) return;
|
||||
if (!$this->log) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->log->warning("Formula function: {$this->name}, argument {$index} should be '{$type}'.");
|
||||
}
|
||||
|
||||
@@ -169,7 +181,10 @@ abstract class BaseFunction
|
||||
*/
|
||||
protected function log(string $msg, string $level = 'notice')
|
||||
{
|
||||
if (!$this->log) return;
|
||||
if (!$this->log) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->log->log($level, 'Formula function: ' . $this->name . ', ' . $msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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\JsonGroup;
|
||||
|
||||
use Espo\Core\Formula\{
|
||||
Functions\BaseFunction,
|
||||
ArgumentList,
|
||||
};
|
||||
|
||||
class RetrieveType extends BaseFunction
|
||||
{
|
||||
public function process(ArgumentList $args)
|
||||
{
|
||||
if (count($args) < 2) {
|
||||
$this->throwTooFewArguments();
|
||||
}
|
||||
|
||||
$jsonString = $this->evaluate($args[0]);
|
||||
$path = $this->evaluate($args[1]);
|
||||
|
||||
if (!is_string($jsonString)) {
|
||||
$this->throwBadArgumentType(1, 'string');
|
||||
}
|
||||
|
||||
if (!is_string($path)) {
|
||||
$this->throwBadArgumentType(2, 'string');
|
||||
}
|
||||
|
||||
if ($path === '') {
|
||||
$this->throwBadArgumentValue(2);
|
||||
}
|
||||
|
||||
$item = json_decode($jsonString);
|
||||
|
||||
$pathArray = $this->splitPath($path);
|
||||
|
||||
return $this->retrieveAttribute($item, $pathArray);
|
||||
}
|
||||
|
||||
private function splitPath(string $path) : array
|
||||
{
|
||||
$pathArray = preg_split('/(?<!\\\)\./', $path);
|
||||
|
||||
foreach ($pathArray as $i => $item) {
|
||||
$pathArray[$i] = str_replace('\.', '.', $item);
|
||||
}
|
||||
|
||||
return $pathArray;
|
||||
}
|
||||
|
||||
private function retrieveAttribute($item, array $path)
|
||||
{
|
||||
if (!count($path)) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
$key = array_shift($path);
|
||||
|
||||
if (is_array($item)) {
|
||||
$key = intval($key);
|
||||
|
||||
$subItem = $item[$key] ?? null;
|
||||
|
||||
if (is_null($subItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->retrieveAttribute($subItem, $path);
|
||||
}
|
||||
|
||||
if (is_object($item)) {
|
||||
$subItem = $item->$key ?? null;
|
||||
|
||||
if (is_null($subItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->retrieveAttribute($subItem, $path);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,6 @@ class NotType extends BaseFunction
|
||||
{
|
||||
public function process(ArgumentList $args)
|
||||
{
|
||||
return !$this->evaluate($args);
|
||||
return !$this->evaluate($args[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ class Parser
|
||||
{
|
||||
protected $priorityList = [
|
||||
['='],
|
||||
['||', '&&'],
|
||||
['||'],
|
||||
['&&'],
|
||||
['==', '!=', '>', '<', '>=', '<='],
|
||||
['+', '-'],
|
||||
['*', '/', '%'],
|
||||
|
||||
@@ -120,7 +120,7 @@ class Htmlizer
|
||||
|
||||
$relationList = $entity->getRelationList();
|
||||
|
||||
if (!$skipLinks && $level === 0 && $this->entityManager) {
|
||||
if (!$skipLinks && $level === 0 && $this->entityManager && $entity->id) {
|
||||
foreach ($relationList as $relation) {
|
||||
$collection = null;
|
||||
|
||||
|
||||
@@ -155,9 +155,11 @@ class InjectableFactory
|
||||
|
||||
$dependencyClass = null;
|
||||
|
||||
if ($param->getType()) {
|
||||
$type = $param->getType();
|
||||
|
||||
if ($type && !$type->isBuiltin()) {
|
||||
try {
|
||||
$dependencyClass = $param->getClass();
|
||||
$dependencyClass = new ReflectionClass($type->getName());
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$badClassName = $param->getType()->getName();
|
||||
@@ -241,15 +243,26 @@ class InjectableFactory
|
||||
}
|
||||
|
||||
$params = $class->getMethod($methodName)->getParameters();
|
||||
|
||||
if (!$params || !count($params)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($skipInstanceCheck) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$injection = $this->container->get($name);
|
||||
|
||||
$paramClass = $params[0]->getClass();
|
||||
$paramClass = null;
|
||||
|
||||
if ($skipInstanceCheck || $paramClass && $paramClass->isInstance($injection)) {
|
||||
$type = $params[0]->getType();
|
||||
|
||||
if ($type && !$type->isBuiltin()) {
|
||||
$paramClass = new ReflectionClass($type->getName());
|
||||
}
|
||||
|
||||
if ($paramClass && $paramClass->isInstance($injection)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,9 @@ class Importer
|
||||
if ($duplicate = $this->findDuplicate($email)) {
|
||||
if ($duplicate->get('status') != 'Being Imported') {
|
||||
$duplicate = $this->entityManager->getEntity('Email', $duplicate->id);
|
||||
|
||||
$this->processDuplicate($duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList);
|
||||
|
||||
return $duplicate;
|
||||
}
|
||||
}
|
||||
@@ -197,6 +199,7 @@ class Importer
|
||||
if ($parser->checkMessageAttribute($message, 'date')) {
|
||||
try {
|
||||
$dt = new DateTime($parser->getMessageAttribute($message, 'date'));
|
||||
|
||||
if ($dt) {
|
||||
$dateSent = $dt->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s');
|
||||
$email->set('dateSent', $dateSent);
|
||||
@@ -209,6 +212,7 @@ class Importer
|
||||
if ($parser->checkMessageAttribute($message, 'delivery-Date')) {
|
||||
try {
|
||||
$dt = new DateTime($parser->getMessageAttribute($message, 'delivery-Date'));
|
||||
|
||||
if ($dt) {
|
||||
$deliveryDate = $dt->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s');
|
||||
$email->set('delivery-Date', $deliveryDate);
|
||||
@@ -222,7 +226,7 @@ class Importer
|
||||
$parser->fetchContentParts($email, $message, $inlineAttachmentList);
|
||||
|
||||
if ($this->filtersMatcher->match($email, $filterList)) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
$email->set('body', 'Not fetched. The email size exceeds the limit.');
|
||||
@@ -233,15 +237,24 @@ class Importer
|
||||
|
||||
$replied = null;
|
||||
|
||||
if ($parser->checkMessageAttribute($message, 'in-Reply-To') && $parser->getMessageAttribute($message, 'in-Reply-To')) {
|
||||
if (
|
||||
$parser->checkMessageAttribute($message, 'in-Reply-To') && $parser->getMessageAttribute($message, 'in-Reply-To')
|
||||
) {
|
||||
$arr = explode(' ', $parser->getMessageAttribute($message, 'in-Reply-To'));
|
||||
$inReplyTo = $arr[0];
|
||||
|
||||
if ($inReplyTo) {
|
||||
if ($inReplyTo[0] !== '<') $inReplyTo = '<' . $inReplyTo . '>';
|
||||
$replied = $this->entityManager->getRepository('Email')->where(array(
|
||||
'messageId' => $inReplyTo
|
||||
))->findOne();
|
||||
if ($inReplyTo[0] !== '<') {
|
||||
$inReplyTo = '<' . $inReplyTo . '>';
|
||||
}
|
||||
|
||||
$replied = $this->entityManager
|
||||
->getRepository('Email')
|
||||
->where([
|
||||
'messageId' => $inReplyTo
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if ($replied) {
|
||||
$email->set('repliedId', $replied->id);
|
||||
$repliedTeamIdList = $replied->getLinkMultipleIdList('teams');
|
||||
@@ -255,6 +268,7 @@ class Importer
|
||||
if ($parser->checkMessageAttribute($message, 'references') && $parser->getMessageAttribute($message, 'references')) {
|
||||
$references = $parser->getMessageAttribute($message, 'references');
|
||||
$delimiter = ' ';
|
||||
|
||||
if (strpos($references, '>,')) {
|
||||
$delimiter = ',';
|
||||
}
|
||||
@@ -278,6 +292,7 @@ class Importer
|
||||
if (!empty($parentType) && !empty($parentId)) {
|
||||
if ($parentType == 'Lead') {
|
||||
$parent = $this->entityManager->getEntity('Lead', $parentId);
|
||||
|
||||
if ($parent && $parent->get('status') == 'Converted') {
|
||||
if ($parent->get('createdAccountId')) {
|
||||
$account = $this->entityManager->getEntity('Account', $parent->get('createdAccountId'));
|
||||
@@ -301,6 +316,7 @@ class Importer
|
||||
|
||||
$email->set('parentType', $parentType);
|
||||
$email->set('parentId', $parentId);
|
||||
|
||||
$parentFound = true;
|
||||
}
|
||||
}
|
||||
@@ -310,6 +326,7 @@ class Importer
|
||||
if (!$parentFound) {
|
||||
if ($replied && $replied->get('parentId') && $replied->get('parentType')) {
|
||||
$parentFound = $this->entityManager->getEntity($replied->get('parentType'), $replied->get('parentId'));
|
||||
|
||||
if ($parentFound) {
|
||||
$email->set('parentType', $replied->get('parentType'));
|
||||
$email->set('parentId', $replied->get('parentId'));
|
||||
@@ -318,6 +335,7 @@ class Importer
|
||||
}
|
||||
if (!$parentFound) {
|
||||
$from = $email->get('from');
|
||||
|
||||
if ($from) {
|
||||
$parentFound = $this->findParent($email, $from);
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ class Recovery
|
||||
'isSystem' => true,
|
||||
]);
|
||||
|
||||
if (!$this->emailAddress->hasSystemSmtp()) {
|
||||
if (!$this->emailSender->hasSystemSmtp()) {
|
||||
$sender->withSmtpParams([
|
||||
'server' => $config->get('internalSmtpServer'),
|
||||
'port' => $config->get('internalSmtpPort'),
|
||||
|
||||
@@ -120,11 +120,11 @@ class Tcpdf extends \TCPDF
|
||||
$html = $this->headerHtml;
|
||||
|
||||
if ($this->useGroupNumbers) {
|
||||
$html = str_replace('{pageNumber}', '{:png:}', $html);
|
||||
$html = str_replace('{pageAbsoluteNumber}', '{:pnp:}', $html);
|
||||
$html = str_replace('{pageNumber}', '{{:png:}}', $html);
|
||||
$html = str_replace('{pageAbsoluteNumber}', '{{:pnp:}}', $html);
|
||||
} else {
|
||||
$html = str_replace('{pageNumber}', '{:pnp:}', $html);
|
||||
$html = str_replace('{pageAbsoluteNumber}', '{:pnp:}', $html);
|
||||
$html = str_replace('{pageNumber}', '{{:pnp:}', $html);
|
||||
$html = str_replace('{pageAbsoluteNumber}', '{{:pnp:}}', $html);
|
||||
}
|
||||
|
||||
if ($this->isUnicodeFont()) {
|
||||
@@ -148,11 +148,11 @@ class Tcpdf extends \TCPDF
|
||||
$html = $this->footerHtml;
|
||||
|
||||
if ($this->useGroupNumbers) {
|
||||
$html = str_replace('{pageNumber}', '{:png:}', $html);
|
||||
$html = str_replace('{pageAbsoluteNumber}', '{:pnp:}', $html);
|
||||
$html = str_replace('{pageNumber}', '{{:png:}}', $html);
|
||||
$html = str_replace('{pageAbsoluteNumber}', '{{:pnp:}}', $html);
|
||||
} else {
|
||||
$html = str_replace('{pageNumber}', '{:pnp:}', $html);
|
||||
$html = str_replace('{pageAbsoluteNumber}', '{:pnp:}', $html);
|
||||
$html = str_replace('{pageNumber}', '{{:pnp:}}', $html);
|
||||
$html = str_replace('{pageAbsoluteNumber}', '{{:pnp:}}', $html);
|
||||
}
|
||||
|
||||
if ($this->isUnicodeFont()) {
|
||||
|
||||
@@ -34,12 +34,13 @@ use Espo\Core\{
|
||||
InjectableFactory,
|
||||
ApplicationUser,
|
||||
Portal\Utils\Route,
|
||||
Utils\Log,
|
||||
};
|
||||
|
||||
class Api extends ApiBase
|
||||
{
|
||||
public function __construct(InjectableFactory $injectableFactory, ApplicationUser $applicationUser, Route $routeUtil)
|
||||
public function __construct(InjectableFactory $injectableFactory, ApplicationUser $applicationUser, Route $routeUtil, Log $log)
|
||||
{
|
||||
parent::__construct($injectableFactory, $applicationUser, $routeUtil);
|
||||
parent::__construct($injectableFactory, $applicationUser, $routeUtil, $log);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,12 @@ class Url
|
||||
{
|
||||
public static function detectPortalId() : ?string
|
||||
{
|
||||
$portalId = $_SERVER['ESPO_PORTAL_ID'] ?? null;
|
||||
|
||||
if ($portalId) {
|
||||
return $portalId;
|
||||
}
|
||||
|
||||
$url = $_SERVER['REQUEST_URI'];
|
||||
|
||||
$portalId = explode('/', $url)[count(explode('/', $_SERVER['SCRIPT_NAME'])) - 1] ?? null;
|
||||
@@ -41,57 +47,61 @@ class Url
|
||||
$portalId = null;
|
||||
}
|
||||
|
||||
if (!isset($portalId)) {
|
||||
$url = $_SERVER['REDIRECT_URL'];
|
||||
|
||||
$portalId = explode('/', $url)[count(explode('/', $_SERVER['SCRIPT_NAME'])) - 1] ?? null;
|
||||
if ($portalId) {
|
||||
return $portalId;
|
||||
}
|
||||
|
||||
$url = $_SERVER['REDIRECT_URL'] ?? null;
|
||||
|
||||
if (!$url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$portalId = explode('/', $url)[count(explode('/', $_SERVER['SCRIPT_NAME'])) - 1] ?? null;
|
||||
|
||||
return $portalId;
|
||||
}
|
||||
|
||||
public static function detectUrl() : string
|
||||
protected static function detectIsCustomUrl() : bool
|
||||
{
|
||||
$url = $_SERVER['REQUEST_URI'];
|
||||
|
||||
$portalId = explode('/', $url)[count(explode('/', $_SERVER['SCRIPT_NAME'])) - 1] ?? null;
|
||||
|
||||
if (strpos($url, '=') !== false) {
|
||||
$portalId = null;
|
||||
}
|
||||
|
||||
if (!isset($portalId)) {
|
||||
return $_SERVER['REDIRECT_URL'];
|
||||
}
|
||||
|
||||
return $url;
|
||||
return (bool) ($_SERVER['ESPO_PORTAL_IS_CUSTOM_URL'] ?? false);
|
||||
}
|
||||
|
||||
public static function normalizeUrl(string $url) : string
|
||||
public static function detectIsInPortalDir() : bool
|
||||
{
|
||||
$urlParts = explode('?', $url);
|
||||
$isCustomUrl = self::detectIsCustomUrl();
|
||||
|
||||
if (substr($urlParts[0], -1) === '/') {
|
||||
return $url;
|
||||
if ($isCustomUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = $urlParts[0] . '/';
|
||||
|
||||
if (count($urlParts) > 1) {
|
||||
$url .= '?' . $urlParts[1];
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function detectIsInDir() : bool
|
||||
{
|
||||
$url = $_SERVER['REQUEST_URI'];
|
||||
|
||||
$a = explode('?', $url);
|
||||
|
||||
$url = rtrim($a[0], '/');
|
||||
|
||||
return strpos($url, '/') !== false;
|
||||
return strpos($url, '/portal') !== false ;
|
||||
}
|
||||
|
||||
public static function detectIsInPortalWithId() : bool
|
||||
{
|
||||
if (!self::detectIsInPortalDir()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = $_SERVER['REQUEST_URI'];
|
||||
|
||||
$a = explode('?', $url);
|
||||
|
||||
$url = rtrim($a[0], '/');
|
||||
|
||||
$folders = explode('/', $url);
|
||||
|
||||
if (count($folders) > 1 && $folders[count($folders) - 2] === 'portal') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2368,7 +2368,9 @@ class SelectManager
|
||||
protected function textFilter($textFilter, array &$result, $noFullText = false)
|
||||
{
|
||||
$fieldDefs = $this->getSeed()->getAttributes();
|
||||
|
||||
$fieldList = $this->getTextFilterFieldList();
|
||||
|
||||
$group = [];
|
||||
|
||||
$textFilterContainsMinLength = $this->getConfig()->get('textFilterContainsMinLength', self::MIN_LENGTH_FOR_CONTENT_SEARCH);
|
||||
@@ -2401,6 +2403,7 @@ class SelectManager
|
||||
$textFilterForFullTextSearch = str_replace('%', '*', $textFilterForFullTextSearch);
|
||||
|
||||
$skipFullTextSearch = false;
|
||||
|
||||
if (!$forceFullTextSearch) {
|
||||
if (mb_strpos($textFilterForFullTextSearch, '*') === 0) {
|
||||
$skipFullTextSearch = true;
|
||||
@@ -2409,9 +2412,12 @@ class SelectManager
|
||||
}
|
||||
}
|
||||
|
||||
if ($noFullText) $skipFullTextSearch = true;
|
||||
if ($noFullText) {
|
||||
$skipFullTextSearch = true;
|
||||
}
|
||||
|
||||
$fullTextSearchData = null;
|
||||
|
||||
if (!$skipFullTextSearch) {
|
||||
$fullTextSearchData = $this->getFullTextSearchDataForTextFilter($textFilterForFullTextSearch, !$useFullTextSearch);
|
||||
}
|
||||
@@ -2419,6 +2425,7 @@ class SelectManager
|
||||
$fullTextGroup = [];
|
||||
|
||||
$fullTextSearchFieldList = [];
|
||||
|
||||
if ($fullTextSearchData) {
|
||||
if ($this->fullTextRelevanceThreshold) {
|
||||
$fullTextGroup[] = [$fullTextSearchData['where'] . '>=' => $this->fullTextRelevanceThreshold];
|
||||
@@ -2434,12 +2441,15 @@ class SelectManager
|
||||
|
||||
$orderTypeMap = [
|
||||
'combined' => self::FT_ORDER_COMBINTED,
|
||||
'relavance' => self::FT_ORDER_RELEVANCE,
|
||||
'relevance' => self::FT_ORDER_RELEVANCE,
|
||||
'original' => self::FT_ORDER_ORIGINAL,
|
||||
];
|
||||
|
||||
$mOrderType = $this->getMetadata()->get(['entityDefs', $this->entityType, 'collection', 'fullTextSearchOrderType']);
|
||||
if ($mOrderType) $fullTextOrderType = $orderTypeMap[$mOrderType];
|
||||
|
||||
if ($mOrderType) {
|
||||
$fullTextOrderType = $orderTypeMap[$mOrderType];
|
||||
}
|
||||
|
||||
if (!isset($result['orderBy']) || $fullTextOrderType === self::FT_ORDER_RELEVANCE) {
|
||||
$result['orderBy'] = [[$relevanceExpression, 'desc']];
|
||||
@@ -2468,9 +2478,14 @@ class SelectManager
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
if ($useFullTextSearch) {
|
||||
if (in_array($field, $fullTextSearchFieldList)) continue;
|
||||
if (in_array($field, $fullTextSearchFieldList)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($forceFullTextSearch) {
|
||||
continue;
|
||||
}
|
||||
if ($forceFullTextSearch) continue;
|
||||
|
||||
$seed = $this->getSeed();
|
||||
|
||||
@@ -2478,21 +2493,34 @@ class SelectManager
|
||||
|
||||
if (strpos($field, '.') !== false) {
|
||||
list($link, $foreignField) = explode('.', $field);
|
||||
|
||||
$foreignEntityType = $seed->getRelationParam($link, 'entity');
|
||||
$seed = $this->getEntityManager()->getEntity($foreignEntityType);
|
||||
|
||||
$this->addLeftJoin($link, $result);
|
||||
|
||||
if ($seed->getRelationParam($link, 'type') === $seed::HAS_MANY) {
|
||||
$this->setDistinct(true, $result);
|
||||
}
|
||||
|
||||
$attributeType = $seed->getAttributeType($foreignField);
|
||||
} else {
|
||||
$attributeType = $seed->getAttributeType($field);
|
||||
|
||||
if ($attributeType === 'foreign') {
|
||||
$link = $seed->getAttributeParam($field, 'relation');
|
||||
|
||||
if ($link) {
|
||||
$this->addLeftJoin($link, $result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($attributeType === 'int') {
|
||||
if (is_numeric($textFilter)) {
|
||||
$group[$field] = intval($textFilter);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2666,11 +2694,6 @@ class SelectManager
|
||||
$selectParams1['havingClause'][] = $selectParams2['havingClause'];
|
||||
}
|
||||
|
||||
if (!empty($selectParams2['leftJoins'])) {
|
||||
foreach ($selectParams2['leftJoins'] as $item) {
|
||||
$this->addLeftJoin($item, $selectParams1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($selectParams2['joins'])) {
|
||||
foreach ($selectParams2['joins'] as $item) {
|
||||
@@ -2678,6 +2701,16 @@ class SelectManager
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($selectParams2['leftJoins'])) {
|
||||
foreach ($selectParams2['leftJoins'] as $item) {
|
||||
if ($this->hasJoin($item, $selectParams1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->addLeftJoin($item, $selectParams1);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($selectParams2['select'])) {
|
||||
$selectParams1['select'] = $selectParams2['select'];
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ class AdminNotificationManager
|
||||
$extension = $this->getEntityManager()->getRepository('Extension')
|
||||
->select(['version'])
|
||||
->where([
|
||||
'name' => $extensionName,
|
||||
'isInstalled' => true,
|
||||
])
|
||||
->order('createdAt', true)
|
||||
|
||||
@@ -33,6 +33,7 @@ use Espo\Core\{
|
||||
Exceptions\Error,
|
||||
Utils\Autoload\Loader,
|
||||
Utils\DataCache,
|
||||
Utils\File\Manager as FileManager,
|
||||
};
|
||||
|
||||
use Exception;
|
||||
@@ -52,14 +53,16 @@ class Autoload
|
||||
protected $config;
|
||||
protected $metadata;
|
||||
protected $dataCache;
|
||||
protected $fileManager;
|
||||
protected $loader;
|
||||
|
||||
public function __construct(Config $config, Metadata $metadata, DataCache $dataCache, Loader $loader)
|
||||
{
|
||||
public function __construct(
|
||||
Config $config, Metadata $metadata, DataCache $dataCache, FileManager $fileManager, Loader $loader
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->metadata = $metadata;
|
||||
$this->dataCache = $dataCache;
|
||||
|
||||
$this->fileManager = $fileManager;
|
||||
$this->loader = $loader;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,12 @@
|
||||
|
||||
namespace Espo\Core\Utils;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\{
|
||||
Exceptions\Error,
|
||||
Utils\File\Manager as FileManager,
|
||||
};
|
||||
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use StdClass;
|
||||
|
||||
/**
|
||||
* Reads and writes the main config file.
|
||||
@@ -88,6 +91,7 @@ class Config
|
||||
$keys = explode('.', $name);
|
||||
|
||||
$lastBranch = $this->loadConfig();
|
||||
|
||||
foreach ($keys as $keyName) {
|
||||
if (isset($lastBranch[$keyName]) && (is_array($lastBranch) || is_object($lastBranch))) {
|
||||
if (is_array($lastBranch)) {
|
||||
@@ -111,6 +115,7 @@ class Config
|
||||
$keys = explode('.', $name);
|
||||
|
||||
$lastBranch = $this->loadConfig();
|
||||
|
||||
foreach ($keys as $keyName) {
|
||||
if (isset($lastBranch[$keyName]) && (is_array($lastBranch) || is_object($lastBranch))) {
|
||||
if (is_array($lastBranch)) {
|
||||
@@ -136,14 +141,16 @@ class Config
|
||||
}
|
||||
|
||||
if (!is_array($name)) {
|
||||
$name = array($name => $value);
|
||||
$name = [$name => $value];
|
||||
}
|
||||
|
||||
foreach ($name as $key => $value) {
|
||||
if (in_array($key, $this->associativeArrayAttributeList) && is_object($value)) {
|
||||
$value = (array) $value;
|
||||
}
|
||||
|
||||
$this->data[$key] = $value;
|
||||
|
||||
if (!$dontMarkDirty) {
|
||||
$this->changedData[$key] = $value;
|
||||
}
|
||||
@@ -157,7 +164,9 @@ class Config
|
||||
{
|
||||
if (array_key_exists($name, $this->data)) {
|
||||
unset($this->data[$name]);
|
||||
|
||||
$this->removeData[] = $name;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -180,10 +189,11 @@ class Config
|
||||
$configPath = $this->getConfigPath();
|
||||
|
||||
if (!file_exists($configPath)) {
|
||||
throw new Error('Config file ['. $configPath .'] is not found.');
|
||||
throw new Error("Config file '{$configPath}' is not found.");
|
||||
}
|
||||
|
||||
$data = include($configPath);
|
||||
|
||||
if (!is_array($data)) {
|
||||
$data = include($configPath);
|
||||
}
|
||||
@@ -201,7 +211,8 @@ class Config
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
$GLOBALS['log']->error('Invalid config data ['. var_export($data, true) .'] while saving to ['. $configPath .'].');
|
||||
$GLOBALS['log']->error("Invalid config data while saving to '{$configPath}'.");
|
||||
|
||||
throw new Error('Invalid config data while saving.');
|
||||
}
|
||||
|
||||
@@ -211,14 +222,16 @@ class Config
|
||||
|
||||
if ($result) {
|
||||
$reloadedData = include($configPath);
|
||||
|
||||
if (!is_array($reloadedData) || $microtime !== ($reloadedData['microtime'] ?? null)) {
|
||||
$result = $this->getFileManager()->putPhpContents($configPath, $data, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
$this->changedData = array();
|
||||
$this->removeData = array();
|
||||
$this->changedData = [];
|
||||
$this->removeData = [];
|
||||
|
||||
$this->loadConfig(true);
|
||||
}
|
||||
|
||||
@@ -244,6 +257,7 @@ class Config
|
||||
$this->data = $this->getFileManager()->getPhpContents($configPath);
|
||||
|
||||
$systemConfig = $this->getFileManager()->getPhpContents($this->systemConfigPath);
|
||||
|
||||
$this->data = Util::merge($systemConfig, $this->data);
|
||||
|
||||
return $this->data;
|
||||
@@ -252,7 +266,7 @@ class Config
|
||||
/**
|
||||
* Get all parameters.
|
||||
*/
|
||||
public function getAllData() : \StdClass
|
||||
public function getAllData() : StdClass
|
||||
{
|
||||
return (object) $this->loadConfig();
|
||||
}
|
||||
|
||||
@@ -63,35 +63,33 @@ class DataCache
|
||||
*
|
||||
* @throws Error if is not cached.
|
||||
*
|
||||
* @return ?array|StdClass|int|float|string
|
||||
* @return ?array|StdClass
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
$cacheFile = $this->getCacheFile($key);
|
||||
|
||||
$result = $this->fileManager->getPhpContents($cacheFile);
|
||||
$data = $this->fileManager->getPhpSafeContents($cacheFile);
|
||||
|
||||
if ($result === false) {
|
||||
if ($data === false) {
|
||||
throw new Error("Could not get '{$key}'.");
|
||||
}
|
||||
|
||||
return $result;
|
||||
if (! $this->checkDataIsValid($data)) {
|
||||
throw new Error("Bad data fetched from cache by key '{$key}'.");
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store in cache.
|
||||
*
|
||||
* @param ?array|StdClass|int|float|string $data
|
||||
* @param ?array|StdClass $data
|
||||
*/
|
||||
public function store(string $key, $data)
|
||||
{
|
||||
if (
|
||||
! is_array($data) &&
|
||||
! $data instanceof StdClass &&
|
||||
! is_int($data) &&
|
||||
! is_float($data) &&
|
||||
! is_int($data)
|
||||
) {
|
||||
if (! $this->checkDataIsValid($data)) {
|
||||
throw new InvalidArgumentException("Bad cache data type.");
|
||||
}
|
||||
|
||||
@@ -104,6 +102,15 @@ class DataCache
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkDataIsValid($data)
|
||||
{
|
||||
$isInvalid =
|
||||
! is_array($data) &&
|
||||
! $data instanceof StdClass;
|
||||
|
||||
return ! $isInvalid;
|
||||
}
|
||||
|
||||
protected function getCacheFile(string $key) : string
|
||||
{
|
||||
if (
|
||||
|
||||
@@ -96,7 +96,7 @@ class PersonName extends Base
|
||||
$whereItems[] = "CONCAT:({$firstName}, ' ', {$middleName}, ' ', {$lastName})";
|
||||
} else
|
||||
if ($format === 'lastFirstMiddle') {
|
||||
$whereItems[] = "CONCAT:({$lastName}, ' ', {$firstColumn}, ' ', {$middleName})";
|
||||
$whereItems[] = "CONCAT:({$lastName}, ' ', {$firstName}, ' ', {$middleName})";
|
||||
}
|
||||
|
||||
$selectExpression = $this->getSelect($fullList);
|
||||
|
||||
@@ -47,6 +47,14 @@ class Manager
|
||||
|
||||
protected $tmpDir = 'data/tmp';
|
||||
|
||||
const RENAME_RETRY_NUMBER = 10;
|
||||
|
||||
const RENAME_RETRY_INTERVAL = 0.1;
|
||||
|
||||
const GET_SAFE_CONTENTS_RETRY_NUMBER = 10;
|
||||
|
||||
const GET_SAFE_CONTENTS_RETRY_INTERVAL = 0.1;
|
||||
|
||||
public function __construct(?Config $config = null)
|
||||
{
|
||||
$params = null;
|
||||
@@ -175,10 +183,10 @@ class Manager
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PHP array from PHP file.
|
||||
* Get data from PHP file.
|
||||
*
|
||||
* @param string|array $path
|
||||
* @return array|object|bool
|
||||
* @return mixed|bool
|
||||
*/
|
||||
public function getPhpContents($path)
|
||||
{
|
||||
@@ -193,6 +201,39 @@ class Manager
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array or StdClass data from PHP file.
|
||||
* If a file is not yet written, it will wait until it's ready.
|
||||
*
|
||||
* @return array|StdClass
|
||||
*/
|
||||
public function getPhpSafeContents(string $path)
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
throw new Error("Can't get contents from non-existing file '{$path}'.");
|
||||
}
|
||||
|
||||
if (!strtolower(substr($path, -4)) == '.php') {
|
||||
throw new Error("Only PHP file are allowed for getting contents.");
|
||||
}
|
||||
|
||||
$counter = 0;
|
||||
|
||||
while ($counter < self::GET_SAFE_CONTENTS_RETRY_NUMBER) {
|
||||
$data = include($path);
|
||||
|
||||
if (is_array($data) || $data instanceof StdClass) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
usleep(self::GET_SAFE_CONTENTS_RETRY_INTERVAL * 1000000);
|
||||
|
||||
$counter ++;
|
||||
}
|
||||
|
||||
throw new Error("Bad data stored in file '{$path}'.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to a file.
|
||||
*
|
||||
@@ -266,6 +307,10 @@ class Manager
|
||||
|
||||
$result = rename($tmpPath, $path);
|
||||
|
||||
if (!$result && stripos(\PHP_OS, 'WIN') === 0) {
|
||||
$result = $this->renameInLoop($tmpPath, $path);
|
||||
}
|
||||
|
||||
if ($this->isFile($tmpPath)) {
|
||||
$this->removeFile($tmpPath);
|
||||
}
|
||||
@@ -273,6 +318,29 @@ class Manager
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function renameInLoop(string $source, string $destination) : bool
|
||||
{
|
||||
$counter = 0;
|
||||
|
||||
while ($counter < self::RENAME_RETRY_NUMBER) {
|
||||
if (!$this->isWritable($destination)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$result = rename($source, $destination);
|
||||
|
||||
if ($result !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
usleep(self::RENAME_RETRY_INTERVAL * 1000000);
|
||||
|
||||
$counter ++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save PHP content to file.
|
||||
*
|
||||
@@ -288,8 +356,8 @@ class Manager
|
||||
*
|
||||
* @param string | array $path
|
||||
* @param string $data
|
||||
* @param integer $flags
|
||||
* @param resource $context
|
||||
* @param integer $flags
|
||||
* @param resource $context
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@@ -377,8 +445,8 @@ class Manager
|
||||
/**
|
||||
* Unset some element of content data.
|
||||
*
|
||||
* @param string | array $path
|
||||
* @param array | string $unsets
|
||||
* @param string | array $path
|
||||
* @param array | string $unsets
|
||||
* @return bool
|
||||
*/
|
||||
public function unsetContents($path, $unsets, $isJSON = true)
|
||||
@@ -412,8 +480,8 @@ class Manager
|
||||
|
||||
|
||||
/**
|
||||
* Concat paths
|
||||
* @param string | array $paths Ex. array('pathPart1', 'pathPart2', 'pathPart3')
|
||||
* Concat paths.
|
||||
* @param string | array $paths Ex. array('pathPart1', 'pathPart2', 'pathPart3')
|
||||
* @return string
|
||||
*/
|
||||
protected function concatPaths($paths)
|
||||
@@ -432,11 +500,11 @@ class Manager
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new dir
|
||||
* Create a new dir.
|
||||
*
|
||||
* @param string | array $path
|
||||
* @param int $permission - ex. 0755
|
||||
* @param bool $recursive
|
||||
* @param string | array $path
|
||||
* @param int $permission - ex. 0755
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@@ -487,11 +555,11 @@ class Manager
|
||||
* Ex. $sourcePath = 'data/uploads/extensions/file.json',
|
||||
* $destPath = 'data/uploads/backup', result will be data/uploads/backup/data/uploads/backup/file.json.
|
||||
*
|
||||
* @param string $sourcePath
|
||||
* @param string $destPath
|
||||
* @param boolean $recursively
|
||||
* @param array $fileList - list of files that should be copied
|
||||
* @param boolean $copyOnlyFiles - copy only files, instead of full path with directories,
|
||||
* @param string $sourcePath
|
||||
* @param string $destPath
|
||||
* @param boolean $recursively
|
||||
* @param array $fileList - list of files that should be copied
|
||||
* @param boolean $copyOnlyFiles - copy only files, instead of full path with directories,
|
||||
* Ex. $sourcePath = 'data/uploads/extensions/file.json',
|
||||
* $destPath = 'data/uploads/backup', result will be 'data/uploads/backup/file.json'
|
||||
* @return boolen
|
||||
@@ -510,7 +578,6 @@ class Manager
|
||||
$permissionDeniedList = [];
|
||||
|
||||
foreach ($fileList as $file) {
|
||||
|
||||
if ($copyOnlyFiles) {
|
||||
$file = pathinfo($file, PATHINFO_BASENAME);
|
||||
}
|
||||
@@ -667,7 +734,6 @@ class Manager
|
||||
*
|
||||
* @param string $dirPath - directory path.
|
||||
* @param bool $removeWithDir - if remove with directory.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function removeInDir($dirPath, $removeWithDir = false)
|
||||
@@ -699,8 +765,8 @@ class Manager
|
||||
/**
|
||||
* Remove items (files or directories).
|
||||
*
|
||||
* @param string | array $items
|
||||
* @param string $dirPath
|
||||
* @param string | array $items
|
||||
* @param string $dirPath
|
||||
* @return boolean
|
||||
*/
|
||||
public function remove($items, $dirPath = null, $removeEmptyDirs = false)
|
||||
@@ -774,8 +840,8 @@ class Manager
|
||||
/**
|
||||
* Check if $dirname is directory.
|
||||
*
|
||||
* @param string $dirname
|
||||
* @param string $basePath
|
||||
* @param string $dirname
|
||||
* @param string $basePath
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
@@ -893,10 +959,10 @@ class Manager
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parent dir name/path
|
||||
* Get parent dir name/path.
|
||||
*
|
||||
* @param string $path
|
||||
* @param boolean $isFullPath
|
||||
* @param string $path
|
||||
* @param boolean $isFullPath
|
||||
* @return string
|
||||
*/
|
||||
public function getParentDirName($path, $isFullPath = true)
|
||||
@@ -907,7 +973,6 @@ class Manager
|
||||
/**
|
||||
* Return content of PHP file.
|
||||
*
|
||||
* @param string $varName - name of variable which contains the content.
|
||||
* @param array $content
|
||||
*
|
||||
* @return string | false
|
||||
@@ -961,12 +1026,8 @@ class Manager
|
||||
|
||||
/**
|
||||
* Check if $paths are writable. Permission denied list are defined in getLastPermissionDeniedList().
|
||||
*
|
||||
* @param array $paths
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isWritableList(array $paths)
|
||||
public function isWritableList(array $paths) : bool
|
||||
{
|
||||
$permissionDeniedList = [];
|
||||
|
||||
@@ -1002,12 +1063,8 @@ class Manager
|
||||
|
||||
/**
|
||||
* Check if $path is writable.
|
||||
*
|
||||
* @param string |array $path
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isWritable($path)
|
||||
public function isWritable(string $path) : bool
|
||||
{
|
||||
$existFile = $this->getExistsPath($path);
|
||||
|
||||
@@ -1016,12 +1073,8 @@ class Manager
|
||||
|
||||
/**
|
||||
* Check if $path is writable.
|
||||
*
|
||||
* @param string | array $path
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isReadable($path)
|
||||
public function isReadable(string $path) : bool
|
||||
{
|
||||
$existFile = $this->getExistsPath($path);
|
||||
|
||||
|
||||
@@ -53,10 +53,10 @@ class ScheduledJob
|
||||
protected $checkingCronPeriod = '25 hours';
|
||||
|
||||
protected $cronSetup = [
|
||||
'linux' => '* * * * * cd {DOCUMENT_ROOT}; {PHP-BIN-DIR} -f {CRON-FILE} > /dev/null 2>&1',
|
||||
'linux' => '* * * * * cd {DOCUMENT_ROOT}; {PHP-BINARY} -f {CRON-FILE} > /dev/null 2>&1',
|
||||
'windows' => '{PHP-BINARY} -f {FULL-CRON-PATH}',
|
||||
'mac' => '* * * * * cd {DOCUMENT_ROOT}; {PHP-BIN-DIR} -f {CRON-FILE} > /dev/null 2>&1',
|
||||
'default' => '* * * * * cd {DOCUMENT_ROOT}; {PHP-BIN-DIR} -f {CRON-FILE} > /dev/null 2>&1',
|
||||
'mac' => '* * * * * cd {DOCUMENT_ROOT}; {PHP-BINARY} -f {CRON-FILE} > /dev/null 2>&1',
|
||||
'default' => '* * * * * cd {DOCUMENT_ROOT}; {PHP-BINARY} -f {CRON-FILE} > /dev/null 2>&1',
|
||||
];
|
||||
|
||||
protected $classFinder;
|
||||
@@ -105,7 +105,6 @@ class ScheduledJob
|
||||
$desc = $language->translate('cronSetup', 'options', 'ScheduledJob');
|
||||
|
||||
$data = array(
|
||||
'PHP-BIN-DIR' => $this->getSystemUtil()->getPhpBin(),
|
||||
'PHP-BINARY' => $this->getSystemUtil()->getPhpBinary(),
|
||||
'CRON-FILE' => $this->cronFile,
|
||||
'DOCUMENT_ROOT' => $this->getSystemUtil()->getRootDir(),
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
namespace Espo\Core\Utils;
|
||||
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
class System
|
||||
{
|
||||
/**
|
||||
@@ -100,22 +102,11 @@ class System
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to PHP
|
||||
*
|
||||
* @return string
|
||||
* Deprecated. Use getPhpBinary()
|
||||
*/
|
||||
public function getPhpBin()
|
||||
{
|
||||
if (isset($_SERVER['PHP_PATH']) && !empty($_SERVER['PHP_PATH'])) {
|
||||
return $_SERVER['PHP_PATH'];
|
||||
}
|
||||
|
||||
$phpBin = @exec('which php');
|
||||
if (!empty($phpBin)) {
|
||||
return $phpBin;
|
||||
}
|
||||
|
||||
return defined("PHP_BINDIR") ? PHP_BINDIR . DIRECTORY_SEPARATOR . 'php' : 'php';
|
||||
return $this->getPhpBinary();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +116,7 @@ class System
|
||||
*/
|
||||
public function getPhpBinary()
|
||||
{
|
||||
return defined("PHP_BINARY") ? PHP_BINARY : 'php';
|
||||
return (new PhpExecutableFinder)->find();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,13 +34,15 @@ use Espo\Core\EntryPoints\{
|
||||
NoAuth,
|
||||
};
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
class OauthCallback implements EntryPoint
|
||||
{
|
||||
use NoAuth;
|
||||
|
||||
public function run()
|
||||
public function run(Request $request)
|
||||
{
|
||||
echo "EspoCRM rocks! If this window is not closed automatically, it's probable that URL you use to access ".
|
||||
echo "If this window is not closed automatically, it's probable that URL you use to access ".
|
||||
"EspoCRM doesn't match URL specified at Administration > Settings > Site URL.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ use Espo\Core\{
|
||||
Utils\Config,
|
||||
Portal\Application as PortalApplication,
|
||||
Portal\ApplicationRunners\Client,
|
||||
Portal\Utils\Url,
|
||||
Api\Request,
|
||||
Api\Response,
|
||||
};
|
||||
@@ -64,26 +65,14 @@ class Portal implements EntryPoint
|
||||
{
|
||||
$data = $data ?? (object) [];
|
||||
|
||||
$id = $request->get('id') ?? $data->id ?? null;
|
||||
$id = $data->id ?? Url::detectPortalId();
|
||||
|
||||
if (!$id) {
|
||||
$url = $_SERVER['REQUEST_URI'];
|
||||
$id = $this->config->get('defaultPortalId');
|
||||
}
|
||||
|
||||
$id = explode('/', $url)[count(explode('/', $_SERVER['SCRIPT_NAME'])) - 1];
|
||||
|
||||
if (!isset($id)) {
|
||||
$url = $_SERVER['REDIRECT_URL'];
|
||||
|
||||
$id = explode('/', $url)[count(explode('/', $_SERVER['SCRIPT_NAME'])) - 1];
|
||||
}
|
||||
|
||||
if (!$id) {
|
||||
$id = $this->config->get('defaultPortalId');
|
||||
}
|
||||
|
||||
if (!$id) {
|
||||
throw new NotFound();
|
||||
}
|
||||
if (!$id) {
|
||||
throw new NotFound("Portal ID not detected.");
|
||||
}
|
||||
|
||||
$application = new PortalApplication($id);
|
||||
|
||||
@@ -34,17 +34,20 @@ use Espo\ORM\Entity;
|
||||
use Espo\Core\{
|
||||
Utils\Config,
|
||||
ORM\EntityManager,
|
||||
ApplicationState,
|
||||
};
|
||||
|
||||
class AssignmentEmailNotification
|
||||
{
|
||||
protected $config;
|
||||
protected $entityManager;
|
||||
protected $applicationState;
|
||||
|
||||
public function __construct(Config $config, EntityManager $entityManager)
|
||||
public function __construct(Config $config, EntityManager $entityManager, ApplicationState $applicationState)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->applicationState = $applicationState;
|
||||
}
|
||||
|
||||
public function afterSave(Entity $entity, array $options = [])
|
||||
@@ -67,17 +70,25 @@ class AssignmentEmailNotification
|
||||
if ($entity->has('assignedUsersIds')) {
|
||||
$userIdList = $entity->getLinkMultipleIdList('assignedUsers');
|
||||
$fetchedAssignedUserIdList = $entity->getFetched('assignedUsersIds');
|
||||
|
||||
if (!is_array($fetchedAssignedUserIdList)) {
|
||||
$fetchedAssignedUserIdList = [];
|
||||
}
|
||||
|
||||
foreach ($userIdList as $userId) {
|
||||
if (in_array($userId, $fetchedAssignedUserIdList)) continue;
|
||||
if (!$this->isNotSelfAssignment($entity, $userId)) continue;
|
||||
if (in_array($userId, $fetchedAssignedUserIdList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->isNotSelfAssignment($entity, $userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->createJob($entity, $userId);
|
||||
}
|
||||
} else {
|
||||
$userId = $entity->get('assignedUserId');
|
||||
|
||||
if (!empty($userId) &&
|
||||
$entity->isAttributeChanged('assignedUserId') && $this->isNotSelfAssignment($entity, $userId)
|
||||
) {
|
||||
@@ -96,26 +107,29 @@ class AssignmentEmailNotification
|
||||
$isNotSelfAssignment = $assignedUserId !== $entity->get('modifiedById');
|
||||
}
|
||||
} else {
|
||||
$isNotSelfAssignment = $assignedUserId !== $this->getUser()->id;
|
||||
$isNotSelfAssignment = $assignedUserId !== $this->applicationState->getUserId();
|
||||
}
|
||||
|
||||
return $isNotSelfAssignment;
|
||||
}
|
||||
|
||||
protected function createJob(Entity $entity, $userId)
|
||||
{
|
||||
$job = $this->entityManager->getEntity('Job');
|
||||
|
||||
$job->set([
|
||||
'serviceName' => 'EmailNotification',
|
||||
'methodName' => 'notifyAboutAssignmentJob',
|
||||
'data' => [
|
||||
'userId' => $userId,
|
||||
'assignerUserId' => $this->getUser()->id,
|
||||
'assignerUserId' => $this->applicationState->getUserId(),
|
||||
'entityId' => $entity->id,
|
||||
'entityType' => $entity->getEntityType(),
|
||||
],
|
||||
'executeTime' => date('Y-m-d H:i:s'),
|
||||
'queue' => 'e0',
|
||||
]);
|
||||
|
||||
$this->entityManager->saveEntity($job);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ use Espo\Core\{
|
||||
Jobs\Job,
|
||||
};
|
||||
|
||||
use Throwable;
|
||||
use DateTime;
|
||||
|
||||
class SubmitPopupReminders implements Job
|
||||
{
|
||||
const REMINDER_PAST_HOURS = 24;
|
||||
@@ -53,21 +56,27 @@ class SubmitPopupReminders implements Job
|
||||
|
||||
public function run()
|
||||
{
|
||||
if (!$this->config->get('useWebSocket')) return;
|
||||
if (!$this->config->get('useWebSocket')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dt = new \DateTime();
|
||||
$dt = new DateTime();
|
||||
|
||||
$now = $dt->format('Y-m-d H:i:s');
|
||||
|
||||
$pastHours = $this->config->get('reminderPastHours', self::REMINDER_PAST_HOURS);
|
||||
|
||||
$nowShifted = $dt->modify('-' . $pastHours . ' hours')->format('Y-m-d H:i:s');
|
||||
|
||||
$reminderList = $this->entityManager->getRepository('Reminder')->where([
|
||||
'type' => 'Popup',
|
||||
'remindAt<=' => $now,
|
||||
'startAt>' => $nowShifted,
|
||||
'isSubmitted' => false,
|
||||
])->find();
|
||||
$reminderList = $this->entityManager
|
||||
->getRepository('Reminder')
|
||||
->where([
|
||||
'type' => 'Popup',
|
||||
'remindAt<=' => $now,
|
||||
'startAt>' => $nowShifted,
|
||||
'isSubmitted' => false,
|
||||
])
|
||||
->find();
|
||||
|
||||
$submitData = [];
|
||||
|
||||
@@ -78,6 +87,7 @@ class SubmitPopupReminders implements Job
|
||||
|
||||
if (!$userId || !$entityType || !$entityId) {
|
||||
$this->deleteReminder($reminder);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -85,19 +95,24 @@ class SubmitPopupReminders implements Job
|
||||
|
||||
if (!$entity) {
|
||||
$this->deleteReminder($reminder);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($entity->hasLinkMultipleField('users')) {
|
||||
$entity->loadLinkMultipleField('users', ['status' => 'acceptanceStatus']);
|
||||
|
||||
$status = $entity->getLinkMultipleColumn('users', 'status', $userId);
|
||||
|
||||
if ($status === 'Declined') {
|
||||
$this->deleteReminder($reminder);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$dateAttribute = 'dateStart';
|
||||
|
||||
if ($entityType === 'Task') {
|
||||
$dateAttribute = 'dateEnd';
|
||||
}
|
||||
@@ -125,10 +140,10 @@ class SubmitPopupReminders implements Job
|
||||
|
||||
foreach ($submitData as $userId => $list) {
|
||||
try {
|
||||
$this->getContainer()->get('webSocketSubmission')->submit('popupNotifications.event', $userId, (object) [
|
||||
$this->webSocketSubmission->submit('popupNotifications.event', $userId, (object) [
|
||||
'list' => $list
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
} catch (Throwable $e) {
|
||||
$GLOBALS['log']->error('Job SubmitPopupReminders: [' . $e->getCode() . '] ' .$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
},
|
||||
"labels": {
|
||||
"Create Task": "Create Task",
|
||||
"Complete": "Complete"
|
||||
"Complete": "Complete",
|
||||
"overdue": "overdue"
|
||||
},
|
||||
"presetFilters": {
|
||||
"actual": "Actual",
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
"massEmail",
|
||||
"status",
|
||||
"target",
|
||||
"sentAt"
|
||||
"sentAt",
|
||||
"emailAddress"
|
||||
]
|
||||
@@ -78,6 +78,7 @@
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"textFilterFields": ["queueItem.id", "queueItem.emailAddress"],
|
||||
"orderBy": "createdAt",
|
||||
"order": "desc"
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"textFilterFields": ["id", "emailAddress"],
|
||||
"orderBy": "createdAt",
|
||||
"order": "desc"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"tab": false,
|
||||
"acl": "recordAllTeamNo",
|
||||
"aclPortal": "recordAllNo",
|
||||
"aclPortalActionList": ["read"],
|
||||
"module": "Crm",
|
||||
"customizable": true,
|
||||
"importable": false,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"tab": true,
|
||||
"acl": "recordAllTeamNo",
|
||||
"aclPortal": "recordAllNo",
|
||||
"aclPortalActionList": ["read"],
|
||||
"module": "Crm",
|
||||
"customizable": true,
|
||||
"importable": true,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"tab": false,
|
||||
"acl": "recordAllTeamNo",
|
||||
"aclPortal": "recordAllNo",
|
||||
"aclPortalActionList": ["read"],
|
||||
"module": "Crm",
|
||||
"customizable": true,
|
||||
"importable": false,
|
||||
|
||||
@@ -873,7 +873,7 @@ class Activities implements
|
||||
|
||||
$service->handleListParams($params);
|
||||
|
||||
$selectParams = $selectManager->getSelectParams($params, false, true);
|
||||
$selectParams = $selectManager->getSelectParams($params, false, true, true);
|
||||
|
||||
$offset = $selectParams['offset'];
|
||||
$limit = $selectParams['limit'];
|
||||
|
||||
@@ -31,14 +31,17 @@ namespace Espo\Modules\Crm\Services;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ConflictSilent;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Modules\Crm\Entities\Lead as LeadEntity;
|
||||
|
||||
use Espo\Core\Templates\Services\Person as PersonService;
|
||||
|
||||
use Espo\Core\Di;
|
||||
|
||||
class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
class Lead extends PersonService implements
|
||||
|
||||
Di\FieldUtilAware
|
||||
{
|
||||
@@ -52,31 +55,36 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
{
|
||||
if (!empty($data->emailId)) {
|
||||
$email = $this->getEntityManager()->getEntity('Email', $data->emailId);
|
||||
|
||||
if ($email && !$email->get('parentId') && $this->getAcl()->check($email)) {
|
||||
$email->set(array(
|
||||
$email->set([
|
||||
'parentType' => 'Lead',
|
||||
'parentId' => $entity->id
|
||||
));
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($email);
|
||||
}
|
||||
}
|
||||
if ($entity->get('campaignId')) {
|
||||
$campaign = $this->getEntityManager()->getEntity('Campaign', $entity->get('campaignId'));
|
||||
if ($campaign) {
|
||||
$log = $this->getEntityManager()->getEntity('CampaignLogRecord');
|
||||
$log->set(array(
|
||||
'action' => 'Lead Created',
|
||||
'actionDate' => date('Y-m-d H:i:s'),
|
||||
'parentType' => 'Lead',
|
||||
'parentId' => $entity->id,
|
||||
'campaignId' => $campaign->id
|
||||
));
|
||||
$this->getEntityManager()->saveEntity($log);
|
||||
}
|
||||
$campaign = $this->getEntityManager()->getEntity('Campaign', $entity->get('campaignId'));
|
||||
|
||||
if ($campaign) {
|
||||
$log = $this->getEntityManager()->getEntity('CampaignLogRecord');
|
||||
|
||||
$log->set([
|
||||
'action' => 'Lead Created',
|
||||
'actionDate' => date('Y-m-d H:i:s'),
|
||||
'parentType' => 'Lead',
|
||||
'parentId' => $entity->id,
|
||||
'campaignId' => $campaign->id,
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getConvertAttributes($id)
|
||||
public function getConvertAttributes(string $id)
|
||||
{
|
||||
$lead = $this->getEntity($id);
|
||||
|
||||
@@ -84,7 +92,7 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$data = array();
|
||||
$data = [];
|
||||
|
||||
$entityList = $this->getMetadata()->get('entityDefs.Lead.convertEntityList', []);
|
||||
|
||||
@@ -93,22 +101,30 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
$convertFieldsDefs = $this->getMetadata()->get('entityDefs.Lead.convertFields', array());
|
||||
|
||||
foreach ($entityList as $entityType) {
|
||||
if (!$this->getAcl()->checkScope($entityType, 'edit')) continue;
|
||||
if (!$this->getAcl()->checkScope($entityType, 'edit')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributes = array();
|
||||
$attributes = [];
|
||||
|
||||
$target = $this->getEntityManager()->getEntity($entityType);
|
||||
|
||||
$fieldMap = array();
|
||||
$fieldMap = [];
|
||||
|
||||
$fieldList = array_keys($this->getMetadata()->get('entityDefs.Lead.fields', []));
|
||||
|
||||
$fieldList = array_keys($this->getMetadata()->get('entityDefs.Lead.fields', array()));
|
||||
foreach ($fieldList as $field) {
|
||||
if (!$this->getMetadata()->get('entityDefs.'.$entityType.'.fields.' . $field)) continue;
|
||||
if (!$this->getMetadata()->get('entityDefs.'.$entityType.'.fields.' . $field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->getMetadata()->get(['entityDefs', $entityType, 'fields', $field, 'type'])
|
||||
!==
|
||||
$this->getMetadata()->get(['entityDefs', 'Lead', 'fields', $field, 'type'])
|
||||
) continue;
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldMap[$field] = $field;
|
||||
}
|
||||
@@ -121,55 +137,69 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
foreach ($fieldMap as $field => $leadField) {
|
||||
$type = $this->getMetadata()->get(['entityDefs', 'Lead', 'fields', $field, 'type']);
|
||||
|
||||
|
||||
if (in_array($type, ['file', 'image'])) {
|
||||
$attachment = $lead->get($field);
|
||||
|
||||
if ($attachment) {
|
||||
$attachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
|
||||
$idAttribute = $field . 'Id';
|
||||
$nameAttribute = $field . 'Name';
|
||||
|
||||
if ($attachment) {
|
||||
$attributes[$idAttribute] = $attachment->id;
|
||||
$attributes[$nameAttribute] = $attachment->get('name');
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
} else if (in_array($type, ['attachmentMultiple'])) {
|
||||
}
|
||||
else if (in_array($type, ['attachmentMultiple'])) {
|
||||
$attachmentList = $lead->get($field);
|
||||
|
||||
if (count($attachmentList)) {
|
||||
$idList = [];
|
||||
$nameHash = (object) [];
|
||||
$typeHash = (object) [];
|
||||
|
||||
foreach ($attachmentList as $attachment) {
|
||||
$attachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
|
||||
|
||||
if ($attachment) {
|
||||
$idList[] = $attachment->id;
|
||||
|
||||
$nameHash->{$attachment->id} = $attachment->get('name');
|
||||
$typeHash->{$attachment->id} = $attachment->get('type');
|
||||
}
|
||||
}
|
||||
|
||||
$attributes[$field . 'Ids'] = $idList;
|
||||
$attributes[$field . 'Names'] = $nameHash;
|
||||
$attributes[$field . 'Types'] = $typeHash;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$leadAttributeList = $this->fieldUtil->getAttributeList('Lead', $leadField);
|
||||
|
||||
$attributeList = $this->fieldUtil->getAttributeList($entityType, $field);
|
||||
|
||||
foreach ($attributeList as $i => $attribute) {
|
||||
if (in_array($attribute, $ignoreAttributeList)) continue;
|
||||
if (in_array($attribute, $ignoreAttributeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$leadAttribute = $leadAttributeList[$i];
|
||||
if (!$lead->has($leadAttribute)) continue;
|
||||
|
||||
if (!$lead->has($leadAttribute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributes[$attribute] = $lead->get($leadAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
$data[$entityType] = $attributes;
|
||||
|
||||
}
|
||||
|
||||
return $data;
|
||||
@@ -197,7 +227,10 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
$account->set(get_object_vars($recordsData->Account));
|
||||
|
||||
if ($duplicateCheck) {
|
||||
$rDuplicateList = $this->getServiceFactory()->create('Account')->findDuplicates($account, $recordsData->Account);
|
||||
$rDuplicateList = $this->getServiceFactory()
|
||||
->create('Account')
|
||||
->findDuplicates($account, $recordsData->Account);
|
||||
|
||||
if ($rDuplicateList) {
|
||||
foreach ($rDuplicateList as $e) {
|
||||
$item = $e->getValueMap();
|
||||
@@ -210,6 +243,7 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
|
||||
if (!$skipSave) {
|
||||
$entityManager->saveEntity($account);
|
||||
|
||||
$lead->set('createdAccountId', $account->id);
|
||||
}
|
||||
}
|
||||
@@ -217,12 +251,16 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
if (!empty($recordsData->Contact)) {
|
||||
$contact = $entityManager->getEntity('Contact');
|
||||
$contact->set(get_object_vars($recordsData->Contact));
|
||||
|
||||
if (isset($account)) {
|
||||
$contact->set('accountId', $account->id);
|
||||
}
|
||||
|
||||
if ($duplicateCheck) {
|
||||
$rDuplicateList = $this->getServiceFactory()->create('Contact')->findDuplicates($contact, $recordsData->Contact);
|
||||
$rDuplicateList = $this->getServiceFactory()
|
||||
->create('Contact')
|
||||
->findDuplicates($contact, $recordsData->Contact);
|
||||
|
||||
if ($rDuplicateList) {
|
||||
foreach ($rDuplicateList as $e) {
|
||||
$item = $e->getValueMap();
|
||||
@@ -235,6 +273,7 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
|
||||
if (!$skipSave) {
|
||||
$entityManager->saveEntity($contact);
|
||||
|
||||
$lead->set('createdContactId', $contact->id);
|
||||
}
|
||||
}
|
||||
@@ -242,15 +281,18 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
if (!empty($recordsData->Opportunity)) {
|
||||
$opportunity = $entityManager->getEntity('Opportunity');
|
||||
$opportunity->set(get_object_vars($recordsData->Opportunity));
|
||||
|
||||
if (isset($account)) {
|
||||
$opportunity->set('accountId', $account->id);
|
||||
}
|
||||
|
||||
if (isset($contact)) {
|
||||
$opportunity->set('contactId', $contact->id);
|
||||
}
|
||||
|
||||
if ($duplicateCheck) {
|
||||
$rDuplicateList = $this->getServiceFactory()->create('Opportunity')
|
||||
$rDuplicateList = $this->getServiceFactory()
|
||||
->create('Opportunity')
|
||||
->findDuplicates($opportunity, $recordsData->Opportunity);
|
||||
|
||||
if ($rDuplicateList) {
|
||||
@@ -265,9 +307,11 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
|
||||
if (!$skipSave) {
|
||||
$entityManager->saveEntity($opportunity);
|
||||
|
||||
if (isset($contact)) {
|
||||
$entityManager->getRepository('Contact')->relate($contact, 'opportunities', $opportunity);
|
||||
}
|
||||
|
||||
$lead->set('createdOpportunityId', $opportunity->id);
|
||||
}
|
||||
}
|
||||
@@ -277,78 +321,110 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
|
||||
'reason' => 'duplicate',
|
||||
'duplicates' => $duplicateList,
|
||||
];
|
||||
throw new \Espo\Core\Exceptions\ConflictSilent(json_encode($reason));
|
||||
|
||||
throw new ConflictSilent(json_encode($reason));
|
||||
}
|
||||
|
||||
$lead->set('status', 'Converted');
|
||||
|
||||
$entityManager->saveEntity($lead);
|
||||
|
||||
if ($meetings = $lead->get('meetings')) {
|
||||
foreach ($meetings as $meeting) {
|
||||
if (!empty($contact)) {
|
||||
$entityManager->getRepository('Meeting')->relate($meeting, 'contacts', $contact);
|
||||
}
|
||||
$leadRepisotory = $entityManager->getRepository('Lead');
|
||||
|
||||
if (!empty($opportunity)) {
|
||||
$meeting->set('parentId', $opportunity->id);
|
||||
$meeting->set('parentType', 'Opportunity');
|
||||
$entityManager->saveEntity($meeting);
|
||||
} else if (!empty($account)) {
|
||||
$meeting->set('parentId', $account->id);
|
||||
$meeting->set('parentType', 'Account');
|
||||
$entityManager->saveEntity($meeting);
|
||||
}
|
||||
$meetings = $leadRepisotory
|
||||
->getRelation($lead, 'meetings')
|
||||
->select(['id', 'parentId', 'parentType'])
|
||||
->find();
|
||||
|
||||
foreach ($meetings as $meeting) {
|
||||
if (!empty($contact)) {
|
||||
$entityManager->getRepository('Meeting')->relate($meeting, 'contacts', $contact);
|
||||
}
|
||||
}
|
||||
if ($calls = $lead->get('calls')) {
|
||||
foreach ($calls as $call) {
|
||||
if (!empty($contact)) {
|
||||
$entityManager->getRepository('Call')->relate($call, 'contacts', $contact);
|
||||
}
|
||||
if (!empty($opportunity)) {
|
||||
$call->set('parentId', $opportunity->id);
|
||||
$call->set('parentType', 'Opportunity');
|
||||
$entityManager->saveEntity($call);
|
||||
} else if (!empty($account)) {
|
||||
$call->set('parentId', $account->id);
|
||||
$call->set('parentType', 'Account');
|
||||
$entityManager->saveEntity($call);
|
||||
}
|
||||
|
||||
if (!empty($opportunity)) {
|
||||
$meeting->set('parentId', $opportunity->id);
|
||||
$meeting->set('parentType', 'Opportunity');
|
||||
|
||||
$entityManager->saveEntity($meeting);
|
||||
}
|
||||
}
|
||||
if ($emails = $lead->get('emails')) {
|
||||
foreach ($emails as $email) {
|
||||
if (!empty($opportunity)) {
|
||||
$email->set('parentId', $opportunity->id);
|
||||
$email->set('parentType', 'Opportunity');
|
||||
$entityManager->saveEntity($email);
|
||||
} else if (!empty($account)) {
|
||||
$email->set('parentId', $account->id);
|
||||
$email->set('parentType', 'Account');
|
||||
$entityManager->saveEntity($email);
|
||||
}
|
||||
else if (!empty($account)) {
|
||||
$meeting->set('parentId', $account->id);
|
||||
$meeting->set('parentType', 'Account');
|
||||
|
||||
$entityManager->saveEntity($meeting);
|
||||
}
|
||||
}
|
||||
|
||||
if ($documents = $lead->get('documents')) {
|
||||
foreach ($documents as $document) {
|
||||
if (!empty($account)) {
|
||||
$entityManager->getRepository('Document')->relate($document, 'accounts', $account);
|
||||
}
|
||||
if (!empty($opportunity)) {
|
||||
$entityManager->getRepository('Document')->relate($document, 'opportunities', $opportunity);
|
||||
}
|
||||
$calls = $leadRepisotory
|
||||
->getRelation($lead, 'calls')
|
||||
->select(['id', 'parentId', 'parentType'])
|
||||
->find();
|
||||
|
||||
foreach ($calls as $call) {
|
||||
if (!empty($contact)) {
|
||||
$entityManager->getRepository('Call')->relate($call, 'contacts', $contact);
|
||||
}
|
||||
|
||||
if (!empty($opportunity)) {
|
||||
$call->set('parentId', $opportunity->id);
|
||||
$call->set('parentType', 'Opportunity');
|
||||
|
||||
$entityManager->saveEntity($call);
|
||||
}
|
||||
else if (!empty($account)) {
|
||||
$call->set('parentId', $account->id);
|
||||
$call->set('parentType', 'Account');
|
||||
|
||||
$entityManager->saveEntity($call);
|
||||
}
|
||||
}
|
||||
|
||||
$emails = $leadRepisotory
|
||||
->getRelation($lead, 'emails')
|
||||
->select(['id', 'parentId', 'parentType'])
|
||||
->find();
|
||||
|
||||
foreach ($emails as $email) {
|
||||
if (!empty($opportunity)) {
|
||||
$email->set('parentId', $opportunity->id);
|
||||
$email->set('parentType', 'Opportunity');
|
||||
|
||||
$entityManager->saveEntity($email);
|
||||
}
|
||||
else if (!empty($account)) {
|
||||
$email->set('parentId', $account->id);
|
||||
$email->set('parentType', 'Account');
|
||||
|
||||
$entityManager->saveEntity($email);
|
||||
}
|
||||
}
|
||||
|
||||
$documents = $leadRepisotory
|
||||
->getRelation($lead, 'documents')
|
||||
->select(['id'])
|
||||
->find();
|
||||
|
||||
foreach ($documents as $document) {
|
||||
if (!empty($account)) {
|
||||
$entityManager->getRepository('Document')->relate($document, 'accounts', $account);
|
||||
}
|
||||
|
||||
if (!empty($opportunity)) {
|
||||
$entityManager->getRepository('Document')->relate($document, 'opportunities', $opportunity);
|
||||
}
|
||||
}
|
||||
|
||||
$streamService = $this->getStreamService();
|
||||
|
||||
if ($streamService->checkIsFollowed($lead, $this->getUser()->id)) {
|
||||
if (!empty($opportunity)) {
|
||||
$streamService->followEntity($opportunity, $this->getUser()->id);
|
||||
}
|
||||
|
||||
if (!empty($account)) {
|
||||
$streamService->followEntity($account, $this->getUser()->id);
|
||||
}
|
||||
|
||||
if (!empty($contact)) {
|
||||
$streamService->followEntity($contact, $this->getUser()->id);
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ class BaseEntity implements Entity
|
||||
// @todo Remove this.
|
||||
if ($this->hasRelation($name) && $this->id && $this->entityManager) {
|
||||
trigger_error(
|
||||
"Accessing related records with Entity::get is deprecated. Use \$entityManager->getRelation(...)->find()",
|
||||
"Accessing related records with Entity::get is deprecated. Use \$repository->getRelation(...)->find()",
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ use Countable;
|
||||
use ArrayAccess;
|
||||
use SeekableIterator;
|
||||
use RuntimeException;
|
||||
use OutOfBoundsException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* A standard collection of entities. It allocates a memory for all entities.
|
||||
@@ -80,7 +82,9 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
{
|
||||
do {
|
||||
$this->position ++;
|
||||
|
||||
$next = false;
|
||||
|
||||
if (!$this->valid() && $this->position <= $this->getLastValidKey()) {
|
||||
$next = true;
|
||||
}
|
||||
@@ -90,13 +94,17 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
private function getLastValidKey()
|
||||
{
|
||||
$keys = array_keys($this->dataList);
|
||||
|
||||
$i = end($keys);
|
||||
|
||||
while ($i > 0) {
|
||||
if (isset($this->dataList[$i])) {
|
||||
break;
|
||||
}
|
||||
|
||||
$i--;
|
||||
}
|
||||
|
||||
return $i;
|
||||
}
|
||||
|
||||
@@ -115,20 +123,23 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
if (!isset($this->dataList[$offset])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getEntityByOffset($offset);
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
if (!($value instanceof Entity)) {
|
||||
throw new \InvalidArgumentException('Only Entity is allowed to be added to EntityCollection.');
|
||||
throw new InvalidArgumentException('Only Entity is allowed to be added to EntityCollection.');
|
||||
}
|
||||
|
||||
if (is_null($offset)) {
|
||||
$this->dataList[] = $value;
|
||||
} else {
|
||||
$this->dataList[$offset] = $value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dataList[$offset] = $value;
|
||||
}
|
||||
|
||||
public function offsetUnset($offset)
|
||||
@@ -144,8 +155,9 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
public function seek($offset)
|
||||
{
|
||||
$this->position = $offset;
|
||||
|
||||
if (!$this->valid()) {
|
||||
throw new \OutOfBoundsException("Invalid seek offset ($offset).");
|
||||
throw new OutOfBoundsException("Invalid seek offset ($offset).");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,13 +172,15 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
|
||||
if ($value instanceof Entity) {
|
||||
return $value;
|
||||
} else if (is_array($value)) {
|
||||
$this->dataList[$offset] = $this->buildEntityFromArray($value);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->dataList[$offset];
|
||||
if (is_array($value)) {
|
||||
$this->dataList[$offset] = $this->buildEntityFromArray($value);
|
||||
|
||||
return $this->dataList[$offset];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function buildEntityFromArray(array $dataArray)
|
||||
@@ -179,9 +193,11 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
|
||||
if ($entity) {
|
||||
$entity->set($dataArray);
|
||||
|
||||
if ($this->isFetched) {
|
||||
$entity->setAsFetched();
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
@@ -213,6 +229,7 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
public function merge(EntityCollection $collection)
|
||||
{
|
||||
$newData = $this->dataList;
|
||||
|
||||
$incomingDataList = $collection->getDataList();
|
||||
|
||||
foreach ($incomingDataList as $v) {
|
||||
@@ -230,12 +247,14 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
if ($this->indexOf($value) !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function indexOf($value)
|
||||
{
|
||||
$index = 0;
|
||||
|
||||
if (is_array($value)) {
|
||||
foreach ($this->dataList as $v) {
|
||||
if (is_array($v)) {
|
||||
@@ -247,6 +266,7 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
$index ++;
|
||||
}
|
||||
} else if ($value instanceof Entity) {
|
||||
@@ -260,9 +280,11 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
$index ++;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -272,14 +294,17 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
|
||||
public function toArray(bool $itemsAsObjects = false) : array
|
||||
{
|
||||
$arr = [];
|
||||
|
||||
foreach ($this as $entity) {
|
||||
if ($itemsAsObjects) {
|
||||
$item = $entity->getValueMap();
|
||||
} else {
|
||||
$item = $entity->toArray();
|
||||
}
|
||||
|
||||
$arr[] = $item;
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
|
||||
@@ -1296,7 +1296,7 @@ abstract class BaseQueryComposer implements QueryComposer
|
||||
$modifiedOrder[] = $newItem;
|
||||
}
|
||||
|
||||
$part = $this->getOrderExpressionPart($entity, $modifiedOrder, null, false, $params, true);
|
||||
$part = $this->getOrderExpressionPart($entity, $modifiedOrder, null, $params, true);
|
||||
|
||||
return $part;
|
||||
}
|
||||
@@ -1446,21 +1446,32 @@ abstract class BaseQueryComposer implements QueryComposer
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return self::getAllAttributesFromComplexExpression($value);
|
||||
$value = [[$value]];
|
||||
}
|
||||
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = [];
|
||||
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $item) {
|
||||
if (!is_array($item) || !isset($item[0])) continue;
|
||||
|
||||
$list = array_merge(
|
||||
$list,
|
||||
self::getAllAttributesFromComplexExpression($item[0])
|
||||
);
|
||||
|
||||
foreach ($value as $item) {
|
||||
if (!is_array($item) || !isset($item[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$expression = $item[0];
|
||||
|
||||
if (strpos($expression, 'LIST:') === 0 && substr_count($expression, ':') === 2) {
|
||||
$expression = explode(':', $expression)[1];
|
||||
}
|
||||
|
||||
$attributeList = self::getAllAttributesFromComplexExpression($expression);
|
||||
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$attributeList
|
||||
);
|
||||
}
|
||||
|
||||
return $list;
|
||||
@@ -1828,7 +1839,6 @@ abstract class BaseQueryComposer implements QueryComposer
|
||||
Entity $entity,
|
||||
$orderBy = null,
|
||||
$order = null,
|
||||
bool $useColumnAlias = false,
|
||||
?array &$params = null,
|
||||
bool $noCustom = false
|
||||
) : ?string {
|
||||
@@ -1849,7 +1859,7 @@ abstract class BaseQueryComposer implements QueryComposer
|
||||
}
|
||||
|
||||
$arr[] = $this->getOrderExpressionPart(
|
||||
$entity, $orderByInternal, $orderInternal, $useColumnAlias, $params, $noCustom
|
||||
$entity, $orderByInternal, $orderInternal, $params, $noCustom
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1860,13 +1870,8 @@ abstract class BaseQueryComposer implements QueryComposer
|
||||
if (strpos($orderBy, 'LIST:') === 0) {
|
||||
list($l, $field, $list) = explode(':', $orderBy);
|
||||
|
||||
if ($useColumnAlias) {
|
||||
$fieldPath = $this->quoteIdentifier(
|
||||
$this->sanitizeSelectAlias($field)
|
||||
);
|
||||
} else {
|
||||
$fieldPath = $this->getAttributePathForOrderBy($entity, $field, $params);
|
||||
}
|
||||
$fieldPath = $this->getAttributePathForOrderBy($entity, $field, $params);
|
||||
|
||||
|
||||
$listQuoted = [];
|
||||
$list = array_reverse(explode(',', $list));
|
||||
@@ -1905,13 +1910,7 @@ abstract class BaseQueryComposer implements QueryComposer
|
||||
return $this->getAttributeOrderSql($entity, $orderBy, $params, $order);
|
||||
}
|
||||
|
||||
if ($useColumnAlias) {
|
||||
$fieldPath = $this->quoteIdentifier(
|
||||
$this->sanitizeSelectAlias($orderBy)
|
||||
);
|
||||
} else {
|
||||
$fieldPath = $this->getAttributePathForOrderBy($entity, $orderBy, $params);
|
||||
}
|
||||
$fieldPath = $this->getAttributePathForOrderBy($entity, $orderBy, $params);
|
||||
|
||||
if (!$fieldPath) {
|
||||
throw new LogicException("Could not handle 'order' for '".$entity->getEntityType()."'.");
|
||||
@@ -1922,7 +1921,7 @@ abstract class BaseQueryComposer implements QueryComposer
|
||||
|
||||
protected function getOrderPart(Entity $entity, $orderBy = null, $order = null, &$params = null) : ?string
|
||||
{
|
||||
return $this->getOrderExpressionPart($entity, $orderBy, $order, false, $params);
|
||||
return $this->getOrderExpressionPart($entity, $orderBy, $order, $params);
|
||||
}
|
||||
|
||||
protected function getAttributePathForOrderBy(Entity $entity, string $orderBy, array $params) : ?string
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
"max": "Max",
|
||||
"translation": "Translation",
|
||||
"previewSize": "Preview Size",
|
||||
"listPreviewSize": "Preview Size in List View",
|
||||
"noEmptyString": "Empty string value is not allowed",
|
||||
"defaultType": "Default Type",
|
||||
"seeMoreDisabled": "Disable Text Cut",
|
||||
@@ -289,6 +290,7 @@
|
||||
},
|
||||
"options": {
|
||||
"previewSize": {
|
||||
"": "Default",
|
||||
"x-small": "X-Small",
|
||||
"small": "Small",
|
||||
"medium": "Medium",
|
||||
|
||||
@@ -144,7 +144,8 @@
|
||||
},
|
||||
"currencyFormat": {
|
||||
"1": "10 USD",
|
||||
"2": "$10"
|
||||
"2": "$10",
|
||||
"3": "10 $"
|
||||
},
|
||||
"personNameFormat": {
|
||||
"firstLast": "First Last",
|
||||
@@ -162,6 +163,19 @@
|
||||
}
|
||||
},
|
||||
"tooltips": {
|
||||
"displayListViewRecordCount": "A total number of records will be shown on the list view.",
|
||||
"currencyList": "What currencies will be available in the system.",
|
||||
"activitiesEntityList": "What records will be available in the Activities panel.",
|
||||
"historyEntityList": "What records will be available in the History panel.",
|
||||
"calendarEntityList": "What records will be available in the Calendar.",
|
||||
"addressStateList": "State suggestions for address fields.",
|
||||
"addressCityList": "City suggestions for address fields.",
|
||||
"addressCountryList": "Country suggestions for address fields.",
|
||||
"exportDisabled": "Users won't be able to export records. Only admin will be allowed.",
|
||||
"globalSearchEntityList": "What records can be searched with Global Search.",
|
||||
"siteUrl": "A URL of this EspoCRM instance. You need to change it if you move to another domain.",
|
||||
"useCache": "Not recommended to disable, unless for development purpose.",
|
||||
"useWebSocket": "WebSocket enables two-way interactive communication between a server and a browser. Requires setting up the WebSocket daemon on your server. Check the documentation for more info.",
|
||||
"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.",
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"Cancel": "Отменить",
|
||||
"Apply": "Применить",
|
||||
"Unlink": "Убрать ссылку",
|
||||
"Mass Update": "Обновить все",
|
||||
"Mass Update": "Массовое обновление",
|
||||
"Export": "Экспортировать",
|
||||
"No Data": "Нет данных",
|
||||
"No Access": "Нет доступа",
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
"uninstallConfirmation": "Ви впевнені, що хочете видалити розширення?",
|
||||
"cronIsNotConfigured": "Заплановані завдання не працюють. Отже, вхідні електронні листи, сповіщення та нагадування не працюють. Будь ласка, дотримуйтесь інструкцій (https://www.espocrm.com/documentation/administration/server-configuration/#user-content-setup-a-crontab), щоб налаштувати cron job.",
|
||||
"newExtensionVersionIsAvailable": "Нова {extensionName} версія {latestVersion} доступна.",
|
||||
"upgradeVersion": "EspoCRM буде оновлено до версії **{версія}**. Будьте терплячі, оскільки це може зайняти деякий час.",
|
||||
"upgradeVersion": "EspoCRM буде оновлено до версії **{version}**. Будьте терплячі, оскільки це може зайняти деякий час.",
|
||||
"upgradeDone": "EspoCRM оновлено до версії **{version}**.",
|
||||
"downloadUpgradePackage": "Завантажте апгрейд [звідси]({url})",
|
||||
"upgradeInfo": "Перегляньте [документацію]({url}) про те, як оновити EspoCRM.\n",
|
||||
@@ -257,4 +257,4 @@
|
||||
"noteStatus": "Повідомлення про оновлення статусу",
|
||||
"passwordChangeLink": "Посилання на зміну паролю"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,10 @@
|
||||
"parent": false
|
||||
},
|
||||
"EmailFolder": {
|
||||
"assignedUser": false
|
||||
"assignedUser": {
|
||||
"read": "yes",
|
||||
"edit": "no"
|
||||
}
|
||||
},
|
||||
"Email": {
|
||||
"inboundEmails": false,
|
||||
|
||||
@@ -53,11 +53,21 @@
|
||||
{
|
||||
"label":"Folders",
|
||||
"link":"#EmailFolder",
|
||||
"configCheck": "!emailFoldersDisabled"
|
||||
"configCheck": "!emailFoldersDisabled",
|
||||
"accessDataList": [
|
||||
{
|
||||
"inPortalDisabled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label":"Filters",
|
||||
"link":"#EmailFilter"
|
||||
"link":"#EmailFilter",
|
||||
"accessDataList": [
|
||||
{
|
||||
"inPortalDisabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"fields": {
|
||||
"timeZone": {
|
||||
"type": "enum",
|
||||
"detault": "",
|
||||
"default": "",
|
||||
"view": "views/preferences/fields/time-zone"
|
||||
},
|
||||
"dateFormat": {
|
||||
@@ -124,7 +124,8 @@
|
||||
"assignmentNotificationsIgnoreEntityTypeList": {
|
||||
"type": "checklist",
|
||||
"translation": "Global.scopeNamesPlural",
|
||||
"view": "views/preferences/fields/assignment-notifications-ignore-entity-type-list"
|
||||
"view": "views/preferences/fields/assignment-notifications-ignore-entity-type-list",
|
||||
"default": []
|
||||
},
|
||||
"assignmentEmailNotificationsIgnoreEntityTypeList": {
|
||||
"type": "checklist",
|
||||
@@ -140,14 +141,17 @@
|
||||
},
|
||||
"signature": {
|
||||
"type": "wysiwyg",
|
||||
"view": "views/preferences/fields/signature"
|
||||
"view": "views/preferences/fields/signature",
|
||||
"default": ""
|
||||
},
|
||||
"defaultReminders": {
|
||||
"type": "jsonArray",
|
||||
"view": "crm:views/meeting/fields/reminders"
|
||||
"view": "crm:views/meeting/fields/reminders",
|
||||
"default": []
|
||||
},
|
||||
"theme": {
|
||||
"type": "enum",
|
||||
"default": "",
|
||||
"view": "views/preferences/fields/theme",
|
||||
"translation": "Global.themes"
|
||||
},
|
||||
@@ -173,7 +177,8 @@
|
||||
},
|
||||
"doNotFillAssignedUserIfNotRequired": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
"tooltip": true,
|
||||
"default": false
|
||||
},
|
||||
"followEntityOnStreamPost": {
|
||||
"type": "bool",
|
||||
@@ -187,16 +192,20 @@
|
||||
"type": "multiEnum",
|
||||
"view": "views/preferences/fields/auto-follow-entity-type-list",
|
||||
"translation": "Global.scopeNamesPlural",
|
||||
"default": [],
|
||||
"tooltip": true
|
||||
},
|
||||
"emailUseExternalClient": {
|
||||
"type": "bool"
|
||||
"type": "bool",
|
||||
"default": false
|
||||
},
|
||||
"scopeColorsDisabled": {
|
||||
"type": "bool"
|
||||
"type": "bool",
|
||||
"default": false
|
||||
},
|
||||
"tabColorsDisabled": {
|
||||
"type": "bool"
|
||||
"type": "bool",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"noDeletedAttribute": true
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"fields": {
|
||||
"useCache": {
|
||||
"type": "bool",
|
||||
"default": true
|
||||
"default": true,
|
||||
"tooltip": true
|
||||
},
|
||||
"recordsPerPage": {
|
||||
"type": "int",
|
||||
@@ -61,7 +62,8 @@
|
||||
"type": "multiEnum",
|
||||
"default": ["USD", "EUR"],
|
||||
"required": true,
|
||||
"view": "views/settings/fields/currency-list"
|
||||
"view": "views/settings/fields/currency-list",
|
||||
"tooltip": true
|
||||
},
|
||||
"defaultCurrency": {
|
||||
"type": "enum",
|
||||
@@ -143,7 +145,8 @@
|
||||
"globalSearchEntityList": {
|
||||
"type": "multiEnum",
|
||||
"translation": "Global.scopeNames",
|
||||
"view": "views/settings/fields/global-search-entity-list"
|
||||
"view": "views/settings/fields/global-search-entity-list",
|
||||
"tooltip": true
|
||||
},
|
||||
"exportDelimiter": {
|
||||
"type": "varchar",
|
||||
@@ -326,7 +329,8 @@
|
||||
},
|
||||
"exportDisabled": {
|
||||
"type": "bool",
|
||||
"default": false
|
||||
"default": false,
|
||||
"tooltip": true
|
||||
},
|
||||
"emailNotificationsDelay": {
|
||||
"type": "int",
|
||||
@@ -399,7 +403,8 @@
|
||||
"type": "varchar"
|
||||
},
|
||||
"displayListViewRecordCount": {
|
||||
"type": "bool"
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
},
|
||||
"userThemesDisabled": {
|
||||
"type": "bool",
|
||||
@@ -461,7 +466,8 @@
|
||||
"disabled": true
|
||||
},
|
||||
"siteUrl": {
|
||||
"type": "varchar"
|
||||
"type": "varchar",
|
||||
"tooltip": true
|
||||
},
|
||||
"applicationName": {
|
||||
"type": "varchar"
|
||||
@@ -490,7 +496,7 @@
|
||||
},
|
||||
"currencyFormat": {
|
||||
"type": "enumInt",
|
||||
"options": [1, 2]
|
||||
"options": [1, 2, 3]
|
||||
},
|
||||
"currencyDecimalPlaces": {
|
||||
"type": "int",
|
||||
@@ -503,15 +509,18 @@
|
||||
},
|
||||
"calendarEntityList": {
|
||||
"type": "multiEnum",
|
||||
"view": "views/settings/fields/calendar-entity-list"
|
||||
"view": "views/settings/fields/calendar-entity-list",
|
||||
"tooltip": true
|
||||
},
|
||||
"activitiesEntityList": {
|
||||
"type": "multiEnum",
|
||||
"view": "views/settings/fields/activities-entity-list"
|
||||
"view": "views/settings/fields/activities-entity-list",
|
||||
"tooltip": true
|
||||
},
|
||||
"historyEntityList": {
|
||||
"type": "multiEnum",
|
||||
"view": "views/settings/fields/history-entity-list"
|
||||
"view": "views/settings/fields/history-entity-list",
|
||||
"tooltip": true
|
||||
},
|
||||
"busyRangesEntityList": {
|
||||
"type": "multiEnum",
|
||||
@@ -578,13 +587,16 @@
|
||||
"tooltip": true
|
||||
},
|
||||
"addressCountryList": {
|
||||
"type": "multiEnum"
|
||||
"type": "multiEnum",
|
||||
"tooltip": true
|
||||
},
|
||||
"addressCityList": {
|
||||
"type": "multiEnum"
|
||||
"type": "multiEnum",
|
||||
"tooltip": true
|
||||
},
|
||||
"addressStateList": {
|
||||
"type": "multiEnum"
|
||||
"type": "multiEnum",
|
||||
"tooltip": true
|
||||
},
|
||||
"jobRunInParallel": {
|
||||
"type": "bool",
|
||||
@@ -621,7 +633,8 @@
|
||||
"tooltip": true
|
||||
},
|
||||
"useWebSocket": {
|
||||
"type": "bool"
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,11 +42,13 @@
|
||||
},
|
||||
{
|
||||
"name":"after",
|
||||
"type":"varchar"
|
||||
"type":"varchar",
|
||||
"view": "views/admin/field-manager/fields/date/after-before"
|
||||
},
|
||||
{
|
||||
"name":"before",
|
||||
"type":"varchar"
|
||||
"type":"varchar",
|
||||
"view": "views/admin/field-manager/fields/date/after-before"
|
||||
},
|
||||
{
|
||||
"type": "bool",
|
||||
|
||||
@@ -39,11 +39,13 @@
|
||||
},
|
||||
{
|
||||
"name":"after",
|
||||
"type":"varchar"
|
||||
"type":"varchar",
|
||||
"view": "views/admin/field-manager/fields/date/after-before"
|
||||
},
|
||||
{
|
||||
"name":"before",
|
||||
"type":"varchar"
|
||||
"type":"varchar",
|
||||
"view": "views/admin/field-manager/fields/date/after-before"
|
||||
},
|
||||
{
|
||||
"type": "bool",
|
||||
|
||||
@@ -39,11 +39,13 @@
|
||||
},
|
||||
{
|
||||
"name":"after",
|
||||
"type":"varchar"
|
||||
"type":"varchar",
|
||||
"view": "views/admin/field-manager/fields/date/after-before"
|
||||
},
|
||||
{
|
||||
"name":"before",
|
||||
"type":"varchar"
|
||||
"type":"varchar",
|
||||
"view": "views/admin/field-manager/fields/date/after-before"
|
||||
},
|
||||
{
|
||||
"type": "bool",
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
"default":"small",
|
||||
"options": ["x-small", "small", "medium", "large"]
|
||||
},
|
||||
{
|
||||
"name": "listPreviewSize",
|
||||
"type": "enum",
|
||||
"default": "small",
|
||||
"options": ["", "small", "medium"],
|
||||
"translation": "Admin.options.previewSize",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"name": "maxFileSize",
|
||||
"type": "float",
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
},
|
||||
{
|
||||
"name":"maxLength",
|
||||
"type":"int"
|
||||
"type":"int",
|
||||
"default": 150,
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
},
|
||||
{
|
||||
"name":"trim",
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"layouts": false,
|
||||
"tab": true,
|
||||
"acl": true,
|
||||
"aclPortal": "recordAllAccountContactOwnNo",
|
||||
"aclPortal": true,
|
||||
"aclPortalLevelList": ["all", "account", "contact", "own", "no"],
|
||||
"aclPortalActionList": ["read"],
|
||||
"notifications": true,
|
||||
"object": true,
|
||||
"customizable": true,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"acl": "recordAllTeamNo",
|
||||
"aclPortal": true,
|
||||
"aclPortalLevelList": ["all", "no"],
|
||||
"aclPortalActionList": ["read"],
|
||||
"customizable": true,
|
||||
"importable": true,
|
||||
"disabled": false
|
||||
|
||||
@@ -162,8 +162,6 @@ class Email extends \Espo\Core\Select\SelectManager
|
||||
$result['whereClause'][] = [
|
||||
'usersMiddle.userId' => $this->getUser()->id
|
||||
];
|
||||
|
||||
$this->addUsersColumns($result);
|
||||
}
|
||||
|
||||
protected function boolFilterOnlyMy(&$result)
|
||||
|
||||
@@ -46,6 +46,7 @@ use Espo\Core\{
|
||||
Utils\Config,
|
||||
Utils\Util,
|
||||
Utils\Language,
|
||||
Utils\FieldUtil,
|
||||
};
|
||||
|
||||
use Espo\Entities\User;
|
||||
@@ -57,8 +58,24 @@ use Espo\ORM\{
|
||||
Entity,
|
||||
};
|
||||
|
||||
use StdClass;
|
||||
use Throwable;
|
||||
|
||||
class App
|
||||
{
|
||||
protected $config;
|
||||
protected $entityManager;
|
||||
protected $metadata;
|
||||
protected $acl;
|
||||
protected $aclManager;
|
||||
protected $dataManager;
|
||||
protected $selectManagerFactory;
|
||||
protected $injectableFactory;
|
||||
protected $serviceFactory;
|
||||
protected $user;
|
||||
protected $preferences;
|
||||
protected $fieldUtil;
|
||||
|
||||
public function __construct(
|
||||
Config $config,
|
||||
EntityManager $entityManager,
|
||||
@@ -70,7 +87,8 @@ class App
|
||||
InjectableFactory $injectableFactory,
|
||||
ServiceFactory $serviceFactory,
|
||||
User $user,
|
||||
Preferences $preferences
|
||||
Preferences $preferences,
|
||||
FieldUtil $fieldUtil
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->entityManager = $entityManager;
|
||||
@@ -83,12 +101,14 @@ class App
|
||||
$this->serviceFactory = $serviceFactory;
|
||||
$this->user = $user;
|
||||
$this->preferences = $preferences;
|
||||
$this->fieldUtil = $fieldUtil;
|
||||
}
|
||||
|
||||
public function getUserData()
|
||||
{
|
||||
$preferencesData = $this->preferences->getValueMap();
|
||||
unset($preferencesData->smtpPassword);
|
||||
|
||||
$this->filterPreferencesData($preferencesData);
|
||||
|
||||
$settingsService = $this->serviceFactory->create('Settings');
|
||||
|
||||
@@ -134,11 +154,16 @@ class App
|
||||
|
||||
foreach (($this->metadata->get(['app', 'appParams']) ?? []) as $paramKey => $item) {
|
||||
$className = $item['className'] ?? null;
|
||||
if (!$className) continue;
|
||||
|
||||
if (!$className) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$itemParams = $this->injectableFactory->create($className)->get();
|
||||
} catch (\Throwable $e) {
|
||||
} catch (Throwable $e) {
|
||||
$GLOBALS['log']->error("appParam {$paramKey}: " . $e->getMessage());
|
||||
|
||||
continue;
|
||||
}
|
||||
$appParams[$paramKey] = $itemParams;
|
||||
@@ -452,19 +477,41 @@ class App
|
||||
$entityTypeList = ['Contact', 'Lead'];
|
||||
|
||||
foreach ($entityTypeList as $entityType) {
|
||||
$entityList = $this->entityManager->getRepository($entityType)->where([
|
||||
'doNotCall' => true,
|
||||
'phoneNumber!=' => null,
|
||||
])->select(['id', 'phoneNumber'])->find();
|
||||
$entityList = $this->entityManager
|
||||
->getRepository($entityType)
|
||||
->where([
|
||||
'doNotCall' => true,
|
||||
'phoneNumber!=' => null,
|
||||
])
|
||||
->select(['id', 'phoneNumber'])
|
||||
->find();
|
||||
|
||||
foreach ($entityList as $entity) {
|
||||
$phoneNumber = $entity->get('phoneNumber');
|
||||
if (!$phoneNumber) continue;
|
||||
$phoneNumberEntity = $this->entityManager->getRepository('PhoneNumber')->getByNumber($phoneNumber);
|
||||
if (!$phoneNumberEntity) continue;
|
||||
$phoneNumberEntity->set('optOut', true);
|
||||
$this->entityManager->saveEntity($phoneNumberEntity);
|
||||
|
||||
if (!$phoneNumber) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$phoneNumberEntity = $this->entityManager->getRepository('PhoneNumber')->getByNumber($phoneNumber);
|
||||
|
||||
if (!$phoneNumberEntity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$phoneNumberEntity->set('optOut', true);
|
||||
|
||||
$this->entityManager->saveEntity($phoneNumberEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function filterPreferencesData(StdClass $data)
|
||||
{
|
||||
$passwordFieldList = $this->fieldUtil->getFieldByTypeList('Preferences', 'password');
|
||||
|
||||
foreach ($passwordFieldList as $field) {
|
||||
unset($data->$field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ class Attachment extends Record
|
||||
|
||||
protected $inlineAttachmentFieldTypeList = ['wysiwyg'];
|
||||
|
||||
protected $adminOnlyHavingInlineAttachmentsEntityTypeList = [
|
||||
'TemplateManager',
|
||||
];
|
||||
|
||||
protected $imageTypeList = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
@@ -160,12 +164,24 @@ class Attachment extends Record
|
||||
|
||||
protected function checkAttachmentField($relatedEntityType, $field, $role = 'Attachment')
|
||||
{
|
||||
if (
|
||||
$this->getUser()->isAdmin()
|
||||
&&
|
||||
$role === 'Inline Attachment'
|
||||
&&
|
||||
in_array($relatedEntityType, $this->adminOnlyHavingInlineAttachmentsEntityTypeList)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldType = $this->getMetadata()->get(['entityDefs', $relatedEntityType, 'fields', $field, 'type']);
|
||||
|
||||
if (!$fieldType) {
|
||||
throw new Error("Field '{$field}' does not exist.");
|
||||
}
|
||||
|
||||
$attachmentFieldTypeListParam = lcfirst(str_replace(' ', '', $role)) . 'FieldTypeList';
|
||||
|
||||
if (!in_array($fieldType, $this->$attachmentFieldTypeListParam)) {
|
||||
throw new Error("Field type '{$fieldType}' is not allowed for {$role}.");
|
||||
}
|
||||
|
||||
@@ -716,7 +716,10 @@ class Email extends Record implements
|
||||
|
||||
$update = $this->entityManager->getQueryBuilder()->update()
|
||||
->in('EmailUser')
|
||||
->set(['folderId' => $folderId])
|
||||
->set([
|
||||
'folderId' => $folderId,
|
||||
'inTrash' => false,
|
||||
])
|
||||
->where([
|
||||
'deleted' => false,
|
||||
'userId' => $userId,
|
||||
|
||||
@@ -133,6 +133,7 @@ class EmailTemplate extends Record implements
|
||||
|
||||
if (!empty($params['relatedId']) && !empty($params['relatedType'])) {
|
||||
$related = $this->getEntityManager()->getEntity($params['relatedType'], $params['relatedId']);
|
||||
|
||||
if ($related) {
|
||||
$entityHash[$related->getEntityType()] = $related;
|
||||
}
|
||||
@@ -155,6 +156,7 @@ class EmailTemplate extends Record implements
|
||||
if ($handlebarsInSubject) {
|
||||
$subject = $htmlizer->render($parent, $subject);
|
||||
}
|
||||
|
||||
if ($handlebarsInBody) {
|
||||
$body = $htmlizer->render($parent, $body);
|
||||
}
|
||||
@@ -244,7 +246,9 @@ class EmailTemplate extends Record implements
|
||||
}
|
||||
|
||||
foreach ($attributeList as $attribute) {
|
||||
if (in_array($attribute, $forbiddenAttributeList)) continue;
|
||||
if (in_array($attribute, $forbiddenAttributeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $entity->get($attribute);
|
||||
|
||||
@@ -252,13 +256,18 @@ class EmailTemplate extends Record implements
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$entity->getAttributeType($attribute)) continue;
|
||||
if (!$entity->getAttributeType($attribute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->formatAttributeValue($entity, $attribute);
|
||||
|
||||
if (is_null($value)) continue;
|
||||
if (is_null($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$variableName = $attribute;
|
||||
|
||||
if (!is_null($prefixLink)) {
|
||||
$variableName = $prefixLink . '.' . $attribute;
|
||||
}
|
||||
@@ -266,7 +275,7 @@ class EmailTemplate extends Record implements
|
||||
$text = str_replace('{' . $type . '.' . $variableName . '}', $value, $text);
|
||||
}
|
||||
|
||||
if (!$skipLinks) {
|
||||
if (!$skipLinks && $entity->id) {
|
||||
$relationDefs = $entity->getRelations();
|
||||
|
||||
foreach ($entity->getRelationList() as $relation) {
|
||||
@@ -301,10 +310,12 @@ class EmailTemplate extends Record implements
|
||||
}
|
||||
|
||||
$replaceData = [];
|
||||
|
||||
$replaceData['today'] = $this->getDateTime()->getTodayString();
|
||||
$replaceData['now'] = $this->getDateTime()->getNowString();
|
||||
|
||||
$timeZone = $this->getConfig()->get('timeZone');
|
||||
|
||||
$now = new DateTime('now', new DateTimezone($timeZone));
|
||||
|
||||
$replaceData['currentYear'] = $now->format('Y');
|
||||
@@ -328,11 +339,13 @@ class EmailTemplate extends Record implements
|
||||
$value = $this->getLanguage()->translateOption($value, $attribute, $entity->getEntityType());
|
||||
} else if ($fieldType === 'array' || $fieldType === 'multiEnum' || $fieldType === 'checklist') {
|
||||
$valueList = [];
|
||||
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $v) {
|
||||
$valueList[] = $this->getLanguage()->translateOption($v, $attribute, $entity->getEntityType());
|
||||
}
|
||||
}
|
||||
|
||||
$value = implode(', ', $valueList);
|
||||
$value = $this->getLanguage()->translateOption($value, $attribute, $entity->getEntityType());
|
||||
} else {
|
||||
@@ -348,13 +361,16 @@ class EmailTemplate extends Record implements
|
||||
if (!is_string($value)) {
|
||||
$value = '';
|
||||
}
|
||||
|
||||
$value = nl2br($value);
|
||||
} else if ($attributeType == 'float') {
|
||||
if (is_float($value)) {
|
||||
$decimalPlaces = 2;
|
||||
|
||||
if ($fieldType === 'currency') {
|
||||
$decimalPlaces = $this->getConfig()->get('currencyDecimalPlaces');
|
||||
}
|
||||
|
||||
$value = $this->getNumber()->format($value, $decimalPlaces);
|
||||
}
|
||||
} else if ($attributeType == 'int') {
|
||||
@@ -370,7 +386,9 @@ class EmailTemplate extends Record implements
|
||||
$value = '';
|
||||
}
|
||||
|
||||
if (!is_string($value)) return null;
|
||||
if (!is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
@@ -389,6 +407,7 @@ class EmailTemplate extends Record implements
|
||||
|
||||
if ($parentId && $parentType) {
|
||||
$e = $this->getEntityManager()->getEntity($parentType, $parentId);
|
||||
|
||||
if ($e && $this->getAcl()->check($e)) {
|
||||
$dataList[] = [
|
||||
'type' => 'parent',
|
||||
@@ -426,6 +445,7 @@ class EmailTemplate extends Record implements
|
||||
foreach ($fm->getEntityTypeFieldList($entityType) as $field) {
|
||||
$fieldType = $fm->getEntityTypeFieldParam($entityType, $field, 'type');
|
||||
$fieldAttributeList = $fm->getAttributeList($entityType, $field);
|
||||
|
||||
if (
|
||||
$fm->getEntityTypeFieldParam($entityType, $field, 'disabled') ||
|
||||
$fm->getEntityTypeFieldParam($entityType, $field, 'directAccessDisabled') ||
|
||||
@@ -441,9 +461,14 @@ class EmailTemplate extends Record implements
|
||||
$attributeList = $fm->getEntityTypeAttributeList($entityType);
|
||||
|
||||
$values = (object) [];
|
||||
|
||||
foreach ($attributeList as $a) {
|
||||
if (!$e->has($a)) continue;
|
||||
if (!$e->has($a)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $emailTemplateService->formatAttributeValue($e, $a);
|
||||
|
||||
if ($value != '') {
|
||||
$values->$a = $value;
|
||||
}
|
||||
|
||||
@@ -43,10 +43,16 @@ use Espo\Core\{
|
||||
Mail\Parsers\MailMimeParser,
|
||||
};
|
||||
|
||||
use Espo\Services\{
|
||||
Email as EmailService,
|
||||
Record as RecordService,
|
||||
};
|
||||
|
||||
use Espo\Entities\{
|
||||
Team,
|
||||
InboundEmail as InboundEmailEntity,
|
||||
User,
|
||||
Email as EmailEntity,
|
||||
};
|
||||
|
||||
use Espo\Core\Di;
|
||||
@@ -57,7 +63,7 @@ use Throwable;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
|
||||
class InboundEmail extends \Espo\Services\Record implements
|
||||
class InboundEmail extends RecordService implements
|
||||
|
||||
Di\CryptAware,
|
||||
Di\EmailSenderAware,
|
||||
@@ -83,9 +89,11 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
protected function handleInput($data)
|
||||
{
|
||||
parent::handleInput($data);
|
||||
|
||||
if (property_exists($data, 'password')) {
|
||||
$data->password = $this->getCrypt()->encrypt($data->password);
|
||||
}
|
||||
|
||||
if (property_exists($data, 'smtpPassword')) {
|
||||
$data->smtpPassword = $this->getCrypt()->encrypt($data->smtpPassword);
|
||||
}
|
||||
@@ -106,6 +114,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
{
|
||||
if (!empty($params['id'])) {
|
||||
$account = $this->getEntityManager()->getEntity('InboundEmail', $params['id']);
|
||||
|
||||
if ($account) {
|
||||
$params['password'] = $this->getCrypt()->decrypt($account->get('password'));
|
||||
$params['imapHandler'] = $account->get('imapHandler');
|
||||
@@ -117,9 +126,11 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$storage = $this->createStorage($params);
|
||||
|
||||
$folders = new RecursiveIteratorIterator($storage->getFolders(), RecursiveIteratorIterator::SELF_FIRST);
|
||||
|
||||
foreach ($folders as $name => $folder) {
|
||||
$foldersArr[] = mb_convert_encoding($folder->getGlobalName(), 'UTF-8', 'UTF7-IMAP');
|
||||
}
|
||||
|
||||
return $foldersArr;
|
||||
}
|
||||
|
||||
@@ -127,6 +138,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
{
|
||||
if (!empty($params['id'])) {
|
||||
$account = $this->getEntityManager()->getEntity('InboundEmail', $params['id']);
|
||||
|
||||
if ($account) {
|
||||
$params['imapHandler'] = $account->get('imapHandler');
|
||||
}
|
||||
@@ -137,6 +149,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
if ($storage->getFolders()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
@@ -153,25 +166,30 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$maxSize = $this->getConfig()->get('emailMessageMaxSize');
|
||||
|
||||
$teamId = $emailAccount->get('teamId');
|
||||
|
||||
$userId = null;
|
||||
|
||||
if ($emailAccount->get('assignToUserId')) {
|
||||
$userId = $emailAccount->get('assignToUserId');
|
||||
}
|
||||
|
||||
$userIdList = [];
|
||||
|
||||
$teamIdList = $emailAccount->getLinkMultipleIdList('teams');
|
||||
|
||||
if (!empty($teamIdList)) {
|
||||
if ($emailAccount->get('addAllTeamUsers')) {
|
||||
$userList = $this->getEntityManager()->getRepository('User')->find([
|
||||
'select' => ['id'],
|
||||
'whereClause' => [
|
||||
$userList = $this->getEntityManager()
|
||||
->getRepository('User')
|
||||
->select(['id'])
|
||||
->distinct()
|
||||
->join('teams')
|
||||
->where([
|
||||
'isActive' => true,
|
||||
'teamsMiddle.teamId' => $teamIdList
|
||||
],
|
||||
'distinct' => true,
|
||||
'joins' => ['teams'],
|
||||
]);
|
||||
'teamsMiddle.teamId' => $teamIdList,
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($userList as $user) {
|
||||
$userIdList[] = $user->id;
|
||||
}
|
||||
@@ -182,18 +200,21 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$teamIdList[] = $teamId;
|
||||
}
|
||||
|
||||
$filterCollection = $this->getEntityManager()->getRepository('EmailFilter')->where([
|
||||
'action' => 'Skip',
|
||||
'OR' => [
|
||||
[
|
||||
'parentType' => $emailAccount->getEntityType(),
|
||||
'parentId' => $emailAccount->id
|
||||
$filterCollection = $this->getEntityManager()
|
||||
->getRepository('EmailFilter')
|
||||
->where([
|
||||
'action' => 'Skip',
|
||||
'OR' => [
|
||||
[
|
||||
'parentType' => $emailAccount->getEntityType(),
|
||||
'parentId' => $emailAccount->id,
|
||||
],
|
||||
[
|
||||
'parentId' => null
|
||||
],
|
||||
],
|
||||
[
|
||||
'parentId' => null
|
||||
]
|
||||
]
|
||||
])->find();
|
||||
])
|
||||
->find();
|
||||
|
||||
$fetchData = $emailAccount->get('fetchData');
|
||||
|
||||
@@ -239,6 +260,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$GLOBALS['log']->error(
|
||||
'InboundEmail '.$emailAccount->id.' (Select Folder) [' . $e->getCode() . '] ' .$e->getMessage()
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -266,6 +288,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$idList = $storage->getIdsFromUID($lastUID);
|
||||
} else {
|
||||
$fetchSince = $emailAccount->get('fetchSince');
|
||||
|
||||
if ($lastDate) {
|
||||
$fetchSince = $lastDate;
|
||||
}
|
||||
@@ -296,8 +319,10 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
|
||||
if ($forceByDate && $previousLastUID) {
|
||||
$uid = $storage->getUniqueId($id);
|
||||
|
||||
if ($uid <= $previousLastUID) {
|
||||
$k++;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -379,6 +404,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
} else {
|
||||
if ($emailAccount->get('reply')) {
|
||||
$user = $this->getEntityManager()->getEntity('User', $userId);
|
||||
|
||||
$this->autoReply($emailAccount, $email, $user);
|
||||
}
|
||||
}
|
||||
@@ -394,15 +420,18 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
|
||||
if ($email && $email->get('dateSent')) {
|
||||
$dt = null;
|
||||
|
||||
try {
|
||||
$dt = new DateTime($email->get('dateSent'));
|
||||
} catch (Exception $e) {}
|
||||
|
||||
if ($dt) {
|
||||
$nowDt = new DateTime();
|
||||
|
||||
if ($dt->getTimestamp() >= $nowDt->getTimestamp()) {
|
||||
$dt = $nowDt;
|
||||
}
|
||||
|
||||
$dateSent = $dt->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s');
|
||||
$lastDate = $dateSent;
|
||||
}
|
||||
@@ -416,6 +445,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
|
||||
if ($forceByDate) {
|
||||
$nowDt = new DateTime();
|
||||
|
||||
$lastDate = $nowDt->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
@@ -425,8 +455,10 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
if ($forceByDate) {
|
||||
if ($previousLastUID) {
|
||||
$idList = $storage->getIdsFromUID($previousLastUID);
|
||||
|
||||
if (count($idList)) {
|
||||
$uid1 = $storage->getUniqueId($idList[0]);
|
||||
|
||||
if ($uid1 && $uid1 > $previousLastUID) {
|
||||
unset($fetchData->byDate->$folder);
|
||||
}
|
||||
@@ -453,6 +485,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$filterCollection, $fetchOnlyHeader, $folderData = null
|
||||
) {
|
||||
$email = null;
|
||||
|
||||
try {
|
||||
$email = $importer->importMessage(
|
||||
$message, $userId, $teamIdList, $userIdList, $filterCollection, $fetchOnlyHeader, $folderData
|
||||
@@ -475,8 +508,10 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
{
|
||||
if ($email->get('parentType') && $email->get('parentId')) {
|
||||
$parent = $this->getEntityManager()->getEntity($email->get('parentType'), $email->get('parentId'));
|
||||
|
||||
if ($parent) {
|
||||
$this->getServiceFactory()->create('Stream')->noteEmailReceived($parent, $email);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -490,6 +525,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$userIdList = $case->getLinkMultipleIdList('assignedUsers');
|
||||
} else {
|
||||
$assignedUserId = $case->get('assignedUserId');
|
||||
|
||||
if ($assignedUserId) {
|
||||
$userIdList[] = $assignedUserId;
|
||||
}
|
||||
@@ -515,48 +551,58 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
{
|
||||
if ($email->get('parentType') == 'Case' && $email->get('parentId')) {
|
||||
$case = $this->getEntityManager()->getEntity('Case', $email->get('parentId'));
|
||||
|
||||
if ($case) {
|
||||
$this->processCaseToEmailFields($case, $email);
|
||||
|
||||
if (!$email->isFetched()) {
|
||||
$this->getServiceFactory()->create('Stream')->noteEmailReceived($case, $email);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (preg_match('/\[#([0-9]+)[^0-9]*\]/', $email->get('name'), $m)) {
|
||||
$caseNumber = $m[1];
|
||||
|
||||
$case = $this->getEntityManager()->getRepository('Case')->where([
|
||||
'number' => $caseNumber
|
||||
])->findOne();
|
||||
$case = $this->getEntityManager()
|
||||
->getRepository('Case')
|
||||
->where([
|
||||
'number' => $caseNumber,
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if ($case) {
|
||||
$email->set('parentType', 'Case');
|
||||
$email->set('parentId', $case->id);
|
||||
|
||||
$this->processCaseToEmailFields($case, $email);
|
||||
|
||||
if (!$email->isFetched()) {
|
||||
$this->getServiceFactory()->create('Stream')->noteEmailReceived($case, $email);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$params = [
|
||||
'caseDistribution' => $inboundEmail->get('caseDistribution'),
|
||||
'teamId' => $inboundEmail->get('teamId'),
|
||||
'userId' => $inboundEmail->get('assignToUserId'),
|
||||
'targetUserPosition' => $inboundEmail->get('targetUserPosition'),
|
||||
'inboundEmailId' => $inboundEmail->id
|
||||
];
|
||||
|
||||
$case = $this->emailToCase($email, $params);
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->getEntityManager()->getEntity('User', $case->get('assignedUserId'));
|
||||
$params = [
|
||||
'caseDistribution' => $inboundEmail->get('caseDistribution'),
|
||||
'teamId' => $inboundEmail->get('teamId'),
|
||||
'userId' => $inboundEmail->get('assignToUserId'),
|
||||
'targetUserPosition' => $inboundEmail->get('targetUserPosition'),
|
||||
'inboundEmailId' => $inboundEmail->id,
|
||||
];
|
||||
|
||||
$this->getServiceFactory()->create('Stream')->noteEmailReceived($case, $email, true);
|
||||
$case = $this->emailToCase($email, $params);
|
||||
|
||||
if ($inboundEmail->get('reply')) {
|
||||
$this->autoReply($inboundEmail, $email, $case, $user);
|
||||
}
|
||||
$user = $this->getEntityManager()->getEntity('User', $case->get('assignedUserId'));
|
||||
|
||||
$this->getServiceFactory()->create('Stream')->noteEmailReceived($case, $email, true);
|
||||
|
||||
if ($inboundEmail->get('reply')) {
|
||||
$this->autoReply($inboundEmail, $email, $case, $user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,6 +627,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
protected function assignLeastBusy(Entity $case, Team $team, $targetUserPosition)
|
||||
{
|
||||
$className = 'Espo\\Custom\\Business\\Distribution\\CaseObj\\LeastBusy';
|
||||
|
||||
if (!class_exists($className)) {
|
||||
$className = 'Espo\\Modules\\Crm\\Business\\Distribution\\CaseObj\\LeastBusy';
|
||||
}
|
||||
@@ -595,10 +642,12 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
}
|
||||
}
|
||||
|
||||
protected function emailToCase(\Espo\Entities\Email $email, array $params = [])
|
||||
protected function emailToCase(EmailEntity $email, array $params = [])
|
||||
{
|
||||
$case = $this->getEntityManager()->getEntity('Case');
|
||||
|
||||
$case->populateDefaults();
|
||||
|
||||
$case->set('name', $email->get('name'));
|
||||
|
||||
$bodyPlain = $email->getBodyPlain();
|
||||
@@ -616,8 +665,13 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
|
||||
foreach ($attachmentIdList as $attachmentId) {
|
||||
$attachment = $this->getEntityManager()->getRepository('Attachment')->get($attachmentId);
|
||||
if (!$attachment) continue;
|
||||
|
||||
if (!$attachment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$copiedAttachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
|
||||
|
||||
$copiedAttachmentIdList[] = $copiedAttachment->id;
|
||||
}
|
||||
|
||||
@@ -626,6 +680,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
}
|
||||
|
||||
$userId = null;
|
||||
|
||||
if (!empty($params['userId'])) {
|
||||
$userId = $params['userId'];
|
||||
}
|
||||
@@ -635,19 +690,23 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
}
|
||||
|
||||
$teamId = false;
|
||||
|
||||
if (!empty($params['teamId'])) {
|
||||
$teamId = $params['teamId'];
|
||||
}
|
||||
|
||||
if ($teamId) {
|
||||
$case->set('teamsIds', [$teamId]);
|
||||
}
|
||||
|
||||
$caseDistribution = '';
|
||||
|
||||
if (!empty($params['caseDistribution'])) {
|
||||
$caseDistribution = $params['caseDistribution'];
|
||||
}
|
||||
|
||||
$targetUserPosition = null;
|
||||
|
||||
if (!empty($params['targetUserPosition'])) {
|
||||
$targetUserPosition = $params['targetUserPosition'];
|
||||
}
|
||||
@@ -660,21 +719,26 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$case->set('status', 'Assigned');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Round-Robin':
|
||||
if ($teamId) {
|
||||
$team = $this->getEntityManager()->getEntity('Team', $teamId);
|
||||
|
||||
if ($team) {
|
||||
$this->assignRoundRobin($case, $team, $targetUserPosition);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Least-Busy':
|
||||
if ($teamId) {
|
||||
$team = $this->getEntityManager()->getEntity('Team', $teamId);
|
||||
|
||||
if ($team) {
|
||||
$this->assignLeastBusy($case, $team, $targetUserPosition);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -686,18 +750,26 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$case->set('accountId', $email->get('accountId'));
|
||||
}
|
||||
|
||||
$contact = $this->getEntityManager()->getRepository('Contact')->join('emailAddresses', 'emailAddressesMultiple')->where([
|
||||
'emailAddressesMultiple.id' => $email->get('fromEmailAddressId')
|
||||
])->findOne();
|
||||
$contact = $this->getEntityManager()
|
||||
->getRepository('Contact')
|
||||
->join('emailAddresses', 'emailAddressesMultiple')
|
||||
->where([
|
||||
'emailAddressesMultiple.id' => $email->get('fromEmailAddressId')
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if ($contact) {
|
||||
$case->set('contactId', $contact->id);
|
||||
} else {
|
||||
if (!$case->get('accountId')) {
|
||||
$lead = $this->getEntityManager()->getRepository('Lead')
|
||||
->join('emailAddresses', 'emailAddressesMultiple')->where([
|
||||
'emailAddressesMultiple.id' => $email->get('fromEmailAddressId')
|
||||
])->findOne();
|
||||
$lead = $this->getEntityManager()
|
||||
->getRepository('Lead')
|
||||
->join('emailAddresses', 'emailAddressesMultiple')
|
||||
->where([
|
||||
'emailAddressesMultiple.id' => $email->get('fromEmailAddressId')
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if ($lead) {
|
||||
$case->set('leadId', $lead->id);
|
||||
}
|
||||
@@ -726,16 +798,22 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
}
|
||||
|
||||
$d = new DateTime();
|
||||
|
||||
$d->modify('-3 hours');
|
||||
|
||||
$threshold = $d->format('Y-m-d H:i:s');
|
||||
|
||||
$emailAddress = $this->getEntityManager()->getRepository('EmailAddress')->getByAddress($email->get('from'));
|
||||
|
||||
$sent = $this->getEntityManager()->getRepository('Email')->where([
|
||||
'toEmailAddresses.id' => $emailAddress->id,
|
||||
'dateSent>' => $threshold,
|
||||
'status' => 'Sent'
|
||||
])->join('toEmailAddresses')->findOne();
|
||||
$sent = $this->getEntityManager()
|
||||
->getRepository('Email')
|
||||
->where([
|
||||
'toEmailAddresses.id' => $emailAddress->id,
|
||||
'dateSent>' => $threshold,
|
||||
'status' => 'Sent',
|
||||
])
|
||||
->join('toEmailAddresses')
|
||||
->findOne();
|
||||
|
||||
if ($sent) {
|
||||
return false;
|
||||
@@ -743,17 +821,23 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
|
||||
try {
|
||||
$replyEmailTemplateId = $inboundEmail->get('replyEmailTemplateId');
|
||||
|
||||
if ($replyEmailTemplateId) {
|
||||
$entityHash = [];
|
||||
|
||||
if ($case) {
|
||||
$entityHash['Case'] = $case;
|
||||
|
||||
if ($case->get('contactId')) {
|
||||
$contact = $this->getEntityManager()->getEntity('Contact', $case->get('contactId'));
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($contact)) {
|
||||
$contact = $this->getEntityManager()->getEntity('Contact');
|
||||
$fromName = \Espo\Services\Email::parseFromName($email->get('fromString'));
|
||||
|
||||
$fromName = EmailService::parseFromName($email->get('fromString'));
|
||||
|
||||
if (!empty($fromName)) {
|
||||
$contact->set('name', $fromName);
|
||||
}
|
||||
@@ -771,11 +855,13 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$replyData = $emailTemplateService->parse($replyEmailTemplateId, ['entityHash' => $entityHash], true);
|
||||
|
||||
$subject = $replyData['subject'];
|
||||
|
||||
if ($case) {
|
||||
$subject = '[#' . $case->get('number'). '] ' . $subject;
|
||||
}
|
||||
|
||||
$reply = $this->getEntityManager()->getEntity('Email');
|
||||
|
||||
$reply->set('to', $email->get('from'));
|
||||
$reply->set('subject', $subject);
|
||||
$reply->set('body', $replyData['body']);
|
||||
@@ -829,8 +915,9 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {}
|
||||
} catch (Exception $e) {
|
||||
$GLOBALS['log']->error("Inbound Email: Auto-reply error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function getSmtpParamsFromInboundEmail(InboundEmailEntity $emailAccount)
|
||||
@@ -844,9 +931,11 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$smtpParams['security'] = $emailAccount->get('smtpSecurity');
|
||||
$smtpParams['username'] = $emailAccount->get('smtpUsername');
|
||||
$smtpParams['password'] = $emailAccount->get('smtpPassword');
|
||||
|
||||
if (array_key_exists('password', $smtpParams)) {
|
||||
$smtpParams['password'] = $this->getCrypt()->decrypt($smtpParams['password']);
|
||||
}
|
||||
|
||||
return $smtpParams;
|
||||
}
|
||||
|
||||
@@ -858,6 +947,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$content = $message->getRawContent();
|
||||
|
||||
$isHard = false;
|
||||
|
||||
if (preg_match('/permanent[ ]*[error|failure]/', $content)) {
|
||||
$isHard = true;
|
||||
}
|
||||
@@ -876,15 +966,21 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
}
|
||||
}
|
||||
|
||||
if (!$queueItemId) return false;
|
||||
if (!$queueItemId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$queueItem = $this->getEntityManager()->getEntity('EmailQueueItem', $queueItemId);
|
||||
if (!$queueItem) return false;
|
||||
|
||||
if (!$queueItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$massEmailId = $queueItem->get('massEmailId');
|
||||
$massEmail = $this->getEntityManager()->getEntity('MassEmail', $massEmailId);
|
||||
|
||||
$campaignId = null;
|
||||
|
||||
if ($massEmail) {
|
||||
$campaignId = $massEmail->get('campaignId');
|
||||
}
|
||||
@@ -897,13 +993,19 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
|
||||
if ($isHard && $emailAddress) {
|
||||
$emailAddressEntity = $this->getEntityManager()->getRepository('EmailAddress')->getByAddress($emailAddress);
|
||||
$emailAddressEntity->set('invalid', true);
|
||||
$this->getEntityManager()->saveEntity($emailAddressEntity);
|
||||
|
||||
if ($emailAddressEntity) {
|
||||
$emailAddressEntity->set('invalid', true);
|
||||
|
||||
$this->getEntityManager()->saveEntity($emailAddressEntity);
|
||||
}
|
||||
}
|
||||
|
||||
if ($campaignId && $target && $target->id) {
|
||||
$this->getCampaignService()
|
||||
->logBounced($campaignId, $queueItemId, $target, $emailAddress, $isHard, null, $queueItem->get('isTest'));
|
||||
->logBounced(
|
||||
$campaignId, $queueItemId, $target, $emailAddress, $isHard, null, $queueItem->get('isTest')
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -918,14 +1020,17 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
return $this->campaignService;
|
||||
}
|
||||
|
||||
public function findAccountForSending(string $emailAddress) : ?\Espo\Entities\InboundEmail
|
||||
public function findAccountForSending(string $emailAddress) : ?InboundEmailEntity
|
||||
{
|
||||
$inboundEmail = $this->getEntityManager()->getRepository('InboundEmail')->where([
|
||||
'status' => 'Active',
|
||||
'useSmtp' => true,
|
||||
'smtpHost!=' => null,
|
||||
'emailAddress' => $emailAddress,
|
||||
])->findOne();
|
||||
$inboundEmail = $this->getEntityManager()
|
||||
->getRepository('InboundEmail')
|
||||
->where([
|
||||
'status' => 'Active',
|
||||
'useSmtp' => true,
|
||||
'smtpHost!=' => null,
|
||||
'emailAddress' => $emailAddress,
|
||||
])
|
||||
->findOne();
|
||||
|
||||
return $inboundEmail;
|
||||
}
|
||||
@@ -933,6 +1038,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
public function findSharedAccountForUser(User $user, $emailAddress)
|
||||
{
|
||||
$groupEmailAccountPermission = $this->getAclManager()->get($user, 'groupEmailAccountPermission');
|
||||
|
||||
$teamIdList = $user->getLinkMultipleIdList('teams');
|
||||
|
||||
$inboundEmail = null;
|
||||
@@ -941,17 +1047,20 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
|
||||
if ($groupEmailAccountPermission && $groupEmailAccountPermission !== 'no') {
|
||||
if ($groupEmailAccountPermission === 'team') {
|
||||
if (!count($teamIdList)) return;
|
||||
if (!count($teamIdList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$selectParams = [
|
||||
'whereClause' => [
|
||||
'status' => 'Active',
|
||||
'useSmtp' => true,
|
||||
'smtpIsShared' => true,
|
||||
'teamsMiddle.teamId' => $teamIdList,
|
||||
'emailAddress' => $emailAddress
|
||||
'emailAddress' => $emailAddress,
|
||||
],
|
||||
'joins' => ['teams'],
|
||||
'distinct' => true
|
||||
'distinct' => true,
|
||||
];
|
||||
} else if ($groupEmailAccountPermission === 'all') {
|
||||
$selectParams = [
|
||||
@@ -959,7 +1068,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
'status' => 'Active',
|
||||
'useSmtp' => true,
|
||||
'smtpIsShared' => true,
|
||||
'emailAddress' => $emailAddress
|
||||
'emailAddress' => $emailAddress,
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -967,6 +1076,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$inboundEmail = $this->getEntityManager()->getRepository('InboundEmail')->findOne($selectParams);
|
||||
|
||||
}
|
||||
|
||||
return $inboundEmail;
|
||||
}
|
||||
|
||||
@@ -1034,9 +1144,11 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
$storage = $this->getStorage($emailAccount);
|
||||
|
||||
$folder = $emailAccount->get('sentFolder');
|
||||
|
||||
if (empty($folder)) {
|
||||
throw new Error("No sent folder for Email Account: " . $emailAccount->id . ".");
|
||||
}
|
||||
|
||||
$storage->appendMessage($message->toString(), $folder);
|
||||
}
|
||||
|
||||
@@ -1044,6 +1156,7 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
{
|
||||
$smtpParams = [];
|
||||
$smtpParams['server'] = $emailAccount->get('smtpHost');
|
||||
|
||||
if ($smtpParams['server']) {
|
||||
$smtpParams['port'] = $emailAccount->get('smtpPort');
|
||||
$smtpParams['auth'] = $emailAccount->get('smtpAuth');
|
||||
@@ -1058,9 +1171,11 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
if ($emailAccount->get('fromName')) {
|
||||
$smtpParams['fromName'] = $emailAccount->get('fromName');
|
||||
}
|
||||
|
||||
if ($emailAccount->get('emailAddress')) {
|
||||
$smtpParams['fromAddress'] = $emailAccount->get('emailAddress');
|
||||
}
|
||||
|
||||
if (array_key_exists('password', $smtpParams) && is_string($smtpParams['password'])) {
|
||||
$smtpParams['password'] = $this->getCrypt()->decrypt($smtpParams['password']);
|
||||
}
|
||||
@@ -1077,7 +1192,9 @@ class InboundEmail extends \Espo\Services\Record implements
|
||||
{
|
||||
$handlerClassName = $emailAccount->get('smtpHandler');
|
||||
|
||||
if (!$handlerClassName) return;
|
||||
if (!$handlerClassName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$handler = $this->injectableFactory->create($handlerClassName);
|
||||
|
||||
@@ -76,7 +76,7 @@ class Portal extends Record implements
|
||||
|
||||
protected function clearRolesCache()
|
||||
{
|
||||
$this->fileManager->removeInDir('data/cache/application/acl-portal');
|
||||
$this->fileManager->removeInDir('data/cache/application/aclPortal');
|
||||
$this->dataManager->updateCacheTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class PortalRole extends Record implements
|
||||
|
||||
protected function clearRolesCache()
|
||||
{
|
||||
$this->fileManager->removeInDir('data/cache/application/acl-portal');
|
||||
$this->fileManager->removeInDir('data/cache/application/aclPortal');
|
||||
$this->dataManager->updateCacheTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ use Espo\Core\{
|
||||
use Espo\Tools\Export\Export as ExportTool;
|
||||
|
||||
use StdClass;
|
||||
use Exception;
|
||||
|
||||
use Espo\Core\Di;
|
||||
|
||||
@@ -1245,13 +1246,19 @@ class Record implements Crud,
|
||||
|
||||
public function delete(string $id)
|
||||
{
|
||||
if (empty($id)) throw new BadRequest("ID is empty.");
|
||||
if (empty($id)) {
|
||||
throw new BadRequest("ID is empty.");
|
||||
}
|
||||
|
||||
$entity = $this->getRepository()->get($id);
|
||||
|
||||
if (!$entity) throw new NotFound("Record {$id} not found.");
|
||||
if (!$entity) {
|
||||
throw new NotFound("Record {$id} not found.");
|
||||
}
|
||||
|
||||
if (!$this->getAcl()->check($entity, 'delete')) throw new ForbiddenSilent("No delete access.");
|
||||
if (!$this->getAcl()->check($entity, 'delete')) {
|
||||
throw new ForbiddenSilent("No delete access.");
|
||||
}
|
||||
|
||||
$this->beforeDeleteEntity($entity);
|
||||
|
||||
@@ -1292,6 +1299,7 @@ class Record implements Crud,
|
||||
}
|
||||
|
||||
$maxSize = 0;
|
||||
|
||||
if ($disableCount) {
|
||||
if (!empty($params['maxSize'])) {
|
||||
$maxSize = $params['maxSize'];
|
||||
@@ -1376,10 +1384,16 @@ class Record implements Crud,
|
||||
];
|
||||
|
||||
foreach ($statusList as $status) {
|
||||
if (in_array($status, $statusIgnoreList)) continue;
|
||||
if (!$status) continue;
|
||||
if (in_array($status, $statusIgnoreList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$selectParamsSub = $selectParams;
|
||||
|
||||
$selectParamsSub['whereClause'][] = [
|
||||
$statusField => $status
|
||||
];
|
||||
@@ -1454,12 +1468,19 @@ class Record implements Crud,
|
||||
|
||||
public function restoreDeleted(string $id)
|
||||
{
|
||||
if (!$this->getUser()->isAdmin()) throw new Forbidden();
|
||||
if (!$this->getUser()->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$entity = $this->getEntityEvenDeleted($id);
|
||||
|
||||
if (!$entity) throw new NotFound();
|
||||
if (!$entity->get('deleted')) throw new Forbidden();
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$entity->get('deleted')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$this->getRepository()->restoreDeleted($entity->id);
|
||||
|
||||
@@ -1475,6 +1496,7 @@ class Record implements Crud,
|
||||
return $this->getConfig()->get('maxSelectTextAttributeLengthForList', self::MAX_SELECT_TEXT_ATTRIBUTE_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1487,12 +1509,15 @@ class Record implements Crud,
|
||||
public function findLinked(string $id, string $link, array $params) : RecordCollection
|
||||
{
|
||||
$entity = $this->getRepository()->get($id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->getAcl()->check($entity, 'read')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (empty($link)) {
|
||||
throw new Error();
|
||||
}
|
||||
@@ -1515,6 +1540,7 @@ class Record implements Crud,
|
||||
}
|
||||
|
||||
$methodName = 'findLinkedEntities' . ucfirst($link);
|
||||
|
||||
if (method_exists($this, $methodName)) {
|
||||
return $this->$methodName($id, $params);
|
||||
}
|
||||
@@ -1638,35 +1664,45 @@ class Record implements Crud,
|
||||
}
|
||||
|
||||
$entity = $this->getRepository()->get($id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if ($this->noEditAccessRequiredForLink) {
|
||||
if (!$this->getAcl()->check($entity, 'read')) throw new Forbidden();
|
||||
if (!$this->getAcl()->check($entity, 'read')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
} else {
|
||||
if (!$this->getAcl()->check($entity, 'edit')) throw new Forbidden();
|
||||
if (!$this->getAcl()->check($entity, 'edit')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
$methodName = 'link' . ucfirst($link);
|
||||
|
||||
if ($link !== 'entity' && $link !== 'entityMass' && method_exists($this, $methodName)) {
|
||||
return $this->$methodName($id, $foreignId);
|
||||
}
|
||||
|
||||
$foreignEntityType = $entity->getRelationParam($link, 'entity');
|
||||
|
||||
if (!$foreignEntityType) {
|
||||
throw new Error("Entity '{$this->entityType}' has not relation '{$link}'.");
|
||||
}
|
||||
|
||||
$foreignEntity = $this->getEntityManager()->getEntity($foreignEntityType, $foreignId);
|
||||
|
||||
if (!$foreignEntity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$accessActionRequired = 'edit';
|
||||
|
||||
if (in_array($link, $this->noEditAccessRequiredLinkList)) {
|
||||
$accessActionRequired = 'read';
|
||||
}
|
||||
|
||||
if (!$this->getAcl()->check($foreignEntity, $accessActionRequired)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
@@ -1712,30 +1748,39 @@ class Record implements Crud,
|
||||
}
|
||||
|
||||
if ($this->noEditAccessRequiredForLink) {
|
||||
if (!$this->getAcl()->check($entity, 'read')) throw new Forbidden();
|
||||
if (!$this->getAcl()->check($entity, 'read')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
} else {
|
||||
if (!$this->getAcl()->check($entity, 'edit')) throw new Forbidden();
|
||||
if (!$this->getAcl()->check($entity, 'edit')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
$methodName = 'unlink' . ucfirst($link);
|
||||
|
||||
if ($link !== 'entity' && method_exists($this, $methodName)) {
|
||||
return $this->$methodName($id, $foreignId);
|
||||
}
|
||||
|
||||
$foreignEntityType = $entity->getRelationParam($link, 'entity');
|
||||
|
||||
if (!$foreignEntityType) {
|
||||
throw new Error("Entity '{$this->entityType}' has not relation '{$link}'.");
|
||||
}
|
||||
|
||||
$foreignEntity = $this->getEntityManager()->getEntity($foreignEntityType, $foreignId);
|
||||
|
||||
if (!$foreignEntity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$accessActionRequired = 'edit';
|
||||
|
||||
if (in_array($link, $this->noEditAccessRequiredLinkList)) {
|
||||
$accessActionRequired = 'read';
|
||||
}
|
||||
|
||||
if (!$this->getAcl()->check($foreignEntity, $accessActionRequired)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
@@ -1756,6 +1801,7 @@ class Record implements Crud,
|
||||
if (!$this->getMetadata()->get(['scopes', $this->entityType, 'stream'])) throw new NotFound();
|
||||
|
||||
$entity = $this->getRepository()->get($id);
|
||||
|
||||
if (!$entity) throw new NotFound();
|
||||
if (!$this->getAcl()->check($entity, 'edit')) throw new Forbidden();
|
||||
if (!$this->getAcl()->check($entity, 'stream')) throw new Forbidden();
|
||||
@@ -1776,6 +1822,7 @@ class Record implements Crud,
|
||||
if (!$this->getMetadata()->get(['scopes', $this->entityType, 'stream'])) throw new NotFound();
|
||||
|
||||
$entity = $this->getRepository()->get($id);
|
||||
|
||||
if (!$entity) throw new NotFound();
|
||||
if (!$this->getAcl()->check($entity, 'edit')) throw new Forbidden();
|
||||
if (!$this->getAcl()->check($entity, 'stream')) throw new Forbidden();
|
||||
@@ -1810,14 +1857,17 @@ class Record implements Crud,
|
||||
}
|
||||
|
||||
$entity = $this->getRepository()->get($id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->getAcl()->check($entity, 'edit')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$methodName = 'massLink' . ucfirst($link);
|
||||
|
||||
if (method_exists($this, $methodName)) {
|
||||
return $this->$methodName($id, $where, $selectData);
|
||||
}
|
||||
@@ -1874,7 +1924,9 @@ class Record implements Crud,
|
||||
|
||||
public function massUpdate(array $params, StdClass $data)
|
||||
{
|
||||
if ($this->getAcl()->get('massUpdatePermission') !== 'yes') throw new Forbidden();
|
||||
if ($this->getAcl()->get('massUpdatePermission') !== 'yes') {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$resultIdList = [];
|
||||
$repository = $this->getRepository();
|
||||
@@ -1891,11 +1943,13 @@ class Record implements Crud,
|
||||
foreach ($collection as $entity) {
|
||||
if ($this->getAcl()->check($entity, 'edit') && $this->checkEntityForMassUpdate($entity, $data)) {
|
||||
$entity->set($data);
|
||||
|
||||
try {
|
||||
$this->processValidation($entity, $data);
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->checkAssignment($entity)) {
|
||||
$repository->save($entity, ['massUpdate' => true, 'skipStreamNotesAcl' => true]);
|
||||
$resultIdList[] = $entity->id;
|
||||
@@ -1944,8 +1998,11 @@ class Record implements Crud,
|
||||
foreach ($collection as $entity) {
|
||||
if ($this->getAcl()->check($entity, 'delete') && $this->checkEntityForMassRemove($entity)) {
|
||||
$repository->remove($entity);
|
||||
|
||||
$resultIdList[] = $entity->id;
|
||||
|
||||
$count++;
|
||||
|
||||
$this->processActionHistoryRecord('delete', $entity);
|
||||
}
|
||||
}
|
||||
@@ -1960,7 +2017,9 @@ class Record implements Crud,
|
||||
|
||||
public function massRecalculateFormula(array $params)
|
||||
{
|
||||
if (!$this->getUser()->isAdmin()) throw new Forbidden();
|
||||
if (!$this->getUser()->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
@@ -1981,7 +2040,10 @@ class Record implements Crud,
|
||||
public function follow(string $id, ?string $userId = null)
|
||||
{
|
||||
$entity = $this->getRepository()->get($id);
|
||||
if (!$entity) throw new NotFoundSilent();
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFoundSilent();
|
||||
}
|
||||
|
||||
if (!$this->getAcl()->check($entity, 'stream')) {
|
||||
throw new Forbidden();
|
||||
@@ -1997,7 +2059,10 @@ class Record implements Crud,
|
||||
public function unfollow(string $id, ?string $userId = null)
|
||||
{
|
||||
$entity = $this->getRepository()->get($id);
|
||||
if (!$entity) throw new NotFoundSilent();
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFoundSilent();
|
||||
}
|
||||
|
||||
if (empty($userId)) {
|
||||
$userId = $this->getUser()->id;
|
||||
@@ -2021,7 +2086,10 @@ class Record implements Crud,
|
||||
$collection = $this->getRepository()->sth()->find($selectParams);
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
if (!$this->getAcl()->check($entity, 'stream') || !$this->getAcl()->check($entity, 'read')) continue;
|
||||
if (!$this->getAcl()->check($entity, 'stream') || !$this->getAcl()->check($entity, 'read')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($streamService->followEntity($entity, $userId)) {
|
||||
$resultIdList[] = $entity->id;
|
||||
}
|
||||
@@ -2031,7 +2099,9 @@ class Record implements Crud,
|
||||
'count' => count($resultIdList),
|
||||
];
|
||||
|
||||
if (isset($params['ids'])) $result['ids'] = $resultIdList;
|
||||
if (isset($params['ids'])) {
|
||||
$result['ids'] = $resultIdList;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
@@ -2042,7 +2112,9 @@ class Record implements Crud,
|
||||
|
||||
$streamService = $this->getStreamService();
|
||||
|
||||
if (empty($userId)) $userId = $this->getUser()->id;
|
||||
if (empty($userId)) {
|
||||
$userId = $this->getUser()->id;
|
||||
}
|
||||
|
||||
$selectParams = $this->convertMassActionSelectParams($params);
|
||||
|
||||
@@ -2066,24 +2138,36 @@ class Record implements Crud,
|
||||
protected function convertMassActionSelectParams($params)
|
||||
{
|
||||
if (array_key_exists('ids', $params)) {
|
||||
if (!is_array($params['ids'])) throw new BadRequest();
|
||||
$selectParams = $this->getSelectParams([]);
|
||||
$selectParams['whereClause'][] = [
|
||||
'id' => $params['ids']
|
||||
];
|
||||
} else if (array_key_exists('where', $params)) {
|
||||
$p = ['where' => $params['where']];
|
||||
if (!empty($params['selectData']) && is_array($params['selectData'])) {
|
||||
foreach ($params['selectData'] as $k => $v) {
|
||||
$p[$k] = $v;
|
||||
}
|
||||
if (!is_array($params['ids'])) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
$selectParams = $this->getSelectParams($p);
|
||||
} else {
|
||||
throw new BadRequest();
|
||||
|
||||
$selectParams = $this->getSelectParams([]);
|
||||
|
||||
$selectParams['whereClause'][] = [
|
||||
'id' => $params['ids'],
|
||||
];
|
||||
|
||||
return $selectParams;
|
||||
}
|
||||
|
||||
return $selectParams;
|
||||
if (array_key_exists('where', $params)) {
|
||||
$searchParams = [
|
||||
'where' => $params['where'],
|
||||
];
|
||||
|
||||
if (!empty($params['selectData']) && is_array($params['selectData'])) {
|
||||
foreach ($params['selectData'] as $k => $v) {
|
||||
$searchParams[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
unset($searchParams['select']);
|
||||
|
||||
return $this->getSelectParams($searchParams);
|
||||
}
|
||||
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
protected function getDuplicateWhereClause(Entity $entity, $data)
|
||||
@@ -2099,7 +2183,12 @@ class Record implements Crud,
|
||||
if ($entity->id) {
|
||||
$where['id!='] = $entity->id;
|
||||
}
|
||||
$duplicate = $this->getRepository()->select(['id'])->where($where)->findOne();
|
||||
|
||||
$duplicate = $this->getRepository()
|
||||
->select(['id'])
|
||||
->where($where)
|
||||
->findOne();
|
||||
|
||||
if ($duplicate) {
|
||||
return true;
|
||||
}
|
||||
@@ -2133,7 +2222,9 @@ class Record implements Crud,
|
||||
|
||||
$limit = self::FIND_DUPLICATES_LIMIT;
|
||||
|
||||
$duplicateList = $this->getRepository()->limit(0, $limit)->find($selectParams);
|
||||
$duplicateList = $this->getRepository()
|
||||
->limit(0, $limit)
|
||||
->find($selectParams);
|
||||
|
||||
if (count($duplicateList)) {
|
||||
return $duplicateList;
|
||||
@@ -2168,14 +2259,17 @@ class Record implements Crud,
|
||||
foreach ($this->internalAttributeList as $attribute) {
|
||||
$entity->clear($attribute);
|
||||
}
|
||||
|
||||
foreach ($this->forbiddenAttributeList as $attribute) {
|
||||
$entity->clear($attribute);
|
||||
}
|
||||
|
||||
if (!$this->getUser()->isAdmin()) {
|
||||
foreach ($this->onlyAdminAttributeList as $attribute) {
|
||||
$entity->clear($attribute);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->getAcl()->getScopeForbiddenAttributeList($entity->getEntityType(), 'read') as $attribute) {
|
||||
$entity->clear($attribute);
|
||||
}
|
||||
@@ -2216,14 +2310,16 @@ class Record implements Crud,
|
||||
|
||||
$this->beforeMerge($entity, $sourceList, $attributes);
|
||||
|
||||
$fieldDefs = $this->getMetadata()->get('entityDefs.' . $entity->getEntityType() . '.fields', array());
|
||||
$fieldDefs = $this->getMetadata()->get('entityDefs.' . $entity->getEntityType() . '.fields', []);
|
||||
|
||||
$hasPhoneNumber = false;
|
||||
|
||||
if (!empty($fieldDefs['phoneNumber']) && $fieldDefs['phoneNumber']['type'] == 'phone') {
|
||||
$hasPhoneNumber = true;
|
||||
}
|
||||
|
||||
$hasEmailAddress = false;
|
||||
|
||||
if (!empty($fieldDefs['emailAddress']) && $fieldDefs['emailAddress']['type'] == 'email') {
|
||||
$hasEmailAddress = true;
|
||||
}
|
||||
@@ -2239,6 +2335,7 @@ class Record implements Crud,
|
||||
if ($hasEmailAddress) {
|
||||
$emailAddressToRelateList = [];
|
||||
$emailAddressList = $repository->findRelated($entity, 'emailAddresses');
|
||||
|
||||
foreach ($emailAddressList as $emailAddress) {
|
||||
$emailAddressToRelateList[] = $emailAddress;
|
||||
}
|
||||
@@ -2266,12 +2363,14 @@ class Record implements Crud,
|
||||
|
||||
if ($hasPhoneNumber) {
|
||||
$phoneNumberList = $repository->findRelated($source, 'phoneNumbers');
|
||||
|
||||
foreach ($phoneNumberList as $phoneNumber) {
|
||||
$phoneNumberToRelateList[] = $phoneNumber;
|
||||
}
|
||||
}
|
||||
if ($hasEmailAddress) {
|
||||
$emailAddressList = $repository->findRelated($source, 'emailAddresses');
|
||||
|
||||
foreach ($emailAddressList as $emailAddress) {
|
||||
$emailAddressToRelateList[] = $emailAddress;
|
||||
}
|
||||
@@ -2280,10 +2379,12 @@ class Record implements Crud,
|
||||
|
||||
$mergeLinkList = [];
|
||||
$linksDefs = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'links']);
|
||||
|
||||
foreach ($linksDefs as $link => $d) {
|
||||
if (!empty($d['notMergeable'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($d['type']) && in_array($d['type'], ['hasMany', 'hasChildren'])) {
|
||||
$mergeLinkList[] = $link;
|
||||
}
|
||||
@@ -2292,6 +2393,7 @@ class Record implements Crud,
|
||||
foreach ($sourceList as $source) {
|
||||
foreach ($mergeLinkList as $link) {
|
||||
$linkedList = $repository->findRelated($source, $link);
|
||||
|
||||
foreach ($linkedList as $linked) {
|
||||
$repository->relate($entity, $link, $linked);
|
||||
}
|
||||
@@ -2306,10 +2408,13 @@ class Record implements Crud,
|
||||
|
||||
if ($hasEmailAddress) {
|
||||
$emailAddressData = [];
|
||||
|
||||
foreach ($emailAddressToRelateList as $i => $emailAddress) {
|
||||
$o = (object) [];
|
||||
|
||||
$o->emailAddress = $emailAddress->get('name');
|
||||
$o->primary = false;
|
||||
|
||||
if (empty($attributes->emailAddress)) {
|
||||
if ($i === 0) {
|
||||
$o->primary = true;
|
||||
@@ -2317,8 +2422,10 @@ class Record implements Crud,
|
||||
} else {
|
||||
$o->primary = $o->emailAddress === $attributes->emailAddress;
|
||||
}
|
||||
|
||||
$o->optOut = $emailAddress->get('optOut');
|
||||
$o->invalid = $emailAddress->get('invalid');
|
||||
|
||||
$emailAddressData[] = $o;
|
||||
}
|
||||
$attributes->emailAddressData = $emailAddressData;
|
||||
@@ -2326,10 +2433,12 @@ class Record implements Crud,
|
||||
|
||||
if ($hasPhoneNumber) {
|
||||
$phoneNumberData = [];
|
||||
|
||||
foreach ($phoneNumberToRelateList as $i => $phoneNumber) {
|
||||
$o = (object) [];
|
||||
$o->phoneNumber = $phoneNumber->get('name');
|
||||
$o->primary = false;
|
||||
|
||||
if (empty($attributes->phoneNumber)) {
|
||||
if ($i === 0) {
|
||||
$o->primary = true;
|
||||
@@ -2337,7 +2446,9 @@ class Record implements Crud,
|
||||
} else {
|
||||
$o->primary = $o->phoneNumber === $attributes->phoneNumber;
|
||||
}
|
||||
|
||||
$o->type = $phoneNumber->get('type');
|
||||
|
||||
$phoneNumberData[] = $o;
|
||||
}
|
||||
$attributes->phoneNumberData = $phoneNumberData;
|
||||
@@ -2364,9 +2475,11 @@ class Record implements Crud,
|
||||
protected function findLinkedFollowers($id, $params)
|
||||
{
|
||||
$entity = $this->getRepository()->get($id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->getAcl()->check($entity, 'read')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
@@ -2396,32 +2509,51 @@ class Record implements Crud,
|
||||
foreach ($fields as $field => $item) {
|
||||
if (!empty($item['duplicateIgnore']) || in_array($field, $this->duplicateIgnoreFieldList)) {
|
||||
$attributeToIgnoreList = $fieldManager->getAttributeList($this->entityType, $field);
|
||||
|
||||
foreach ($attributeToIgnoreList as $attribute) {
|
||||
unset($attributes->$attribute);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($item['type'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($item['type'])) continue;
|
||||
$type = $item['type'];
|
||||
|
||||
if (in_array($type, ['file', 'image'])) {
|
||||
$attachment = $entity->get($field);
|
||||
if ($attachment) {
|
||||
$attachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
|
||||
|
||||
$attachment = $this->getEntityManager()
|
||||
->getRepository('Attachment')
|
||||
->getCopiedAttachment($attachment);
|
||||
|
||||
$idAttribute = $field . 'Id';
|
||||
|
||||
if ($attachment) {
|
||||
$attributes->$idAttribute = $attachment->id;
|
||||
}
|
||||
}
|
||||
} else if (in_array($type, ['attachmentMultiple'])) {
|
||||
$attachmentList = $entity->get($field);
|
||||
|
||||
if (count($attachmentList)) {
|
||||
$idList = [];
|
||||
$nameHash = (object) [];
|
||||
$typeHash = (object) [];
|
||||
|
||||
foreach ($attachmentList as $attachment) {
|
||||
$attachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
|
||||
$attachment = $this->getEntityManager()
|
||||
->getRepository('Attachment')
|
||||
->getCopiedAttachment($attachment);
|
||||
|
||||
$attachment->set('field', $field);
|
||||
|
||||
$this->getEntityManager()->saveEntity($attachment);
|
||||
|
||||
if ($attachment) {
|
||||
$idList[] = $attachment->id;
|
||||
$nameHash->{$attachment->id} = $attachment->get('name');
|
||||
@@ -2435,8 +2567,12 @@ class Record implements Crud,
|
||||
} else if ($type === 'linkMultiple') {
|
||||
$foreignLink = $entity->getRelationParam($field, 'foreign');
|
||||
$foreignEntityType = $entity->getRelationParam($field, 'entity');
|
||||
|
||||
if ($foreignEntityType && $foreignLink) {
|
||||
$foreignRelationType = $this->getMetadata()->get(['entityDefs', $foreignEntityType, 'links', $foreignLink, 'type']);
|
||||
$foreignRelationType = $this->getMetadata()->get(
|
||||
['entityDefs', $foreignEntityType, 'links', $foreignLink, 'type']
|
||||
);
|
||||
|
||||
if ($foreignRelationType !== 'hasMany') {
|
||||
unset($attributes->{$field . 'Ids'});
|
||||
unset($attributes->{$field . 'Names'});
|
||||
@@ -2461,7 +2597,9 @@ class Record implements Crud,
|
||||
|
||||
$duplicatingEntityId = $data->_duplicatingEntityId;
|
||||
if (!$duplicatingEntityId) return;
|
||||
|
||||
$duplicatingEntity = $this->getEntityManager()->getEntity($entity->getEntityType(), $duplicatingEntityId);
|
||||
|
||||
if (!$duplicatingEntity) return;
|
||||
if (!$this->getAcl()->check($duplicatingEntity, 'read')) return;
|
||||
|
||||
@@ -2474,6 +2612,7 @@ class Record implements Crud,
|
||||
|
||||
foreach ($this->duplicatingLinkList as $link) {
|
||||
$linkedList = $repository->findRelated($duplicatingEntity, $link);
|
||||
|
||||
foreach ($linkedList as $linked) {
|
||||
$repository->relate($entity, $link, $linked);
|
||||
}
|
||||
|
||||
@@ -675,7 +675,7 @@ class User extends Record implements
|
||||
|
||||
protected function clearPortalRolesCache()
|
||||
{
|
||||
$this->fileManager->removeInDir('data/cache/application/acl-portal');
|
||||
$this->fileManager->removeInDir('data/cache/application/aclPortal');
|
||||
$this->dataManager->updateCacheTimestamp();
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,8 @@ class Export
|
||||
|
||||
$collection = null;
|
||||
|
||||
$exportAllFields = !array_key_exists('fieldList', $params);
|
||||
|
||||
if (array_key_exists('collection', $params)) {
|
||||
$collection = $params['collection'];
|
||||
} else {
|
||||
@@ -138,26 +140,33 @@ class Export
|
||||
|
||||
if (array_key_exists('ids', $params)) {
|
||||
$ids = $params['ids'];
|
||||
|
||||
$where = [
|
||||
[
|
||||
'type' => 'in',
|
||||
'field' => 'id',
|
||||
'value' => $ids
|
||||
'value' => $ids,
|
||||
]
|
||||
];
|
||||
$selectParams = $selectManager->getSelectParams(['where' => $where], true, true);
|
||||
|
||||
$selectParams = $selectManager->getSelectParams(['where' => $where], true, true, true);
|
||||
}
|
||||
else if (array_key_exists('where', $params)) {
|
||||
$where = $params['where'];
|
||||
|
||||
$p = [];
|
||||
$p['where'] = $where;
|
||||
$searchParams = [];
|
||||
|
||||
$searchParams['where'] = $where;
|
||||
|
||||
if (!empty($params['selectData']) && is_array($params['selectData'])) {
|
||||
foreach ($params['selectData'] as $k => $v) {
|
||||
$p[$k] = $v;
|
||||
$searchParams[$k] = $v;
|
||||
}
|
||||
}
|
||||
$selectParams = $this->getSelectParams($p);
|
||||
|
||||
unset($searchParams['select']);
|
||||
|
||||
$selectParams = $selectManager->getSelectParams($searchParams, true, true, true);
|
||||
}
|
||||
else {
|
||||
throw new BadRequest();
|
||||
@@ -172,14 +181,15 @@ class Export
|
||||
|
||||
$select = Select::fromRaw($selectParams);
|
||||
|
||||
$collection = $this->entityManager->getRepository($this->entityType)
|
||||
$collection = $this->entityManager
|
||||
->getRepository($this->entityType)
|
||||
->clone($select)
|
||||
->sth()
|
||||
->find();
|
||||
}
|
||||
|
||||
$attributeListToSkip = [
|
||||
'deleted'
|
||||
'deleted',
|
||||
];
|
||||
|
||||
foreach ($this->skipAttributeList as $attribute) {
|
||||
@@ -208,15 +218,12 @@ class Export
|
||||
}
|
||||
}
|
||||
|
||||
if (!array_key_exists('fieldList', $params)) {
|
||||
$exportAllFields = true;
|
||||
|
||||
if ($exportAllFields) {
|
||||
$fieldDefs = $this->metadata->get(['entityDefs', $this->entityType, 'fields'], []);
|
||||
$fieldList = array_keys($fieldDefs);
|
||||
|
||||
array_unshift($fieldList, 'id');
|
||||
} else {
|
||||
$exportAllFields = false;
|
||||
$fieldList = $params['fieldList'];
|
||||
}
|
||||
|
||||
@@ -225,6 +232,7 @@ class Export
|
||||
unset($fieldList[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
$fieldList = array_values($fieldList);
|
||||
|
||||
if (method_exists($exportObj, 'filterFieldList')) {
|
||||
|
||||
@@ -56,7 +56,11 @@ class Csv
|
||||
public function loadAdditionalFields(Entity $entity, $fieldList)
|
||||
{
|
||||
foreach ($fieldList as $field) {
|
||||
if ($this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']) === 'linkMultiple') {
|
||||
$fieldType = $this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']);
|
||||
|
||||
if (
|
||||
$fieldType === 'linkMultiple' || $fieldType === 'attachmentMultiple'
|
||||
) {
|
||||
if (!$entity->has($field . 'Ids')) {
|
||||
$entity->loadLinkMultipleField($field);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,9 @@ class Xlsx
|
||||
}
|
||||
}
|
||||
foreach ($fieldList as $field) {
|
||||
if ($this->getMetadata()->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']) === 'linkMultiple') {
|
||||
$fieldType = $this->getMetadata()->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']);
|
||||
|
||||
if ($fieldType === 'linkMultiple' || $fieldType === 'attachmentMultiple') {
|
||||
if (!$entity->has($field . 'Ids')) {
|
||||
$entity->loadLinkMultipleField($field);
|
||||
}
|
||||
@@ -348,21 +350,24 @@ class Xlsx
|
||||
} else if ($type == 'currency') {
|
||||
if (array_key_exists($name.'Currency', $row) && array_key_exists($name, $row)) {
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name] ? $row[$name] : '');
|
||||
$currency = $row[$name . 'Currency'];
|
||||
$currencySymbol = $this->getMetadata()->get(['app', 'currency', 'symbolMap', $currency], '');
|
||||
|
||||
$currency = $row[$name . 'Currency'] ?? $this->getConfig()->get('defaultCurrency');
|
||||
|
||||
$sheet->getStyle("$col$rowNumber")
|
||||
->getNumberFormat()
|
||||
->setFormatCode('[$'.$currencySymbol.'-409]#,##0.00;-[$'.$currencySymbol.'-409]#,##0.00');
|
||||
->setFormatCode(
|
||||
$this->getCurrencyFormatCode($currency)
|
||||
);
|
||||
}
|
||||
} else if ($type == 'currencyConverted') {
|
||||
if (array_key_exists($name, $row)) {
|
||||
$currency = $this->getConfig()->get('defaultCurrency');
|
||||
$currencySymbol = $this->getMetadata()->get(['app', 'currency', 'symbolMap', $currency], '');
|
||||
|
||||
$sheet->getStyle("$col$rowNumber")
|
||||
->getNumberFormat()
|
||||
->setFormatCode('[$'.$currencySymbol.'-409]#,##0.00;-[$'.$currencySymbol.'-409]#,##0.00');
|
||||
->setFormatCode(
|
||||
$this->getCurrencyFormatCode($currency)
|
||||
);
|
||||
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name] ? $row[$name] : '');
|
||||
}
|
||||
@@ -442,7 +447,7 @@ class Xlsx
|
||||
}
|
||||
$sheet->setCellValue("$col$rowNumber", $value);
|
||||
}
|
||||
} else if ($type == 'linkMultiple') {
|
||||
} else if ($type == 'linkMultiple' || $type == 'attachmentMultiple') {
|
||||
if (array_key_exists($name . 'Ids', $row) && array_key_exists($name . 'Names', $row)) {
|
||||
$nameList = [];
|
||||
foreach ($row[$name . 'Ids'] as $relatedId) {
|
||||
@@ -661,4 +666,17 @@ class Xlsx
|
||||
|
||||
return $xlsx;
|
||||
}
|
||||
|
||||
protected function getCurrencyFormatCode(string $currency) : string
|
||||
{
|
||||
$currencySymbol = $this->getMetadata()->get(['app', 'currency', 'symbolMap', $currency], '');
|
||||
|
||||
$currencyFormat = $this->getConfig()->get('currencyFormat') ?? 2;
|
||||
|
||||
if ($currencyFormat == 3) {
|
||||
return '#,##0.00_-"' . $currencySymbol . '"';
|
||||
}
|
||||
|
||||
return '[$'.$currencySymbol.'-409]#,##0.00;-[$'.$currencySymbol.'-409]#,##0.00';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +37,12 @@ use Espo\Core\{
|
||||
Utils\Language,
|
||||
};
|
||||
|
||||
class LabelManager implements Di\DefaultLanguageAware, Di\MetadataAware, Di\FileManagerAware
|
||||
class LabelManager implements Di\DefaultLanguageAware, Di\MetadataAware, Di\FileManagerAware, Di\DataCacheAware
|
||||
{
|
||||
use Di\DefaultLanguageSetter;
|
||||
use Di\MetadataSetter;
|
||||
use Di\FileManagerSetter;
|
||||
use Di\DataCacheSetter;
|
||||
|
||||
protected $ignoreList = [
|
||||
'Global.sets',
|
||||
@@ -71,7 +72,7 @@ class LabelManager implements Di\DefaultLanguageAware, Di\MetadataAware, Di\File
|
||||
|
||||
public function getScopeData($language, $scope)
|
||||
{
|
||||
$languageObj = new Language($language, $this->fileManager, $this->metadata);
|
||||
$languageObj = new Language($language, $this->fileManager, $this->metadata, $this->dataCache);
|
||||
|
||||
$data = $languageObj->get($scope);
|
||||
|
||||
@@ -179,8 +180,8 @@ class LabelManager implements Di\DefaultLanguageAware, Di\MetadataAware, Di\File
|
||||
|
||||
public function saveLabels($language, $scope, $labels)
|
||||
{
|
||||
$languageObj = new Language($language, $this->fileManager, $this->metadata);
|
||||
$languageOriginalObj = new Language($language, $this->fileManager, $this->metadata, false, true);
|
||||
$languageObj = new Language($language, $this->fileManager, $this->metadata, $this->dataCache);
|
||||
$languageOriginalObj = new Language($language, $this->fileManager, $this->metadata, $this->dataCache, false, true);
|
||||
|
||||
$returnDataHash = [];
|
||||
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="edit-container">{{{edit}}}</div>
|
||||
<div class="edit-container record">{{{edit}}}</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep) {
|
||||
define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
@@ -42,25 +42,32 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
|
||||
return {
|
||||
scopeList: this.scopeList,
|
||||
scope: this.scope,
|
||||
isNew: !(this.id)
|
||||
isNew: !(this.id),
|
||||
};
|
||||
},
|
||||
|
||||
events: {
|
||||
'change .scope-switcher input[name="scope"]': function () {
|
||||
this.notify('Loading...');
|
||||
|
||||
var scope = $('.scope-switcher input[name="scope"]:checked').val();
|
||||
this.scope = scope;
|
||||
|
||||
this.getModelFactory().create(this.scope, function (model) {
|
||||
model.populateDefaults();
|
||||
var attributes = this.getView('edit').fetch();
|
||||
|
||||
attributes = _.extend(attributes, this.getView('edit').model.toJSON());
|
||||
|
||||
model.set(attributes);
|
||||
|
||||
this.model = model;
|
||||
|
||||
this.createRecordView(model, function (view) {
|
||||
view.render();
|
||||
view.notify(false);
|
||||
});
|
||||
|
||||
this.handleAccess(model);
|
||||
}.bind(this));
|
||||
},
|
||||
@@ -75,11 +82,13 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
|
||||
|
||||
if (this.options.allDay) {
|
||||
var allDayScopeList = this.getMetadata().get('clientDefs.Calendar.allDayScopeList') || [];
|
||||
|
||||
if (~allDayScopeList.indexOf(this.scope)) {
|
||||
this.model.set('dateStart', null);
|
||||
this.model.set('dateEnd', null);
|
||||
this.model.set('dateStartDate', null);
|
||||
this.model.set('dateEndDate', this.options.dateEndDate);
|
||||
|
||||
if (this.options.dateEndDate !== this.options.dateStartDate) {
|
||||
this.model.set('dateStartDate', this.options.dateStartDate)
|
||||
}
|
||||
@@ -102,7 +111,9 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
|
||||
this.dateIsChanged = true;
|
||||
}
|
||||
}, this);
|
||||
this.listenTo(this.model, 'change:dateEnd', function (m, value, o) {
|
||||
|
||||
this.listenTo(
|
||||
this.model, 'change:dateEnd', function (m, value, o) {
|
||||
if (o.ui || o.updatedByDuration) {
|
||||
this.dateIsChanged = true;
|
||||
}
|
||||
@@ -115,6 +126,7 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
|
||||
if (this.id && !this.getAcl().checkModel(model, 'edit') || !this.id && !this.getAcl().checkModel(model, 'create')) {
|
||||
this.hideButton('save');
|
||||
this.hideButton('fullForm');
|
||||
|
||||
this.$el.find('button[data-name="save"]').addClass('hidden');
|
||||
this.$el.find('button[data-name="fullForm"]').addClass('hidden');
|
||||
} else {
|
||||
@@ -131,8 +143,10 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
|
||||
|
||||
afterRender: function () {
|
||||
Dep.prototype.afterRender.call(this);
|
||||
|
||||
if (this.hasView('edit')) {
|
||||
var model = this.getView('edit').model;
|
||||
|
||||
if (model) {
|
||||
this.handleAccess(model);
|
||||
}
|
||||
@@ -145,6 +159,7 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
|
||||
|
||||
if (!this.options.id && !this.options.scope) {
|
||||
var scopeList = [];
|
||||
|
||||
this.scopeList.forEach(function (scope) {
|
||||
if (this.getAcl().check(scope, 'create')) {
|
||||
if (~this.enabledScopeList.indexOf(scope)) {
|
||||
@@ -152,6 +167,7 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.scopeList = scopeList;
|
||||
|
||||
var calendarDefaultEntity = scopeList[0];
|
||||
@@ -187,6 +203,7 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
|
||||
this.confirm(this.translate('removeRecordConfirmation', 'messages'), function () {
|
||||
var $buttons = this.dialog.$el.find('.modal-footer button');
|
||||
$buttons.addClass('disabled');
|
||||
|
||||
model.destroy({
|
||||
success: function () {
|
||||
this.trigger('after:destroy', model);
|
||||
@@ -194,10 +211,9 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
|
||||
}.bind(this),
|
||||
error: function () {
|
||||
$buttons.removeClass('disabled');
|
||||
}
|
||||
},
|
||||
});
|
||||
}, this);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -34,19 +34,30 @@ define('crm:views/case/record/detail', 'views/record/detail', function (Dep) {
|
||||
|
||||
setupActionItems: function () {
|
||||
Dep.prototype.setupActionItems.call(this);
|
||||
|
||||
if (
|
||||
this.getAcl().checkModel(this.model, 'edit') &&
|
||||
!~['Closed', 'Rejected', 'Duplicate'].indexOf(this.model.get('status')) &&
|
||||
this.getAcl().checkField(this.entityType, 'status', 'edit')
|
||||
) {
|
||||
this.dropdownItemList.push({
|
||||
'label': 'Close',
|
||||
'name': 'close',
|
||||
});
|
||||
this.dropdownItemList.push({
|
||||
'label': 'Reject',
|
||||
'name': 'reject',
|
||||
});
|
||||
|
||||
var statusList = this.getMetadata().get(
|
||||
['entityDefs', 'Case', 'fields', 'status', 'options']
|
||||
) || [];
|
||||
|
||||
if (~statusList.indexOf('Closed')) {
|
||||
this.dropdownItemList.push({
|
||||
'label': 'Close',
|
||||
'name': 'close',
|
||||
});
|
||||
}
|
||||
|
||||
if (~statusList.indexOf('Rejected')) {
|
||||
this.dropdownItemList.push({
|
||||
'label': 'Reject',
|
||||
'name': 'reject',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -68,14 +68,14 @@ define('crm:views/record/panels/tasks', 'views/record/panels/relationship', func
|
||||
name: 'name',
|
||||
link: true,
|
||||
},
|
||||
{
|
||||
name: 'isOverdue'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'isOverdue'
|
||||
},
|
||||
{name: 'assignedUser'},
|
||||
{name: 'status'},
|
||||
{name: 'dateEnd'},
|
||||
{name: 'status'},
|
||||
]
|
||||
]
|
||||
},
|
||||
|
||||
@@ -32,7 +32,8 @@ Espo.define('crm:views/task/fields/is-overdue', 'views/fields/base', function (D
|
||||
|
||||
readOnly: true,
|
||||
|
||||
_template: '{{#if isOverdue}}<span class="label label-danger">{{translate "overdue"}}</span>{{/if}}',
|
||||
_template: '{{#if isOverdue}}<span class="label label-danger">' +
|
||||
'{{translate "overdue" scope="Task"}}</span>{{/if}}',
|
||||
|
||||
data: function () {
|
||||
var isOverdue = false;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ul class="list-group">
|
||||
<ul class="list-group no-side-margin">
|
||||
<li data-id="inbox" class="list-group-item">
|
||||
<a href="javascript:" data-action="selectFolder" data-id="inbox" class="side-link">{{translate 'inbox' category='presetFilters' scope='Email'}}</a>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{{#if isNotEmpty}}
|
||||
{{value}} {{currencySymbol}}
|
||||
{{else}}
|
||||
{{#if valueIsSet}}{{{translate 'None'}}}{{else}}...{{/if}}
|
||||
{{/if}}
|
||||
@@ -0,0 +1,3 @@
|
||||
{{#if isNotEmpty}}
|
||||
<span title="{{currencySymbol}}{{value}}">{{value}} {{currencySymbol}}</span>
|
||||
{{/if}}
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="button-container">
|
||||
<button class="btn btn-default pull-right hidden" data-action="reset">{{translate 'Reset'}}</button>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default dropdown-toggle select-field" data-toggle="dropdown" tabindex="-1">{{translate 'Select Field'}} <span class="caret"></span></button>
|
||||
<button class="btn btn-default dropdown-toggle select-field" data-toggle="dropdown" tabindex="-1">{{translate 'Add Field'}} <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu pull-left filter-list">
|
||||
{{#each ../fieldList}}
|
||||
<li data-name="{{./this}}"><a href="javascript:" data-name="{{./this}}" data-action="add-field">{{translate this scope=../../entityType category='fields'}}</a></li>
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
<div class="cell">
|
||||
<a href="javascript:"
|
||||
class="action{{#unless showFold}} hidden{{/unless}} small"
|
||||
data-action="fold"
|
||||
data-id="{{model.id}}"><span class="fas fa-chevron-down"></span></a>
|
||||
|
||||
<div class="cell">
|
||||
<a href="javascript:" class="action{{#unless showFold}} hidden{{/unless}} small" data-action="fold" data-id="{{model.id}}"><span class="fas fa-chevron-down"></span></a>
|
||||
<a href="javascript:" class="action{{#unless showUnfold}} hidden{{/unless}} small" data-action="unfold" data-id="{{model.id}}"><span class="fas fa-chevron-right"></span></a>
|
||||
<span data-name="white-space" data-id="{{model.id}}" class="empty-icon{{#unless isEnd}} hidden{{/unless}}"> </span>
|
||||
<a href="javascript:"
|
||||
class="action{{#unless showUnfold}} hidden{{/unless}} small"
|
||||
data-action="unfold"
|
||||
data-id="{{model.id}}"><span class="fas fa-chevron-right"></span></a>
|
||||
|
||||
<a href="#{{model.name}}/view/{{model.id}}" class="link{{#if isSelected}} text-bold{{/if}}" data-id="{{model.id}}">{{name}}</a>
|
||||
</div>
|
||||
<div class="children{{#unless isUnfolded}} hidden{{/unless}}">{{{children}}}</div>
|
||||
<span data-name="white-space" data-id="{{model.id}}" class="empty-icon{{#unless isEnd}} hidden{{/unless}}"> </span>
|
||||
|
||||
<a href="#{{model.name}}/view/{{model.id}}"
|
||||
class="link{{#if isSelected}} text-bold{{/if}}" data-id="{{model.id}}"
|
||||
>{{name}}</a>
|
||||
|
||||
{{#unless readOnly}}
|
||||
<a href="javascript:"
|
||||
class="action small remove-link hidden"
|
||||
data-action="remove" data-id="{{model.id}}"
|
||||
title="{{translate 'Remove'}}"
|
||||
>
|
||||
<span class="fas fa-times"></span>
|
||||
</a>
|
||||
{{/unless}}
|
||||
</div>
|
||||
<div class="children{{#unless isUnfolded}} hidden{{/unless}}">{{{children}}}</div>
|
||||
@@ -14,7 +14,7 @@
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
<div class="list list-expanded">
|
||||
<div class="list list-expanded list-tree">
|
||||
{{#if showRootMenu}}
|
||||
<div class="btn-group pull-right">
|
||||
<a href="javascript:" class="small dropdown-toggle btn-link" data-toggle="dropdown">
|
||||
|
||||
+17
-1
@@ -142,7 +142,10 @@ define('ui', [], function () {
|
||||
if (this.removeOnClose) {
|
||||
this.$el.on('hidden.bs.modal', function (e) {
|
||||
if (this.$el.get(0) == e.target) {
|
||||
if (this.skipRemove) return;
|
||||
if (this.skipRemove) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.remove();
|
||||
}
|
||||
}.bind(this));
|
||||
@@ -324,11 +327,22 @@ define('ui', [], function () {
|
||||
|
||||
$('body > .popover').addClass('hidden');
|
||||
};
|
||||
|
||||
Dialog.prototype.hide = function () {
|
||||
this.$el.find('.modal-content').addClass('hidden');
|
||||
};
|
||||
|
||||
Dialog.prototype.hideWithBackdrop = function () {
|
||||
var $modalBackdrop = $('.modal-backdrop');
|
||||
$modalBackdrop.last().addClass('hidden');
|
||||
|
||||
$($modalBackdrop.get($modalBackdrop.length - 2)).removeClass('hidden');
|
||||
|
||||
var $modalConainer = $('.modal-container');
|
||||
$($modalConainer.get($modalConainer.length - 2)).removeClass('overlaid');
|
||||
|
||||
this.skipRemove = true;
|
||||
|
||||
setTimeout(function () {
|
||||
this.skipRemove = false;
|
||||
}.bind(this), 50);
|
||||
@@ -337,6 +351,7 @@ define('ui', [], function () {
|
||||
|
||||
this.$el.find('.modal-content').addClass('hidden');
|
||||
};
|
||||
|
||||
Dialog.prototype.close = function () {
|
||||
var $modalBackdrop = $('.modal-backdrop');
|
||||
$modalBackdrop.last().removeClass('hidden');
|
||||
@@ -347,6 +362,7 @@ define('ui', [], function () {
|
||||
this.$el.modal('hide');
|
||||
$(this).trigger('dialog:close');
|
||||
};
|
||||
|
||||
Dialog.prototype.remove = function () {
|
||||
this.onRemove();
|
||||
this.$el.remove();
|
||||
|
||||
@@ -88,6 +88,7 @@ define('views/admin/field-manager/edit', ['view', 'model'], function (Dep, Model
|
||||
this.model.set('tooltipText', this.getLanguage().translate(this.field, 'tooltips', this.scope));
|
||||
}
|
||||
} else {
|
||||
this.model.scope = this.scope;
|
||||
this.model.set('type', this.type);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/************************************************************************
|
||||
* 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.
|
||||
************************************************************************/
|
||||
|
||||
define('views/admin/field-manager/fields/date/after-before', 'views/fields/varchar', function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
setupOptions: function () {
|
||||
Dep.prototype.setupOptions.call(this);
|
||||
|
||||
if (!this.model.scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
var list = this.getFieldManager().getEntityTypeFieldList(
|
||||
this.model.scope,
|
||||
{
|
||||
typeList: ['date', 'datetime', 'datetimeOptional'],
|
||||
}
|
||||
);
|
||||
|
||||
if (this.model.get('name')) {
|
||||
list = list.filter(function (item) {
|
||||
return item !== this.model.get('name');
|
||||
}, this);
|
||||
}
|
||||
|
||||
this.params.options = list;
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
@@ -56,7 +56,7 @@ define('views/admin/formula/modals/add-function', ['views/modal', 'model'], func
|
||||
setup: function () {
|
||||
this.header = this.translate('Function');
|
||||
|
||||
this.documentationUrl = 'https://www.espocrm.com/documentation/administration/formula/';
|
||||
this.documentationUrl = 'https://docs.espocrm.com/administration/formula/';
|
||||
|
||||
this.functionDataList = this.getMetadata().get('app.formula.functionList') || [];
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@ define('views/admin/index', 'view', function (Dep) {
|
||||
panelItem.items.forEach(function (item) {
|
||||
item.label = this.translate(item.label, 'labels', 'Admin');
|
||||
panelItem.itemList.push(item);
|
||||
item.keywords = [];
|
||||
}, this);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
Espo.define('views/admin/integrations/index', 'view', function (Dep) {
|
||||
define('views/admin/integrations/index', 'view', function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
@@ -51,12 +51,13 @@ Espo.define('views/admin/integrations/index', 'view', function (Dep) {
|
||||
},
|
||||
|
||||
setup: function () {
|
||||
this.integrationList = Object.keys(this.getMetadata().get('integrations') || {});;
|
||||
this.integrationList = Object.keys(this.getMetadata().get('integrations') || {});
|
||||
|
||||
this.integration = this.options.integration || null;
|
||||
|
||||
this.on('after:render', function () {
|
||||
this.renderHeader();
|
||||
|
||||
if (!this.integration) {
|
||||
this.renderDefaultPage();
|
||||
} else {
|
||||
@@ -70,34 +71,45 @@ Espo.define('views/admin/integrations/index', 'view', function (Dep) {
|
||||
|
||||
this.getRouter().navigate('#Admin/integrations/name=' + integration, {trigger: false});
|
||||
|
||||
var viewName = this.getMetadata().get('integrations.' + integration + '.view') || 'views/admin/integrations/' + Espo.Utils.camelCaseToHyphen(this.getMetadata().get('integrations.' + integration + '.authMethod'));
|
||||
var viewName = this.getMetadata().get('integrations.' + integration + '.view') ||
|
||||
'views/admin/integrations/' +
|
||||
Espo.Utils.camelCaseToHyphen(this.getMetadata().get('integrations.' + integration + '.authMethod'));
|
||||
|
||||
this.notify('Loading...');
|
||||
|
||||
this.createView('content', viewName, {
|
||||
el: '#integration-content',
|
||||
integration: integration,
|
||||
}, function (view) {
|
||||
this.renderHeader();
|
||||
|
||||
view.render();
|
||||
|
||||
this.notify(false);
|
||||
|
||||
$(window).scrollTop(0);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
renderDefaultPage: function () {
|
||||
$('#integration-header').html('').hide();
|
||||
|
||||
if (this.integrationList.length) {
|
||||
var msg = this.translate('selectIntegration', 'messages', 'Integration');
|
||||
} else {
|
||||
var msg = '<p class="lead">' + this.translate('noIntegrations', 'messages', 'Integration') + '</p>';
|
||||
}
|
||||
|
||||
$('#integration-content').html(msg);
|
||||
},
|
||||
|
||||
renderHeader: function () {
|
||||
if (!this.integration) {
|
||||
$('#integration-header').html('');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$('#integration-header').show().html(this.translate(this.integration, 'titles', 'Integration'));
|
||||
},
|
||||
|
||||
@@ -106,5 +118,3 @@ Espo.define('views/admin/integrations/index', 'view', function (Dep) {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -93,9 +93,8 @@ define('views/email/record/row-actions/default', 'views/record/row-actions/defau
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (this.model.get('isUsers')) {
|
||||
if (!this.model.get('isImportant')) {
|
||||
if (!this.model.get('inTrash')) {
|
||||
@@ -117,17 +116,17 @@ define('views/email/record/row-actions/default', 'views/record/row-actions/defau
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.model.get('isUsers') && this.model.get('status') !== 'Draft') {
|
||||
if (!this.model.get('inTrash')) {
|
||||
list.push({
|
||||
action: 'moveToFolder',
|
||||
label: 'Move to Folder',
|
||||
data: {
|
||||
id: this.model.id
|
||||
}
|
||||
});
|
||||
}
|
||||
list.push({
|
||||
action: 'moveToFolder',
|
||||
label: 'Move to Folder',
|
||||
data: {
|
||||
id: this.model.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.acl.delete) {
|
||||
list.push({
|
||||
action: 'quickRemove',
|
||||
@@ -137,6 +136,7 @@ define('views/email/record/row-actions/default', 'views/record/row-actions/defau
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
Espo.define('views/export/modals/export', ['views/modal', 'model'], function (Dep, Model) {
|
||||
define('views/export/modals/export', ['views/modal', 'model'], function (Dep, Model) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
@@ -44,11 +44,11 @@ Espo.define('views/export/modals/export', ['views/modal', 'model'], function (De
|
||||
{
|
||||
name: 'export',
|
||||
label: 'Export',
|
||||
style: 'danger'
|
||||
style: 'danger',
|
||||
},
|
||||
{
|
||||
name: 'cancel',
|
||||
label: 'Cancel'
|
||||
label: 'Cancel',
|
||||
}
|
||||
];
|
||||
|
||||
@@ -73,44 +73,53 @@ Espo.define('views/export/modals/export', ['views/modal', 'model'], function (De
|
||||
this.createView('record', 'views/export/record/record', {
|
||||
scope: this.scope,
|
||||
model: this.model,
|
||||
el: this.getSelector() + ' .record'
|
||||
el: this.getSelector() + ' .record',
|
||||
});
|
||||
},
|
||||
|
||||
actionExport: function () {
|
||||
var data = this.getView('record').fetch();
|
||||
this.model.set(data);
|
||||
if (this.getView('record').validate()) return;
|
||||
|
||||
if (this.getView('record').validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var returnData = {
|
||||
exportAllFields: data.exportAllFields,
|
||||
format: data.format
|
||||
format: data.format,
|
||||
};
|
||||
|
||||
if (!data.exportAllFields) {
|
||||
var attributeList = [];
|
||||
|
||||
data.fieldList.forEach(function (item) {
|
||||
if (item === 'id') {
|
||||
attributeList.push('id');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var type = this.getMetadata().get(['entityDefs', this.scope, 'fields', item, 'type']);
|
||||
if (type) {;
|
||||
|
||||
if (type) {
|
||||
this.getFieldManager().getAttributeList(type, item).forEach(function (attribute) {
|
||||
attributeList.push(attribute);
|
||||
}, this);
|
||||
}
|
||||
|
||||
if (~item.indexOf('_')) {
|
||||
attributeList.push(item);
|
||||
}
|
||||
}, this);
|
||||
|
||||
returnData.attributeList = attributeList;
|
||||
returnData.fieldList = data.fieldList;
|
||||
}
|
||||
|
||||
this.trigger('proceed', returnData);
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
Espo.define('views/export/record/record', 'views/record/base', function (Dep) {
|
||||
define('views/export/record/record', 'views/record/base', function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
@@ -48,10 +48,10 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) {
|
||||
|
||||
fieldList = fieldList.filter(function (item) {
|
||||
var defs = this.getMetadata().get(['entityDefs', this.scope, 'fields', item]) || {};
|
||||
|
||||
if (defs.disabled) return;
|
||||
if (defs.exportDisabled) return;
|
||||
if (defs.type === 'map') return;
|
||||
if (defs.type === 'attachmentMultiple') return;
|
||||
|
||||
return true;
|
||||
}, this);
|
||||
@@ -61,32 +61,43 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) {
|
||||
fieldList.unshift('id');
|
||||
|
||||
var translatedOptions = {};
|
||||
|
||||
fieldList.forEach(function (item) {
|
||||
translatedOptions[item] = this.getLanguage().translate(item, 'fields', this.scope);
|
||||
}, this);
|
||||
|
||||
this.createField('exportAllFields', 'views/fields/bool', {
|
||||
});
|
||||
this.createField('exportAllFields', 'views/fields/bool', {});
|
||||
|
||||
var setFieldList = this.model.get('fieldList') || [];
|
||||
|
||||
setFieldList.forEach(function (item) {
|
||||
if (~fieldList.indexOf(item)) return;
|
||||
if (!~item.indexOf('_')) return;
|
||||
if (~fieldList.indexOf(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!~item.indexOf('_')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var arr = item.split('_');
|
||||
|
||||
fieldList.push(item);
|
||||
|
||||
var foreignScope = this.getMetadata().get(['entityDefs', this.scope, 'links', arr[0], 'entity']);
|
||||
if (!foreignScope) return;
|
||||
translatedOptions[item] = this.getLanguage().translate(arr[0], 'links', this.scope) + '.' + this.getLanguage().translate(arr[1], 'fields', foreignScope);
|
||||
|
||||
if (!foreignScope) {
|
||||
return;
|
||||
}
|
||||
|
||||
translatedOptions[item] = this.getLanguage().translate(arr[0], 'links', this.scope) + '.' +
|
||||
this.getLanguage().translate(arr[1], 'fields', foreignScope);
|
||||
}, this);
|
||||
|
||||
|
||||
this.createField('fieldList', 'views/fields/multi-enum', {
|
||||
required: true,
|
||||
translatedOptions: translatedOptions,
|
||||
options: fieldList
|
||||
options: fieldList,
|
||||
});
|
||||
|
||||
var formatList =
|
||||
@@ -98,6 +109,7 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) {
|
||||
});
|
||||
|
||||
this.controlAllFields();
|
||||
|
||||
this.listenTo(this.model, 'change:exportAllFields', function () {
|
||||
this.controlAllFields();
|
||||
}, this);
|
||||
@@ -109,7 +121,7 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) {
|
||||
} else {
|
||||
this.hideField('fieldList');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -356,11 +356,6 @@ define('views/fields/address', 'views/fields/base', function (Dep) {
|
||||
this.$country.attr('autocomplete', 'espo-country');
|
||||
}
|
||||
|
||||
this.controlStreetTextareaHeight();
|
||||
this.$street.on('input', function (e) {
|
||||
this.controlStreetTextareaHeight();
|
||||
}.bind(this));
|
||||
|
||||
var cityList = this.getConfig().get('addressCityList') || [];
|
||||
if (cityList.length) {
|
||||
this.$city.autocomplete({
|
||||
@@ -394,11 +389,6 @@ define('views/fields/address', 'views/fields/base', function (Dep) {
|
||||
this.$city.attr('autocomplete', 'espo-city');
|
||||
}
|
||||
|
||||
this.controlStreetTextareaHeight();
|
||||
this.$street.on('input', function (e) {
|
||||
this.controlStreetTextareaHeight();
|
||||
}.bind(this));
|
||||
|
||||
var stateList = this.getConfig().get('addressStateList') || [];
|
||||
if (stateList.length) {
|
||||
this.$state.autocomplete({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user