Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d755d3795 | |||
| 9fe61d2342 | |||
| 1f17d79e2d | |||
| 3b0c7f1d5b | |||
| d30edee790 | |||
| 0815cc3b5e | |||
| ddff78756d | |||
| f8c01808d4 | |||
| e073c82992 | |||
| aa82f96993 | |||
| 867a0225f1 | |||
| 4ab636ded8 | |||
| f764375e0e | |||
| 98b20a3e24 | |||
| ca17a5453a | |||
| 641382dc66 | |||
| 8f89fa4597 | |||
| 543eb21a8b | |||
| 9fbc62837f | |||
| d3d710b2b9 | |||
| 4426d81e77 | |||
| 6b3e81e3fd | |||
| dd708778d6 | |||
| 0bc06f3cae | |||
| 0ab8853455 | |||
| a612ffd2d2 | |||
| bc19a6bb5f | |||
| 8cc6a2ddf0 | |||
| c1f0c47cf7 | |||
| 7e71eb0aeb | |||
| 196f787a1f | |||
| b7bec1793c | |||
| b97aa8e3a7 | |||
| 21dcf59f21 | |||
| 326b6c8c53 | |||
| 7ebf0e7bf8 | |||
| 014df8b9d3 | |||
| 6d85055147 | |||
| 077f7d8855 | |||
| 296484360f | |||
| 1071f8a045 | |||
| 962d3a049c | |||
| 161859606a | |||
| a71df0cff6 | |||
| 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 |
+3
-3
@@ -7,7 +7,7 @@
|
||||
/data/tmp/*
|
||||
/build
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
/npm-debug.log
|
||||
/test.php
|
||||
/main.html
|
||||
|
||||
@@ -15,7 +15,7 @@ npm-debug.log
|
||||
!/tests/unit/testData/cache/.data
|
||||
/tests/integration/config.php
|
||||
.phpunit.result.cache
|
||||
composer.phar
|
||||
vendor/
|
||||
/composer.phar
|
||||
/vendor/
|
||||
/custom/Espo/Custom/*
|
||||
/install/config.php
|
||||
@@ -148,14 +148,12 @@ class GoogleMaps
|
||||
|
||||
$GLOBALS['log']->debug("Template helper _googleMapsImage: URL: {$url}.");
|
||||
|
||||
$image = \Espo\Core\TemplateHelpers\GoogleMaps::getImage($url);
|
||||
$image = \Espo\Classes\TemplateHelpers\GoogleMaps::getImage($url);
|
||||
|
||||
if (!$image) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'google_maps_image');
|
||||
file_put_contents($filePath, $image);
|
||||
|
||||
|
||||
@@ -178,6 +178,8 @@ class Auth
|
||||
}
|
||||
|
||||
if ($hasAuthData) {
|
||||
$authResult = null;
|
||||
|
||||
try {
|
||||
$authResult = $this->authentication->login($username, $password, $request, $authenticationMethod);
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ use Espo\Core\{
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class Upgrade implements Command
|
||||
{
|
||||
@@ -78,7 +79,7 @@ class Upgrade implements Command
|
||||
$versionInfo = $this->getVersionInfo($toVersion);
|
||||
|
||||
$nextVersion = $versionInfo->nextVersion ?? null;
|
||||
$lastVersion = $infoData->lastVersion ?? null;
|
||||
$lastVersion = $versionInfo->lastVersion ?? null;
|
||||
|
||||
$packageFile = $this->getPackageFile($params, $versionInfo);
|
||||
|
||||
@@ -127,7 +128,7 @@ class Upgrade implements Command
|
||||
try {
|
||||
$this->runUpgradeProcess($upgradeId, $params);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
catch (Throwable $e) {
|
||||
$errorMessage = $e->getMessage();
|
||||
}
|
||||
|
||||
@@ -137,7 +138,8 @@ class Upgrade implements Command
|
||||
$this->fileManager->unlink($packageFile);
|
||||
}
|
||||
|
||||
if (!empty($errorMessage)) {
|
||||
if (isset($errorMessage)) {
|
||||
$errorMessage = !empty($errorMessage) ? $errorMessage : "Error: An unexpected error occurred.";
|
||||
fwrite(\STDOUT, $errorMessage . "\n");
|
||||
|
||||
return;
|
||||
@@ -287,8 +289,11 @@ class Upgrade implements Command
|
||||
$upgradeManager = $this->getUpgradeManager(true);
|
||||
$upgradeManager->runInstallStep($stepName, ['id' => $upgradeId]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$GLOBALS['log']->error('Upgrade Error: ' . $e->getMessage());
|
||||
} catch (Throwable $e) {
|
||||
try {
|
||||
$GLOBALS['log']->error('Upgrade Error: ' . $e->getMessage());
|
||||
}
|
||||
catch (Throwable $t) {}
|
||||
|
||||
throw new Error($e->getMessage());
|
||||
}
|
||||
@@ -308,7 +313,10 @@ class Upgrade implements Command
|
||||
$shellResult = shell_exec($command);
|
||||
|
||||
if ($shellResult !== 'true') {
|
||||
$GLOBALS['log']->error('Upgrade Error: ' . $shellResult);
|
||||
try {
|
||||
$GLOBALS['log']->error('Upgrade Error: ' . $shellResult);
|
||||
}
|
||||
catch (Throwable $t) {}
|
||||
|
||||
throw new Error($shellResult);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ class UpgradeStep implements Command
|
||||
try {
|
||||
$result = $upgradeManager->runInstallStep($stepName, $params); // throw Exception on error
|
||||
} catch (\Exception $e) {
|
||||
die("Error: " . $e->getMessage() . "\n");
|
||||
die("Error: " . $e->getMessage());
|
||||
}
|
||||
|
||||
if (is_bool($result)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class BaseLanguage implements Loader
|
||||
$this->fileManager,
|
||||
$this->metadata,
|
||||
$this->dataCache,
|
||||
$this->config->get('useCache')
|
||||
$this->config->get('useCache') ?? false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class DefaultLanguage extends BaseLanguage
|
||||
$this->fileManager,
|
||||
$this->metadata,
|
||||
$this->dataCache,
|
||||
$this->config->get('useCache')
|
||||
$this->config->get('useCache') ?? false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class Language implements Loader
|
||||
$this->fileManager,
|
||||
$this->metadata,
|
||||
$this->dataCache,
|
||||
$this->config->get('useCache')
|
||||
$this->config->get('useCache') ?? false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class Metadata implements Loader
|
||||
|
||||
public function load() : MetadataService
|
||||
{
|
||||
$useCache = $this->config->get('useCache');
|
||||
$useCache = $this->config->get('useCache') ?? false;
|
||||
|
||||
return new MetadataService($this->fileManager, $this->dataCache, $useCache);
|
||||
}
|
||||
|
||||
@@ -123,4 +123,22 @@ class DefaultNotificator implements Notificator
|
||||
|
||||
return $this->userIdEnabledMap[$userId];
|
||||
}
|
||||
|
||||
/**
|
||||
* For backward compatibility.
|
||||
* @todo Remove.
|
||||
*/
|
||||
protected function getEntityManager()
|
||||
{
|
||||
return $this->entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* For backward compatibility.
|
||||
* @todo Remove.
|
||||
*/
|
||||
protected function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ class CategoryTree extends Database
|
||||
->select()
|
||||
->from($pathEntityType)
|
||||
->select(['ascendorId', "'" . $entity->id . "'"])
|
||||
->where([
|
||||
'descendorId' => $parentId,
|
||||
])
|
||||
->build();
|
||||
|
||||
$subSelect2 = $em->getQueryBuilder()
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ abstract class Base
|
||||
{
|
||||
if (!isset($this->data['restoreFileList'])) {
|
||||
$backupPath = $this->getPath('backupPath');
|
||||
$this->data['restoreFileList'] = $this->getFileList($backupPath);
|
||||
$this->data['restoreFileList'] = $this->getFileList($backupPath, true);
|
||||
}
|
||||
|
||||
return $this->data['restoreFileList'];
|
||||
@@ -496,7 +496,7 @@ abstract class Base
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getFileList($dirPath)
|
||||
protected function getFileList($dirPath, $skipVendorFileList = false)
|
||||
{
|
||||
$fileList = array();
|
||||
|
||||
@@ -508,10 +508,11 @@ abstract class Base
|
||||
}
|
||||
}
|
||||
|
||||
//vendor file list
|
||||
$vendorFileList = $this->getVendorFileList('copy');
|
||||
if (!empty($vendorFileList)) {
|
||||
$fileList = array_merge($fileList, $vendorFileList);
|
||||
if (!$skipVendorFileList) {
|
||||
$vendorFileList = $this->getVendorFileList('copy');
|
||||
if (!empty($vendorFileList)) {
|
||||
$fileList = array_merge($fileList, $vendorFileList);
|
||||
}
|
||||
}
|
||||
|
||||
return $fileList;
|
||||
@@ -738,7 +739,11 @@ abstract class Base
|
||||
return true;
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$GLOBALS['log']->error('Database rebuild failure, details: '. $e->getMessage() .'.');
|
||||
|
||||
try {
|
||||
$GLOBALS['log']->error('Database rebuild failure, details: '. $e->getMessage() .'.');
|
||||
}
|
||||
catch (Throwable $e) {}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -77,8 +77,6 @@ class Install extends \Espo\Core\Upgrades\Actions\Base
|
||||
|
||||
protected function initPackage(array $data)
|
||||
{
|
||||
$GLOBALS['log']->setLevel('info');
|
||||
|
||||
$processId = $data['id'];
|
||||
|
||||
if (empty($processId)) {
|
||||
@@ -150,19 +148,23 @@ class Install extends \Espo\Core\Upgrades\Actions\Base
|
||||
|
||||
$GLOBALS['log']->info('Installation process ['. $this->getProcessId() .']: Start "copy" step.');
|
||||
|
||||
/* remove files defined in a manifest "deleteBeforeCopy" */
|
||||
$this->deleteFiles('deleteBeforeCopy', true);
|
||||
/* remove files defined in a manifest */
|
||||
if (!$this->deleteFiles('delete', true)) {
|
||||
$this->throwErrorAndRemovePackage('Cannot delete files.');
|
||||
}
|
||||
|
||||
/* copy files from directory "Files" to EspoCRM files */
|
||||
if (!$this->copyFiles()) {
|
||||
$this->throwErrorAndRemovePackage('Cannot copy files.');
|
||||
}
|
||||
|
||||
/* remove files defined in a manifest */
|
||||
$this->deleteFiles('delete', true);
|
||||
if (!$this->deleteFiles('vendor')) {
|
||||
$this->throwErrorAndRemovePackage('Cannot delete vendor files.');
|
||||
}
|
||||
|
||||
$this->deleteFiles('vendor');
|
||||
$this->copyFiles('vendor');
|
||||
if (!$this->copyFiles('vendor')) {
|
||||
$this->throwErrorAndRemovePackage('Cannot copy vendor files.');
|
||||
}
|
||||
|
||||
$GLOBALS['log']->info('Installation process ['. $this->getProcessId() .']: End "copy" step.');
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ class AdminNotificationManager
|
||||
$extension = $this->getEntityManager()->getRepository('Extension')
|
||||
->select(['version'])
|
||||
->where([
|
||||
'name' => $extensionName,
|
||||
'isInstalled' => true,
|
||||
])
|
||||
->order('createdAt', true)
|
||||
|
||||
@@ -66,22 +66,13 @@ class Autoload
|
||||
$this->loader = $loader;
|
||||
}
|
||||
|
||||
public function get($key = null, $returns = null)
|
||||
protected function getData() : array
|
||||
{
|
||||
if (!isset($this->data)) {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
if (!isset($key)) {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
return Util::getValueByKey($this->data, $key, $returns);
|
||||
}
|
||||
|
||||
public function getAll()
|
||||
{
|
||||
return $this->get();
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
protected function init()
|
||||
@@ -94,46 +85,48 @@ class Autoload
|
||||
return;
|
||||
}
|
||||
|
||||
$this->data = $this->unify();
|
||||
$this->data = $this->loadData();
|
||||
|
||||
if ($useCache) {
|
||||
$result = $this->dataCache->store($this->cacheKey, $this->data);
|
||||
}
|
||||
}
|
||||
|
||||
protected function unify()
|
||||
protected function loadData() : array
|
||||
{
|
||||
$data = $this->loadData($this->paths['corePath']);
|
||||
$data = $this->loadDataFromFile($this->paths['corePath']);
|
||||
|
||||
foreach ($this->metadata->getModuleList() as $moduleName) {
|
||||
$modulePath = str_replace('{*}', $moduleName, $this->paths['modulePath']);
|
||||
|
||||
$data = array_merge($data, $this->loadData($modulePath));
|
||||
$data = array_merge_recursive($data, $this->loadDataFromFile($modulePath));
|
||||
}
|
||||
|
||||
$data = array_merge($data, $this->loadData($this->paths['customPath']));
|
||||
$data = array_merge_recursive($data, $this->loadDataFromFile($this->paths['customPath']));
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function loadData($autoloadFile, $returns = [])
|
||||
protected function loadDataFromFile(string $filePath) : array
|
||||
{
|
||||
if (file_exists($autoloadFile)) {
|
||||
$content = $this->fileManager->getContents($autoloadFile);
|
||||
|
||||
$arrayContent = Json::getArrayData($content);
|
||||
|
||||
if (!empty($arrayContent)) {
|
||||
return $this->normalizeData($arrayContent);
|
||||
}
|
||||
|
||||
$GLOBALS['log']->error('Autoload: Empty file or syntax error ['.$autoloadFile.'].');
|
||||
if (!$this->fileManager->isFile($filePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $returns;
|
||||
$content = $this->fileManager->getContents($filePath);
|
||||
|
||||
$arrayContent = Json::getArrayData($content);
|
||||
|
||||
if (empty($arrayContent)) {
|
||||
$GLOBALS['log']->error("Autoload: Empty file or syntax error in '{$filePath}'.");
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->normalizeData($arrayContent);
|
||||
}
|
||||
|
||||
protected function normalizeData(array $data)
|
||||
protected function normalizeData(array $data) : array
|
||||
{
|
||||
$normalizedData = [];
|
||||
|
||||
@@ -161,12 +154,14 @@ class Autoload
|
||||
public function register()
|
||||
{
|
||||
try {
|
||||
$autoloadList = $this->getAll();
|
||||
$data = $this->getData();
|
||||
}
|
||||
catch (Exception $e) {} //bad permissions
|
||||
catch (Exception $e) {} // bad permissions
|
||||
|
||||
if (!empty($autoloadList)) {
|
||||
$this->loader->register($autoloadList);
|
||||
if (empty($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loader->register($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,42 +38,42 @@ class Loader
|
||||
$this->namespaceLoader = $namespaceLoader;
|
||||
}
|
||||
|
||||
public function register(array $autoloadList)
|
||||
public function register(array $data)
|
||||
{
|
||||
/* load "psr-4", "psr-0", "classmap" */
|
||||
$this->namespaceLoader->register($autoloadList);
|
||||
$this->namespaceLoader->register($data);
|
||||
|
||||
/* load "autoloadFileList" */
|
||||
$this->registerAutoloadFileList($autoloadList);
|
||||
$this->registerAutoloadFileList($data);
|
||||
|
||||
/* load "files" */
|
||||
$this->registerFiles($autoloadList);
|
||||
$this->registerFiles($data);
|
||||
}
|
||||
|
||||
protected function registerAutoloadFileList(array $autoloadList)
|
||||
protected function registerAutoloadFileList(array $data)
|
||||
{
|
||||
$keyName = 'autoloadFileList';
|
||||
|
||||
if (!isset($autoloadList[$keyName])) {
|
||||
if (!isset($data[$keyName])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($autoloadList[$keyName] as $filePath) {
|
||||
foreach ($data[$keyName] as $filePath) {
|
||||
if (file_exists($filePath)) {
|
||||
require_once($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function registerFiles(array $autoloadList)
|
||||
protected function registerFiles(array $data)
|
||||
{
|
||||
$keyName = 'files';
|
||||
|
||||
if (!isset($autoloadList[$keyName])) {
|
||||
if (!isset($data[$keyName])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($autoloadList[$keyName] as $id => $filePath) {
|
||||
foreach ($data[$keyName] as $id => $filePath) {
|
||||
if (file_exists($filePath)) {
|
||||
require_once($filePath);
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ class NamespaceLoader
|
||||
$this->classLoader = new ClassLoader();
|
||||
}
|
||||
|
||||
public function register(array $autoloadList)
|
||||
public function register(array $data)
|
||||
{
|
||||
$this->addListToClassLoader($this->classLoader, $autoloadList);
|
||||
$this->addListToClassLoader($this->classLoader, $data);
|
||||
|
||||
$this->classLoader->register(true);
|
||||
}
|
||||
@@ -147,7 +147,7 @@ class NamespaceLoader
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function addListToClassLoader($classLoader, array $list, $skipVendorNamespaces = false)
|
||||
protected function addListToClassLoader(ClassLoader $classLoader, array $list, bool $skipVendorNamespaces = false)
|
||||
{
|
||||
foreach ($this->methodNameMap as $type => $methodName) {
|
||||
if (!isset($list[$type])) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -262,7 +262,7 @@ class Manager
|
||||
}
|
||||
|
||||
if ($result && function_exists('opcache_invalidate')) {
|
||||
@opcache_invalidate($fullPath);
|
||||
opcache_invalidate($fullPath);
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -516,6 +516,12 @@ class Manager
|
||||
return true;
|
||||
}
|
||||
|
||||
$parentDirPath = dirname($fullPath);
|
||||
|
||||
if (!file_exists($parentDirPath)) {
|
||||
$this->mkdir($parentDirPath, $permission);
|
||||
}
|
||||
|
||||
$defaultPermissions = $this->getPermissionUtils()->getRequiredPermissions($fullPath);
|
||||
|
||||
if (!isset($permission)) {
|
||||
@@ -524,11 +530,11 @@ class Manager
|
||||
}
|
||||
|
||||
try {
|
||||
$umask = @umask(0);
|
||||
$result = mkdir($fullPath, $permission, true);
|
||||
$umask = umask(0);
|
||||
$result = mkdir($fullPath, $permission);
|
||||
|
||||
if ($umask) {
|
||||
@umask($umask);
|
||||
umask($umask);
|
||||
}
|
||||
|
||||
if (!empty($defaultPermissions['user'])) {
|
||||
@@ -615,7 +621,7 @@ class Manager
|
||||
$this->getPermissionUtils()->setDefaultPermissions($destFile);
|
||||
|
||||
if (function_exists('opcache_invalidate')) {
|
||||
@opcache_invalidate($destFile);
|
||||
opcache_invalidate($destFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -719,7 +725,7 @@ class Manager
|
||||
|
||||
if (file_exists($filePath) && is_file($filePath)) {
|
||||
if (function_exists('opcache_invalidate')) {
|
||||
@opcache_invalidate($filePath, true);
|
||||
opcache_invalidate($filePath, true);
|
||||
}
|
||||
|
||||
$result &= unlink($filePath);
|
||||
@@ -821,7 +827,7 @@ class Manager
|
||||
|
||||
/**
|
||||
* Remove empty parent directories if they are empty.
|
||||
* @param string $path
|
||||
* @param string $path
|
||||
* @return bool
|
||||
*/
|
||||
protected function removeEmptyDirs($path)
|
||||
@@ -838,29 +844,19 @@ class Manager
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if $dirname is directory.
|
||||
*
|
||||
* @param string $dirname
|
||||
* @param string $basePath
|
||||
*
|
||||
* @return boolean
|
||||
* Check whether a path is a directory.
|
||||
*/
|
||||
public function isDir($dirname, $basePath = null)
|
||||
public function isDir(string $dirPath) : bool
|
||||
{
|
||||
if (!empty($basePath)) {
|
||||
$dirname = $this->concatPaths([$basePath, $dirname]);
|
||||
}
|
||||
|
||||
return is_dir($dirname);
|
||||
return is_dir($dirPath);
|
||||
}
|
||||
|
||||
public function isFile($filename, $basePath = null)
|
||||
/**
|
||||
* Check whether a path is a file.
|
||||
*/
|
||||
public function isFile(string $filePath) : bool
|
||||
{
|
||||
if (!empty($basePath)) {
|
||||
$filename = $this->concatPaths([$basePath, $filename]);
|
||||
}
|
||||
|
||||
return is_file($filename);
|
||||
return is_file($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -891,12 +887,9 @@ class Manager
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if directory is empty.
|
||||
*
|
||||
* @param string $path
|
||||
* @return boolean
|
||||
* Check whether a directory is empty.
|
||||
*/
|
||||
public function isDirEmpty($path)
|
||||
public function isDirEmpty(string $path) : bool
|
||||
{
|
||||
if (is_dir($path)) {
|
||||
$fileList = $this->getFileList($path, true);
|
||||
|
||||
@@ -390,7 +390,7 @@ class Permission
|
||||
protected function chmodReal($filename, $mode)
|
||||
{
|
||||
try {
|
||||
$result = @chmod($filename, $mode);
|
||||
$result = chmod($filename, $mode);
|
||||
} catch (\Exception $e) {
|
||||
$result = false;
|
||||
}
|
||||
@@ -400,7 +400,7 @@ class Permission
|
||||
$this->chgrp($filename, $this->getDefaultGroup(true));
|
||||
|
||||
try {
|
||||
$result = @chmod($filename, $mode);
|
||||
$result = chmod($filename, $mode);
|
||||
} catch (\Exception $e) {
|
||||
throw new Error($e->getMessage());
|
||||
}
|
||||
@@ -412,7 +412,7 @@ class Permission
|
||||
protected function chownReal($path, $user)
|
||||
{
|
||||
try {
|
||||
$result = @chown($path, $user);
|
||||
$result = chown($path, $user);
|
||||
} catch (\Exception $e) {
|
||||
throw new Error($e->getMessage());
|
||||
}
|
||||
@@ -423,7 +423,7 @@ class Permission
|
||||
protected function chgrpReal($path, $group)
|
||||
{
|
||||
try {
|
||||
$result = @chgrp($path, $group);
|
||||
$result = chgrp($path, $group);
|
||||
} catch (\Exception $e) {
|
||||
throw new Error($e->getMessage());
|
||||
}
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -98,7 +98,7 @@ class Lead extends PersonService implements
|
||||
|
||||
$ignoreAttributeList = ['createdAt', 'modifiedAt', 'modifiedById', 'modifiedByName', 'createdById', 'createdByName'];
|
||||
|
||||
$convertFieldsDefs = $this->getMetadata()->get('entityDefs.Lead.convertFields', array());
|
||||
$convertFieldsDefs = $this->getMetadata()->get('entityDefs.Lead.convertFields', []);
|
||||
|
||||
foreach ($entityList as $entityType) {
|
||||
if (!$this->getAcl()->checkScope($entityType, 'edit')) {
|
||||
@@ -141,7 +141,10 @@ class Lead extends PersonService implements
|
||||
$attachment = $lead->get($field);
|
||||
|
||||
if ($attachment) {
|
||||
$attachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
|
||||
$attachment = $this->getEntityManager()
|
||||
->getRepository('Attachment')
|
||||
->getCopiedAttachment($attachment);
|
||||
|
||||
$idAttribute = $field . 'Id';
|
||||
$nameAttribute = $field . 'Name';
|
||||
|
||||
@@ -189,7 +192,11 @@ class Lead extends PersonService implements
|
||||
continue;
|
||||
}
|
||||
|
||||
$leadAttribute = $leadAttributeList[$i];
|
||||
$leadAttribute = $leadAttributeList[$i] ?? null;
|
||||
|
||||
if (!$leadAttribute) {
|
||||
throw new Error("Not compatible fields in 'convertFields' map.");
|
||||
}
|
||||
|
||||
if (!$lead->has($leadAttribute)) {
|
||||
continue;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -37,13 +37,7 @@ interface Builder
|
||||
{
|
||||
/**
|
||||
* Build a query instance.
|
||||
* @todo Uncomment when 7.4 is a min supported PHP version. Need the support of contravariant method parameters.
|
||||
* @todo Uncomment when 7.4 is a min supported PHP version.
|
||||
*/
|
||||
//public function build() : Query;
|
||||
|
||||
/**
|
||||
* Clone an existing query for a further modification and building.
|
||||
* @todo Uncomment when 7.4 is a min supported PHP version. Need the support of contravariant method parameters.
|
||||
*/
|
||||
//public function clone(Query $query) : self;
|
||||
}
|
||||
|
||||
@@ -297,12 +297,6 @@ class RDBRepository extends Repository
|
||||
$additionalColumnsConditions = $params['additionalColumnsConditions'] ?? [];
|
||||
unset($params['additionalColumnsConditions']);
|
||||
|
||||
if ($type === Entity::MANY_MANY && count($additionalColumnsConditions)) {
|
||||
$select = $this->applyRelationAdditionalColumnsConditions(
|
||||
$entity, $relationName, $additionalColumnsConditions, $select
|
||||
);
|
||||
}
|
||||
|
||||
$select = null;
|
||||
|
||||
if ($entityType) {
|
||||
@@ -310,6 +304,12 @@ class RDBRepository extends Repository
|
||||
$select = Select::fromRaw($params);
|
||||
}
|
||||
|
||||
if ($type === Entity::MANY_MANY && count($additionalColumnsConditions)) {
|
||||
$select = $this->applyRelationAdditionalColumnsConditions(
|
||||
$entity, $relationName, $additionalColumnsConditions, $select
|
||||
);
|
||||
}
|
||||
|
||||
return (int) $this->getMapper()->countRelated($entity, $relationName, $select);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"tooltips": {
|
||||
"name": "Give the filter a descriptive name.",
|
||||
"subject": "Use a wildcard *:\n\ntext* - starts with text,\n*text* - contains text,\n*text - ends with text.",
|
||||
"subject": "Use a wildcard *: \n\n * `text*` – starts with text,\n * `*text*` – contains text,\n * `*text` – ends with text.",
|
||||
"bodyContains": "Body of the email contains any of the specified words or phrases.",
|
||||
"from": "Emails being sent from the specified address. Leave empty if not needed. You can use wildcard *.",
|
||||
"to": "Emails being sent to the specified address. Leave empty if not needed. You can use wildcard *.",
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"notModified": "You have not modified the record",
|
||||
"duplicate": "The record you are creating might already exist",
|
||||
"dropToAttach": "Drop to attach",
|
||||
"fieldInvalid": "{field} is invalid",
|
||||
"fieldIsRequired": "{field} is required",
|
||||
"fieldShouldBeEmail": "{field} should be a valid email",
|
||||
"fieldShouldBeFloat": "{field} should be a valid float",
|
||||
|
||||
@@ -144,7 +144,8 @@
|
||||
},
|
||||
"currencyFormat": {
|
||||
"1": "10 USD",
|
||||
"2": "$10"
|
||||
"2": "$10",
|
||||
"3": "10 $"
|
||||
},
|
||||
"personNameFormat": {
|
||||
"firstLast": "First Last",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
"isActive": {
|
||||
"nonAdminReadOnly": true
|
||||
},
|
||||
"emailAddress": {
|
||||
"nonAdminReadOnly": true
|
||||
},
|
||||
"teams": {
|
||||
"nonAdminReadOnly": true
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"userId": {
|
||||
"type": "varchar",
|
||||
"maxLength": 11,
|
||||
"maxLength": 24,
|
||||
"index": 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
|
||||
|
||||
@@ -496,7 +496,7 @@
|
||||
},
|
||||
"currencyFormat": {
|
||||
"type": "enumInt",
|
||||
"options": [1, 2]
|
||||
"options": [1, 2, 3]
|
||||
},
|
||||
"currencyDecimalPlaces": {
|
||||
"type": "int",
|
||||
|
||||
@@ -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}.");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -798,7 +798,9 @@ class InboundEmail extends RecordService 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'));
|
||||
@@ -819,6 +821,7 @@ class InboundEmail extends RecordService implements
|
||||
|
||||
try {
|
||||
$replyEmailTemplateId = $inboundEmail->get('replyEmailTemplateId');
|
||||
|
||||
if ($replyEmailTemplateId) {
|
||||
$entityHash = [];
|
||||
|
||||
@@ -912,7 +915,9 @@ class InboundEmail extends RecordService implements
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
} catch (Exception $e) {
|
||||
$GLOBALS['log']->error("Inbound Email: Auto-reply error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function getSmtpParamsFromInboundEmail(InboundEmailEntity $emailAccount)
|
||||
@@ -988,9 +993,12 @@ class InboundEmail extends RecordService 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) {
|
||||
|
||||
@@ -81,7 +81,7 @@ class Notification extends \Espo\Services\Record implements
|
||||
$collection = $this->entityManager->createCollection();
|
||||
|
||||
$userList = $this->getEntityManager()->getRepository('User')
|
||||
->select(['id'])
|
||||
->select(['id', 'type'])
|
||||
->where([
|
||||
'isActive' => true,
|
||||
'id' => $userIdList,
|
||||
@@ -90,7 +90,10 @@ class Notification extends \Espo\Services\Record implements
|
||||
foreach ($userList as $user) {
|
||||
$userId = $user->id;
|
||||
|
||||
if (!$this->checkUserNoteAccess($user, $note)) continue;
|
||||
if (!$this->checkUserNoteAccess($user, $note)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($note->get('createdById') === $user->id) continue;
|
||||
if ($related && $related->getEntityType() == 'Email' && $related->get('sentById') == $user->id) continue;
|
||||
if ($related && $related->get('createdById') == $user->id) continue;
|
||||
|
||||
@@ -59,6 +59,7 @@ use Espo\Core\{
|
||||
use Espo\Tools\Export\Export as ExportTool;
|
||||
|
||||
use StdClass;
|
||||
use Exception;
|
||||
|
||||
use Espo\Core\Di;
|
||||
|
||||
@@ -705,7 +706,7 @@ class Record implements Crud,
|
||||
}
|
||||
}
|
||||
|
||||
protected function processValidationField(Entity $entity, $field, $data)
|
||||
protected function processValidationField(Entity $entity, string $field, $data)
|
||||
{
|
||||
$fieldType = $this->fieldUtil->getEntityTypeFieldParam($this->entityType, $field, 'type');
|
||||
|
||||
@@ -723,10 +724,11 @@ class Record implements Crud,
|
||||
}
|
||||
|
||||
$skipPropertyName = 'validate' . ucfirst($type) . 'SkipFieldList';
|
||||
|
||||
if (property_exists($this, $skipPropertyName)) {
|
||||
$skipList = $this->$skipPropertyName;
|
||||
|
||||
if (in_array($type, $skipList)) {
|
||||
if (in_array($field, $skipList)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1245,13 +1247,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 +1300,7 @@ class Record implements Crud,
|
||||
}
|
||||
|
||||
$maxSize = 0;
|
||||
|
||||
if ($disableCount) {
|
||||
if (!empty($params['maxSize'])) {
|
||||
$maxSize = $params['maxSize'];
|
||||
@@ -1376,10 +1385,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 +1469,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 +1497,7 @@ class Record implements Crud,
|
||||
return $this->getConfig()->get('maxSelectTextAttributeLengthForList', self::MAX_SELECT_TEXT_ATTRIBUTE_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1487,12 +1510,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 +1541,7 @@ class Record implements Crud,
|
||||
}
|
||||
|
||||
$methodName = 'findLinkedEntities' . ucfirst($link);
|
||||
|
||||
if (method_exists($this, $methodName)) {
|
||||
return $this->$methodName($id, $params);
|
||||
}
|
||||
@@ -1638,35 +1665,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 +1749,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 +1802,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 +1823,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 +1858,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 +1925,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 +1944,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 +1999,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 +2018,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 +2041,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 +2060,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 +2087,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 +2100,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 +2113,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 +2139,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 +2184,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 +2223,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 +2260,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 +2311,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 +2336,7 @@ class Record implements Crud,
|
||||
if ($hasEmailAddress) {
|
||||
$emailAddressToRelateList = [];
|
||||
$emailAddressList = $repository->findRelated($entity, 'emailAddresses');
|
||||
|
||||
foreach ($emailAddressList as $emailAddress) {
|
||||
$emailAddressToRelateList[] = $emailAddress;
|
||||
}
|
||||
@@ -2266,12 +2364,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 +2380,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 +2394,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 +2409,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 +2423,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 +2434,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 +2447,9 @@ class Record implements Crud,
|
||||
} else {
|
||||
$o->primary = $o->phoneNumber === $attributes->phoneNumber;
|
||||
}
|
||||
|
||||
$o->type = $phoneNumber->get('type');
|
||||
|
||||
$phoneNumberData[] = $o;
|
||||
}
|
||||
$attributes->phoneNumberData = $phoneNumberData;
|
||||
@@ -2364,9 +2476,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 +2510,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 +2568,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 +2598,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 +2613,7 @@ class Record implements Crud,
|
||||
|
||||
foreach ($this->duplicatingLinkList as $link) {
|
||||
$linkedList = $repository->findRelated($duplicatingEntity, $link);
|
||||
|
||||
foreach ($linkedList as $linked) {
|
||||
$repository->relate($entity, $link, $linked);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -36,12 +36,23 @@ use Espo\Core\{
|
||||
Utils\Config,
|
||||
Utils\Metadata,
|
||||
Utils\Language,
|
||||
Utils\DateTime,
|
||||
Utils\DateTime as DateTimeUtil,
|
||||
FileStorage\Manager as FileStorageManager,
|
||||
Utils\File\Manager as FileManager,
|
||||
ORM\EntityManager,
|
||||
};
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
|
||||
class Xlsx
|
||||
{
|
||||
protected $config;
|
||||
@@ -56,7 +67,7 @@ class Xlsx
|
||||
Config $config,
|
||||
Metadata $metadata,
|
||||
Language $language,
|
||||
DateTime $dateTime,
|
||||
DateTimeUtil $dateTime,
|
||||
EntityManager $entityManager,
|
||||
FileStorageManager $fileStorageManager,
|
||||
FileManager $fileManager
|
||||
@@ -113,7 +124,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);
|
||||
}
|
||||
@@ -144,18 +157,24 @@ class Xlsx
|
||||
}
|
||||
|
||||
$linkDefs = $this->getMetadata()->get(['entityDefs', $entityType, 'links']);
|
||||
|
||||
if (is_array($linkDefs)) {
|
||||
foreach ($linkDefs as $link => $defs) {
|
||||
if (empty($defs['type'])) continue;
|
||||
if (empty($defs['type'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($defs['type'] === 'belongsToParent') {
|
||||
$linkList[] = $link;
|
||||
} else if ($defs['type'] === 'belongsTo' && !empty($defs['noJoin'])) {
|
||||
}
|
||||
else if ($defs['type'] === 'belongsTo' && !empty($defs['noJoin'])) {
|
||||
if ($this->getMetadata()->get(['entityDefs', $entityType, 'fields', $link])) {
|
||||
$linkList[] = $link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($linkList as $item) {
|
||||
if (in_array($item, $fieldList) && !in_array($item . 'Name', $attributeList)) {
|
||||
$attributeList[] = $item . 'Name';
|
||||
@@ -164,6 +183,7 @@ class Xlsx
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
$type = $this->getMetadata()->get(['entityDefs', $entityType, 'fields', $field, 'type']);
|
||||
|
||||
if ($type === 'currencyConverted') {
|
||||
if (!in_array($field, $attributeList)) {
|
||||
$attributeList[] = $field;
|
||||
@@ -178,43 +198,49 @@ class Xlsx
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$phpExcel = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||
$phpExcel = new Spreadsheet();
|
||||
|
||||
$sheet = $phpExcel->setActiveSheetIndex(0);
|
||||
|
||||
if (isset($params['exportName'])) {
|
||||
$exportName = $params['exportName'];
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
$exportName = $this->language->translate($entityType, 'scopeNamesPlural');
|
||||
}
|
||||
|
||||
$sheetName = mb_substr($exportName, 0, 30, 'utf-8');
|
||||
$badCharList = ['*', ':', '/', '\\', '?', '[', ']'];
|
||||
|
||||
foreach ($badCharList as $badChar) {
|
||||
$sheetName = str_replace($badCharList, ' ', $sheetName);
|
||||
}
|
||||
|
||||
$sheetName = str_replace('\'', '', $sheetName);
|
||||
|
||||
$sheet->setTitle($sheetName);
|
||||
|
||||
$fieldList = $params['fieldList'];
|
||||
|
||||
$titleStyle = array(
|
||||
'font' => array(
|
||||
$titleStyle = [
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'size' => 12
|
||||
)
|
||||
);
|
||||
$dateStyle = array(
|
||||
'font' => array(
|
||||
'size' => 12
|
||||
)
|
||||
);
|
||||
'size' => 12,
|
||||
],
|
||||
];
|
||||
|
||||
$now = new \DateTime();
|
||||
$now->setTimezone(new \DateTimeZone($this->config->get('timeZone', 'UTC')));
|
||||
$dateStyle = [
|
||||
'font' => [
|
||||
'size' => 12,
|
||||
],
|
||||
];
|
||||
|
||||
$now = new DateTime();
|
||||
|
||||
$now->setTimezone(new DateTimeZone($this->config->get('timeZone', 'UTC')));
|
||||
|
||||
$sheet->setCellValue('A1', $exportName);
|
||||
$sheet->setCellValue('B1', \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime($now->format('Y-m-d H:i:s'))));
|
||||
$sheet->setCellValue('B1', SharedDate::PHPToExcel(strtotime($now->format('Y-m-d H:i:s'))));
|
||||
|
||||
$sheet->getStyle('A1')->applyFromArray($titleStyle);
|
||||
$sheet->getStyle('B1')->applyFromArray($dateStyle);
|
||||
@@ -227,6 +253,7 @@ class Xlsx
|
||||
foreach ($azRangeCopied as $i => $char1) {
|
||||
foreach ($azRangeCopied as $j => $char2) {
|
||||
$azRange[] = $char1 . $char2;
|
||||
|
||||
if ($i * count($azRangeCopied) + $j === count($fieldList)) {
|
||||
break 2;
|
||||
}
|
||||
@@ -249,39 +276,45 @@ class Xlsx
|
||||
}
|
||||
|
||||
$label = $name;
|
||||
|
||||
if (strpos($name, '_') !== false) {
|
||||
list($linkName, $foreignField) = explode('_', $name);
|
||||
$foreignScope = $this->metadata->get(['entityDefs', $entityType, 'links', $linkName, 'entity']);
|
||||
if ($foreignScope) {
|
||||
$label = $this->language->translate($linkName, 'links', $entityType) . '.' . $this->language->translate($foreignField, 'fields', $foreignScope);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
$label = $this->language->translate($name, 'fields', $entityType);
|
||||
}
|
||||
|
||||
$sheet->setCellValue($col . $rowNumber, $label);
|
||||
|
||||
$sheet->getColumnDimension($col)->setAutoSize(true);
|
||||
|
||||
if (in_array($defs['type'], ['phone', 'email', 'url', 'link', 'linkParent'])) {
|
||||
$linkColList[] = $col;
|
||||
} else if ($name == 'name') {
|
||||
}
|
||||
else if ($name == 'name') {
|
||||
$linkColList[] = $col;
|
||||
}
|
||||
|
||||
$lastIndex = $i;
|
||||
}
|
||||
|
||||
$col = $azRange[$i];
|
||||
|
||||
$headerStyle = array(
|
||||
'font' => array(
|
||||
$headerStyle = [
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'size' => 12
|
||||
)
|
||||
);
|
||||
'size' => 12,
|
||||
]
|
||||
];
|
||||
|
||||
$sheet->getStyle("A$rowNumber:$col$rowNumber")->applyFromArray($headerStyle);
|
||||
$sheet->setAutoFilter("A$rowNumber:$col$rowNumber");
|
||||
|
||||
$typesCache = array();
|
||||
$typesCache = [];
|
||||
|
||||
$rowNumber++;
|
||||
|
||||
@@ -295,18 +328,28 @@ class Xlsx
|
||||
|
||||
if ($dataFp) {
|
||||
$line = fgets($dataFp);
|
||||
if ($line === false) break;
|
||||
|
||||
if ($line === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$row = unserialize(base64_decode($line));
|
||||
} else {
|
||||
if ($lineIndex >= $lineCount) break;
|
||||
}
|
||||
else {
|
||||
if ($lineIndex >= $lineCount) {
|
||||
break;
|
||||
}
|
||||
|
||||
$row = $dataList[$lineIndex];
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
|
||||
foreach ($fieldList as $i => $name) {
|
||||
$col = $azRange[$i];
|
||||
|
||||
$defs = $this->metadata->get(['entityDefs', $entityType, 'fields', $name]);
|
||||
|
||||
if (!$defs) {
|
||||
$defs = array();
|
||||
$defs['type'] = 'base';
|
||||
@@ -315,17 +358,22 @@ class Xlsx
|
||||
$type = $defs['type'];
|
||||
$foreignField = $name;
|
||||
$linkName = null;
|
||||
|
||||
if (strpos($name, '_') !== false) {
|
||||
list($linkName, $foreignField) = explode('_', $name);
|
||||
|
||||
$foreignScope = $this->metadata->get(['entityDefs', $entityType, 'links', $linkName, 'entity']);
|
||||
|
||||
if ($foreignScope) {
|
||||
$type = $this->metadata->get(['entityDefs', $foreignScope, 'fields', $foreignField, 'type'], $type);
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === 'foreign') {
|
||||
$linkName = $this->metadata->get(['entityDefs', $entityType, 'fields', $name, 'link']);
|
||||
$foreignField = $this->metadata->get(['entityDefs', $entityType, 'fields', $name, 'field']);
|
||||
$foreignScope = $this->metadata->get(['entityDefs', $entityType, 'links', $linkName, 'entity']);
|
||||
|
||||
if ($foreignScope) {
|
||||
$type = $this->metadata->get(['entityDefs', $foreignScope, 'fields', $foreignField, 'type'], $type);
|
||||
}
|
||||
@@ -333,40 +381,50 @@ class Xlsx
|
||||
$typesCache[$name] = $type;
|
||||
|
||||
$link = null;
|
||||
|
||||
if ($type == 'link') {
|
||||
if (array_key_exists($name.'Name', $row)) {
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name.'Name']);
|
||||
}
|
||||
} else if ($type == 'linkParent') {
|
||||
}
|
||||
else if ($type == 'linkParent') {
|
||||
if (array_key_exists($name.'Name', $row)) {
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name.'Name']);
|
||||
}
|
||||
} else if ($type == 'int') {
|
||||
}
|
||||
else if ($type == 'int') {
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name] ?: 0);
|
||||
} else if ($type == 'float') {
|
||||
}
|
||||
else if ($type == 'float') {
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name] ?: 0);
|
||||
} else if ($type == 'currency') {
|
||||
}
|
||||
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') {
|
||||
}
|
||||
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] : '');
|
||||
}
|
||||
} else if ($type == 'personName') {
|
||||
}
|
||||
else if ($type == 'personName') {
|
||||
if (!empty($row['name'])) {
|
||||
$sheet->setCellValue("$col$rowNumber", $row['name']);
|
||||
} else {
|
||||
@@ -382,41 +440,51 @@ class Xlsx
|
||||
}
|
||||
$sheet->setCellValue("$col$rowNumber", $personName);
|
||||
}
|
||||
} else if ($type == 'date') {
|
||||
}
|
||||
else if ($type == 'date') {
|
||||
if (isset($row[$name])) {
|
||||
$sheet->setCellValue("$col$rowNumber", \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime($row[$name])));
|
||||
$sheet->setCellValue("$col$rowNumber", SharedDate::PHPToExcel(strtotime($row[$name])));
|
||||
}
|
||||
} else if ($type == 'datetime' || $type == 'datetimeOptional') {
|
||||
}
|
||||
else if ($type == 'datetime' || $type == 'datetimeOptional') {
|
||||
$value = null;
|
||||
|
||||
if ($type == 'datetimeOptional') {
|
||||
if (isset($row[$name . 'Date']) && $row[$name . 'Date']) {
|
||||
$value = $row[$name . 'Date'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$value) {
|
||||
if (isset($row[$name])) {
|
||||
$value = $row[$name];
|
||||
}
|
||||
}
|
||||
|
||||
if ($value && strlen($value) > 11) {
|
||||
try {
|
||||
$timeZone = $this->config->get('timeZone');
|
||||
$dt = new \DateTime($value);
|
||||
$dt->setTimezone(new \DateTimeZone($timeZone));
|
||||
|
||||
$dt = new DateTime($value);
|
||||
$dt->setTimezone(new DateTimeZone($timeZone));
|
||||
|
||||
$value = $dt->format($this->dateTime->getInternalDateTimeFormat());
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$value = '';
|
||||
}
|
||||
}
|
||||
|
||||
if ($value) {
|
||||
$sheet->setCellValue("$col$rowNumber", \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime($value)));
|
||||
$sheet->setCellValue("$col$rowNumber", SharedDate::PHPToExcel(strtotime($value)));
|
||||
}
|
||||
} else if ($type == 'image') {
|
||||
}
|
||||
else if ($type == 'image') {
|
||||
if (isset($row[$name . 'Id']) && $row[$name . 'Id']) {
|
||||
$attachment = $this->getEntityManager()->getEntity('Attachment', $row[$name . 'Id']);
|
||||
|
||||
if ($attachment) {
|
||||
$objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
|
||||
$objDrawing = new Drawing();
|
||||
$filePath = $this->fileStorageManager->getLocalFilePath($attachment);
|
||||
|
||||
if ($filePath && file_exists($filePath)) {
|
||||
@@ -429,66 +497,87 @@ class Xlsx
|
||||
}
|
||||
}
|
||||
|
||||
} else if ($type == 'file') {
|
||||
}
|
||||
else if ($type == 'file') {
|
||||
if (array_key_exists($name.'Name', $row)) {
|
||||
$sheet->setCellValue("$col$rowNumber", $row[$name.'Name']);
|
||||
}
|
||||
} else if ($type == 'enum') {
|
||||
}
|
||||
else if ($type == 'enum') {
|
||||
if (array_key_exists($name, $row)) {
|
||||
if ($linkName) {
|
||||
$value = $this->language->translateOption($row[$name], $foreignField, $foreignScope);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
$value = $this->language->translateOption($row[$name], $name, $entityType);
|
||||
}
|
||||
|
||||
$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) {
|
||||
$relatedName = $relatedId;
|
||||
|
||||
if (property_exists($row[$name . 'Names'], $relatedId)) {
|
||||
$relatedName = $row[$name . 'Names']->$relatedId;
|
||||
}
|
||||
|
||||
$nameList[] = $relatedName;
|
||||
}
|
||||
|
||||
$sheet->setCellValue("$col$rowNumber", implode(', ', $nameList));
|
||||
}
|
||||
} else if ($type == 'address') {
|
||||
}
|
||||
else if ($type == 'address') {
|
||||
$value = '';
|
||||
|
||||
if (!empty($row[$name . 'Street'])) {
|
||||
$value = $value .= $row[$name.'Street'];
|
||||
}
|
||||
|
||||
if (!empty($row[$name.'City']) || !empty($row[$name.'State']) || !empty($row[$name.'PostalCode'])) {
|
||||
if ($value) {
|
||||
$value .= "\n";
|
||||
}
|
||||
|
||||
if (!empty($row[$name.'City'])) {
|
||||
$value .= $row[$name.'City'];
|
||||
|
||||
if (
|
||||
!empty($row[$name.'State']) || !empty($row[$name.'PostalCode'])
|
||||
) {
|
||||
$value .= ', ';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($row[$name.'State'])) {
|
||||
$value .= $row[$name.'State'];
|
||||
|
||||
if (!empty($row[$name.'PostalCode'])) {
|
||||
$value .= ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($row[$name.'PostalCode'])) {
|
||||
$value .= $row[$name.'PostalCode'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($row[$name.'Country'])) {
|
||||
if ($value) {
|
||||
$value .= "\n";
|
||||
}
|
||||
|
||||
$value .= $row[$name.'Country'];
|
||||
}
|
||||
|
||||
$sheet->setCellValue("$col$rowNumber", $value);
|
||||
} else if ($type == 'duration') {
|
||||
}
|
||||
else if ($type == 'duration') {
|
||||
if (!empty($row[$name])) {
|
||||
$seconds = intval($row[$name]);
|
||||
|
||||
@@ -499,35 +588,58 @@ class Xlsx
|
||||
$minutes = intval(floor($seconds / 60));
|
||||
|
||||
$value = '';
|
||||
|
||||
if ($days) {
|
||||
$value .= (string) $days . $this->language->translate('d', 'durationUnits');
|
||||
|
||||
if ($minutes || $hours) {
|
||||
$value .= ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if ($hours) {
|
||||
$value .= (string) $hours . $this->language->translate('h', 'durationUnits');
|
||||
|
||||
if ($minutes) {
|
||||
$value .= ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if ($minutes) {
|
||||
$value .= (string) $minutes . $this->language->translate('m', 'durationUnits');
|
||||
}
|
||||
|
||||
$sheet->setCellValue("$col$rowNumber", $value);
|
||||
}
|
||||
} else if ($type == 'multiEnum' || $type == 'array') {
|
||||
}
|
||||
else if ($type == 'multiEnum' || $type == 'array') {
|
||||
if (!empty($row[$name])) {
|
||||
$array = json_decode($row[$name]);
|
||||
if (is_array($array)) {
|
||||
$value = implode(', ', $array);
|
||||
$sheet->setCellValue("$col$rowNumber", $value, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
|
||||
|
||||
if (!is_array($array)) {
|
||||
$array = [];
|
||||
}
|
||||
|
||||
foreach ($array as $i => $item) {
|
||||
if ($linkName) {
|
||||
$itemValue = $this->language
|
||||
->translateOption($item, $foreignField, $foreignScope);
|
||||
}
|
||||
else {
|
||||
$itemValue = $this->language
|
||||
->translateOption($item, $name, $entityType);
|
||||
}
|
||||
$array[$i] = $itemValue;
|
||||
}
|
||||
|
||||
$value = implode(', ', $array);
|
||||
|
||||
$sheet->setCellValue("$col$rowNumber", $value, DataType::TYPE_STRING);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if (array_key_exists($name, $row)) {
|
||||
$sheet->setCellValueExplicit("$col$rowNumber", $row[$name], \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
|
||||
$sheet->setCellValueExplicit("$col$rowNumber", $row[$name], DataType::TYPE_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,8 +647,10 @@ class Xlsx
|
||||
|
||||
$foreignLink = null;
|
||||
$isForeign = false;
|
||||
|
||||
if (strpos($name, '_')) {
|
||||
$isForeign = true;
|
||||
|
||||
list($foreignLink, $foreignField) = explode('_', $name);
|
||||
}
|
||||
|
||||
@@ -544,59 +658,80 @@ class Xlsx
|
||||
if (array_key_exists('id', $row)) {
|
||||
$link = $this->getConfig()->getSiteUrl() . "/#".$entityType . "/view/" . $row['id'];
|
||||
}
|
||||
} else if ($type == 'url') {
|
||||
}
|
||||
else if ($type == 'url') {
|
||||
if (array_key_exists($name, $row) && filter_var($row[$name], FILTER_VALIDATE_URL)) {
|
||||
$link = $row[$name];
|
||||
}
|
||||
} else if ($type == 'link') {
|
||||
}
|
||||
else if ($type == 'link') {
|
||||
if (array_key_exists($name.'Id', $row)) {
|
||||
$foreignEntity = null;
|
||||
|
||||
if (!$isForeign) {
|
||||
$foreignEntity = $this->getMetadata()->get(['entityDefs', $entityType, 'links', $name, 'entity']);
|
||||
} else {
|
||||
$foreignEntity1 = $this->getMetadata()->get(['entityDefs', $entityType, 'links', $foreignLink, 'entity']);
|
||||
$foreignEntity = $this->getMetadata()->get(['entityDefs', $foreignEntity1, 'links', $foreignField, 'entity']);
|
||||
$foreignEntity = $this->getMetadata()->get(
|
||||
['entityDefs', $entityType, 'links', $name, 'entity']
|
||||
);
|
||||
}
|
||||
else {
|
||||
$foreignEntity1 = $this->getMetadata()->get(
|
||||
['entityDefs', $entityType, 'links', $foreignLink, 'entity']
|
||||
);
|
||||
|
||||
$foreignEntity = $this->getMetadata()->get(
|
||||
['entityDefs', $foreignEntity1, 'links', $foreignField, 'entity']
|
||||
);
|
||||
}
|
||||
|
||||
if ($foreignEntity) {
|
||||
$link = $this->getConfig()->getSiteUrl() . "/#" . $foreignEntity. "/view/". $row[$name.'Id'];
|
||||
}
|
||||
}
|
||||
} else if ($type == 'file') {
|
||||
}
|
||||
else if ($type == 'file') {
|
||||
if (array_key_exists($name.'Id', $row)) {
|
||||
$link = $this->getConfig()->getSiteUrl() . "/?entryPoint=download&id=" . $row[$name.'Id'];
|
||||
}
|
||||
} else if ($type == 'linkParent') {
|
||||
}
|
||||
else if ($type == 'linkParent') {
|
||||
if (array_key_exists($name.'Id', $row) && array_key_exists($name.'Type', $row)) {
|
||||
$link = $this->getConfig()->getSiteUrl() . "/#".$row[$name.'Type']."/view/". $row[$name.'Id'];
|
||||
}
|
||||
} else if ($type == 'phone') {
|
||||
}
|
||||
else if ($type == 'phone') {
|
||||
if (array_key_exists($name, $row)) {
|
||||
$link = "tel:".$row[$name];
|
||||
}
|
||||
} else if ($type == 'email' && array_key_exists($name, $row)) {
|
||||
}
|
||||
|
||||
else if ($type == 'email' && array_key_exists($name, $row)) {
|
||||
if (array_key_exists($name, $row)) {
|
||||
$link = "mailto:".$row[$name];
|
||||
}
|
||||
}
|
||||
|
||||
if ($link) {
|
||||
$sheet->getCell("$col$rowNumber")->getHyperlink()->setUrl($link);
|
||||
$sheet->getCell("$col$rowNumber")->getHyperlink()->setTooltip($link);
|
||||
}
|
||||
}
|
||||
|
||||
$rowNumber++;
|
||||
}
|
||||
|
||||
$sheet->getStyle("A2:A$rowNumber")
|
||||
->getNumberFormat()
|
||||
->setFormatCode(\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_TEXT);
|
||||
->setFormatCode(NumberFormat::FORMAT_TEXT);
|
||||
|
||||
$startingRowNumber = 4;
|
||||
|
||||
foreach ($fieldList as $i => $name) {
|
||||
$col = $azRange[$i];
|
||||
|
||||
if (!array_key_exists($name, $typesCache)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$type = $typesCache[$name];
|
||||
|
||||
switch ($type) {
|
||||
@@ -604,31 +739,37 @@ class Xlsx
|
||||
case 'currencyConverted': {
|
||||
|
||||
} break;
|
||||
|
||||
case 'int': {
|
||||
$sheet->getStyle($col.$startingRowNumber.':'.$col.$rowNumber)
|
||||
->getNumberFormat()
|
||||
->setFormatCode('0');
|
||||
} break;
|
||||
|
||||
case 'float': {
|
||||
$sheet->getStyle($col.$startingRowNumber.':'.$col.$rowNumber)
|
||||
->getNumberFormat()
|
||||
->setFormatCode(\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1);
|
||||
->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1);
|
||||
} break;
|
||||
|
||||
case 'date': {
|
||||
$sheet->getStyle($col.$startingRowNumber.':'.$col.$rowNumber)
|
||||
->getNumberFormat()
|
||||
->setFormatCode($this->dateTime->getDateFormat());
|
||||
} break;
|
||||
|
||||
case 'datetime': {
|
||||
$sheet->getStyle($col.$startingRowNumber.':'.$col.$rowNumber)
|
||||
->getNumberFormat()
|
||||
->setFormatCode($this->dateTime->getDateTimeFormat());
|
||||
} break;
|
||||
|
||||
case 'datetimeOptional': {
|
||||
$sheet->getStyle($col.$startingRowNumber.':'.$col.$rowNumber)
|
||||
->getNumberFormat()
|
||||
->setFormatCode($this->dateTime->getDateTimeFormat());
|
||||
} break;
|
||||
|
||||
default: {
|
||||
$sheet->getStyle($col.$startingRowNumber.':'.$col.$rowNumber)
|
||||
->getNumberFormat()
|
||||
@@ -640,25 +781,42 @@ class Xlsx
|
||||
$linkStyle = [
|
||||
'font' => [
|
||||
'color' => ['rgb' => '345b7c'],
|
||||
'underline' => 'single'
|
||||
'underline' => 'single',
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($linkColList as $linkColumn) {
|
||||
$sheet->getStyle($linkColumn.$startingRowNumber.':'.$linkColumn.$rowNumber)->applyFromArray($linkStyle);
|
||||
}
|
||||
|
||||
$objWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpExcel, 'Xlsx');
|
||||
$objWriter = IOFactory::createWriter($phpExcel, 'Xlsx');
|
||||
|
||||
if (!$this->fileManager->isDir('data/cache/')) {
|
||||
$this->fileManager->mkdir('data/cache/');
|
||||
}
|
||||
|
||||
$tempFileName = 'data/cache/' . 'export_' . substr(md5(rand()), 0, 7);
|
||||
|
||||
$objWriter->save($tempFileName);
|
||||
|
||||
$fp = fopen($tempFileName, 'r');
|
||||
$xlsx = stream_get_contents($fp);
|
||||
|
||||
$this->fileManager->unlink($tempFileName);
|
||||
|
||||
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 = [];
|
||||
|
||||
|
||||
@@ -201,6 +201,8 @@ class LeadCapture
|
||||
$lead = $this->getLeadWithPopulatedData($leadCapture, $data);
|
||||
}
|
||||
|
||||
$campaign = null;
|
||||
|
||||
$campaingService = $this->serviceFactory->create('Campaign');
|
||||
|
||||
if ($leadCapture->get('campaignId')) {
|
||||
|
||||
+242
-228
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -219,7 +219,13 @@ define('views/admin/layouts/index', 'view', function (Dep) {
|
||||
},
|
||||
|
||||
navigate: function (scope, type) {
|
||||
this.getRouter().navigate('#Admin/layouts/scope=' + scope + '&type=' + type, {trigger: false});
|
||||
var url = '#Admin/layouts/scope=' + scope + '&type=' + type;
|
||||
|
||||
if (this.em) {
|
||||
url += '&em=true';
|
||||
}
|
||||
|
||||
this.getRouter().navigate(url, {trigger: false});
|
||||
},
|
||||
|
||||
renderDefaultPage: function () {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -257,6 +257,8 @@ define('views/fields/base', 'view', function (Dep) {
|
||||
this.events = {};
|
||||
}
|
||||
|
||||
this.validations = Espo.Utils.clone(this.validations);
|
||||
|
||||
this.defs = this.options.defs || {};
|
||||
this.name = this.options.name || this.defs.name;
|
||||
this.params = this.options.params || this.defs.params || {};
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
Espo.define('views/fields/currency', 'views/fields/float', function (Dep) {
|
||||
define('views/fields/currency', 'views/fields/float', function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
@@ -40,12 +40,16 @@ Espo.define('views/fields/currency', 'views/fields/float', function (Dep) {
|
||||
|
||||
detailTemplate2: 'fields/currency/detail-2',
|
||||
|
||||
detailTemplate3: 'fields/currency/detail-3',
|
||||
|
||||
listTemplate: 'fields/currency/list',
|
||||
|
||||
listTemplate1: 'fields/currency/list-1',
|
||||
|
||||
listTemplate2: 'fields/currency/list-2',
|
||||
|
||||
listTemplate3: 'fields/currency/list-3',
|
||||
|
||||
detailTemplateNoCurrency: 'fields/currency/detail-no-currency',
|
||||
|
||||
maxDecimalPlaces: 3,
|
||||
|
||||
@@ -44,6 +44,8 @@ define('views/fields/enum', ['views/fields/base', 'lib!Selectize'], function (De
|
||||
|
||||
translatedOptions: null,
|
||||
|
||||
fetchEmptyValueAsNull: false,
|
||||
|
||||
searchTypeList: ['anyOf', 'noneOf', 'isEmpty', 'isNotEmpty'],
|
||||
|
||||
data: function () {
|
||||
|
||||
@@ -253,10 +253,6 @@ define('views/fields/link-parent', 'views/fields/base', function (Dep) {
|
||||
e.currentTarget.value = this.model.get(this.nameName);
|
||||
}
|
||||
}.bind(this));
|
||||
} else if (this.mode == 'search') {
|
||||
this.$elementName.on('blur', function (e) {
|
||||
e.currentTarget.value = '';
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
if (!this.autocompleteDisabled) {
|
||||
|
||||
@@ -260,10 +260,6 @@ define('views/fields/link', 'views/fields/base', function (Dep) {
|
||||
e.currentTarget.value = this.model.get(this.nameName);
|
||||
}
|
||||
}.bind(this));
|
||||
} else if (this.mode == 'search') {
|
||||
this.$elementName.on('blur', function (e) {
|
||||
e.currentTarget.value = '';
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
var $elementName = this.$elementName;
|
||||
|
||||
@@ -44,6 +44,8 @@ define('views/fields/wysiwyg', ['views/fields/text', 'lib!Summernote'], function
|
||||
|
||||
seeMoreDisabled: true,
|
||||
|
||||
fetchEmptyValueAsNull: false,
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
|
||||
@@ -532,6 +534,12 @@ define('views/fields/wysiwyg', ['views/fields/text', 'lib!Summernote'], function
|
||||
data[this.name] = code;
|
||||
} else {
|
||||
data[this.name] = this.$element.val();
|
||||
|
||||
if (this.fetchEmptyValueAsNull) {
|
||||
if (!data[this.name]) {
|
||||
data[this.name] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.model.has('isHtml') && this.hasBodyPlainField) {
|
||||
|
||||
@@ -388,13 +388,13 @@ define('views/import/step2', 'view', function (Dep) {
|
||||
},
|
||||
|
||||
disableButtons: function () {
|
||||
this.$el.find('button[data-action="next"]').addClass('disabled');
|
||||
this.$el.find('button[data-action="back"]').addClass('disabled');
|
||||
this.$el.find('button[data-action="next"]').addClass('disabled').attr('disabled', 'disabled');
|
||||
this.$el.find('button[data-action="back"]').addClass('disabled').attr('disabled', 'disabled');
|
||||
},
|
||||
|
||||
enableButtons: function () {
|
||||
this.$el.find('button[data-action="next"]').removeClass('disabled');
|
||||
this.$el.find('button[data-action="back"]').removeClass('disabled');
|
||||
this.$el.find('button[data-action="next"]').removeClass('disabled').removeAttr('disabled');
|
||||
this.$el.find('button[data-action="back"]').removeClass('disabled').removeAttr('disabled');
|
||||
},
|
||||
|
||||
fetch: function (skipValidation) {
|
||||
|
||||
@@ -58,9 +58,11 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
|
||||
data: function () {
|
||||
var data = {};
|
||||
|
||||
data.categoriesDisabled = this.categoriesDisabled;
|
||||
data.isExpanded = this.isExpanded;
|
||||
data.hasExpandedToggler = this.hasExpandedToggler;
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -101,7 +103,10 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
this.applyCategoryToCollection();
|
||||
|
||||
this.listenTo(this.collection, 'sync', function (c, d, o) {
|
||||
if (o && o.openCategory) return;
|
||||
if (o && o.openCategory) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.controlListVisibility();
|
||||
}, this);
|
||||
},
|
||||
@@ -120,6 +125,7 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
this.openCategory(params.categoryId, params.categoryName);
|
||||
}
|
||||
}
|
||||
|
||||
this.selectCurrentCategory();
|
||||
}
|
||||
},
|
||||
@@ -129,13 +135,14 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
for (var i = 0; i < this.collection.where.length; i++) {
|
||||
if (this.collection.where[i].type === 'textFilter') {
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.collection.data && this.collection.data.textFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -161,9 +168,11 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
} else {
|
||||
this.controlListVisibility();
|
||||
}
|
||||
|
||||
if (!this.categoriesDisabled && !this.hasView('categories')) {
|
||||
this.loadCategories();
|
||||
}
|
||||
|
||||
if (!this.isExpanded && !this.hasView('nestedCategories')) {
|
||||
this.loadNestedCategories();
|
||||
}
|
||||
@@ -171,6 +180,7 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
|
||||
actionExpand: function () {
|
||||
this.isExpanded = true;
|
||||
|
||||
this.setIsExpandedStoredValue(true);
|
||||
|
||||
this.applyCategoryToCollection();
|
||||
@@ -307,12 +317,15 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
this.listenToOnce(collection, 'sync', function () {
|
||||
callback.call(this, collection);
|
||||
}, this);
|
||||
|
||||
collection.fetch();
|
||||
}, this);
|
||||
},
|
||||
|
||||
applyCategoryToNestedCategoriesCollection: function () {
|
||||
if (!this.nestedCategoriesCollection) return;
|
||||
if (!this.nestedCategoriesCollection) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.nestedCategoriesCollection.where = null;
|
||||
|
||||
@@ -335,6 +348,7 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
collection.maxDepth = 1;
|
||||
|
||||
collection.data.checkIfEmpty = true;
|
||||
|
||||
if (!this.getAcl().checkScope(this.scope, 'create')) {
|
||||
collection.data.onlyNotEmpty = true;
|
||||
}
|
||||
@@ -344,6 +358,7 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
collection.fetch().then(function () {
|
||||
this.controlListVisibility();
|
||||
this.controlNestedCategoriesVisibility();
|
||||
|
||||
callback.call(this, collection);
|
||||
}.bind(this));
|
||||
}, this);
|
||||
@@ -353,7 +368,7 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
this.getNestedCategoriesCollection(function (collection) {
|
||||
this.createView('nestedCategories', 'views/record/list-nested-categories', {
|
||||
collection: collection,
|
||||
el: this.options.el + ' .nested-categories-container'
|
||||
el: this.options.el + ' .nested-categories-container',
|
||||
}, function (view) {
|
||||
view.render();
|
||||
});
|
||||
@@ -378,18 +393,22 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
if (this.currentCategoryId) {
|
||||
view.setSelected(this.currentCategoryId);
|
||||
}
|
||||
|
||||
view.render();
|
||||
|
||||
this.listenTo(view, 'select', function (model) {
|
||||
if (!this.isExpanded) {
|
||||
var id = null;
|
||||
var name = null;
|
||||
|
||||
if (model && model.id) {
|
||||
id = model.id;
|
||||
name = model.get('name');
|
||||
}
|
||||
|
||||
this.openCategory(id, name);
|
||||
this.navigateToCurrentCategory();
|
||||
|
||||
return;
|
||||
}
|
||||
this.currentCategoryId = null;
|
||||
@@ -409,6 +428,7 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
this.listenToOnce(this.collection, 'sync', function () {
|
||||
this.notify(false);
|
||||
}, this);
|
||||
|
||||
this.collection.fetch();
|
||||
|
||||
}, this);
|
||||
@@ -417,9 +437,7 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
}, this);
|
||||
},
|
||||
|
||||
|
||||
applyCategoryToCollection: function () {
|
||||
|
||||
this.collection.whereFunction = function () {
|
||||
var filter;
|
||||
var isExpanded = this.isExpanded;
|
||||
@@ -457,14 +475,14 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
filter = {
|
||||
field: this.categoryField,
|
||||
type: this.categoryFilterType,
|
||||
value: this.currentCategoryId
|
||||
value: this.currentCategoryId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
return [filter];
|
||||
}
|
||||
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
@@ -479,19 +497,26 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
if (this.currentCategoryId) {
|
||||
var names = {};
|
||||
names[this.currentCategoryId] = this.currentCategoryName;
|
||||
|
||||
var data = {};
|
||||
|
||||
var idsAttribute = this.categoryField + 'Ids';
|
||||
var namesAttribute = this.categoryField + 'Names';
|
||||
|
||||
data[idsAttribute] = [this.currentCategoryId],
|
||||
data[namesAttribute] = names;
|
||||
|
||||
return data;
|
||||
}
|
||||
} else {
|
||||
var idAttribute = this.categoryField + 'Id';
|
||||
var nameAttribute = this.categoryField + 'Name';
|
||||
|
||||
var data = {};
|
||||
|
||||
data[idAttribute] = this.currentCategoryId;
|
||||
data[nameAttribute] = this.currentCategoryName;
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
@@ -500,6 +525,5 @@ define('views/list-with-categories', 'views/list', function (Dep) {
|
||||
this.clearView('categories');
|
||||
this.getRouter().navigate('#' + this.categoryScope, {trigger: true});
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,8 +62,12 @@ define('views/modals/compose-email', 'views/modals/edit', function (Dep) {
|
||||
var attributes = this.options.attributes || {};
|
||||
|
||||
require('email-helper', function (EmailHelper) {
|
||||
this.getRouter().confirmLeaveOut = false;
|
||||
|
||||
var emailHelper = new EmailHelper();
|
||||
|
||||
var link = emailHelper.composeMailToLink(attributes, this.getConfig().get('outboundEmailBccAddress'));
|
||||
|
||||
document.location.href = link;
|
||||
}.bind(this));
|
||||
|
||||
@@ -80,7 +84,9 @@ define('views/modals/compose-email', 'views/modals/edit', function (Dep) {
|
||||
},
|
||||
|
||||
createRecordView: function (model, callback) {
|
||||
var viewName = this.getMetadata().get('clientDefs.' + model.name + '.recordViews.compose') || 'views/email/record/compose';
|
||||
var viewName = this.getMetadata().get('clientDefs.' + model.name + '.recordViews.compose') ||
|
||||
'views/email/record/compose';
|
||||
|
||||
var options = {
|
||||
model: model,
|
||||
el: this.containerSelector + ' .edit-container',
|
||||
@@ -93,6 +99,7 @@ define('views/modals/compose-email', 'views/modals/edit', function (Dep) {
|
||||
appendSignature: this.options.appendSignature,
|
||||
exit: function () {}
|
||||
};
|
||||
|
||||
this.createView('edit', viewName, options, callback);
|
||||
},
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ define('views/preferences/fields/signature', 'views/fields/wysiwyg', function (D
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
fetchEmptyValueAsNull: true,
|
||||
|
||||
toolbar: [
|
||||
["style", ["bold", "italic", "underline", "clear"]],
|
||||
["color", ["color"]],
|
||||
|
||||
@@ -246,6 +246,8 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
this.getAcl().check(this.entityType, 'edit')
|
||||
&&
|
||||
!~this.getAcl().getScopeForbiddenFieldList(this.entityType).indexOf('assignedUser')
|
||||
&&
|
||||
!this.getUser().isPortal()
|
||||
) {
|
||||
if (this.model.has('assignedUserId')) {
|
||||
this.dropdownItemList.push({
|
||||
|
||||
@@ -998,7 +998,7 @@ define('views/record/list', 'view', function (Dep) {
|
||||
}, this);
|
||||
|
||||
if (
|
||||
this.getConfig().get('exportDisabled') && !this.getUser().get('isAdmin')
|
||||
this.getConfig().get('exportDisabled') && !this.getUser().isAdmin()
|
||||
||
|
||||
this.getAcl().get('exportPermission') === 'no'
|
||||
||
|
||||
|
||||
@@ -25,14 +25,40 @@
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
Espo.define('views/settings/fields/default-currency', 'views/fields/enum', function (Dep) {
|
||||
|
||||
define('views/settings/fields/default-currency', 'views/fields/enum', function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
|
||||
this.validations.push('existing');
|
||||
},
|
||||
|
||||
setupOptions: function () {
|
||||
this.params.options = Espo.Utils.clone(this.getConfig().get('currencyList') || []);
|
||||
}
|
||||
},
|
||||
|
||||
validateExisting: function () {
|
||||
var currencyList = this.model.get('currencyList');
|
||||
|
||||
if (!currencyList) {
|
||||
return;
|
||||
}
|
||||
|
||||
var value = this.model.get(this.name);
|
||||
|
||||
if (~currencyList.indexOf(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = this.translate('fieldInvalid', 'messages').replace('{field}', this.getLabelText());
|
||||
|
||||
this.showValidationMessage(msg);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -126,7 +126,9 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
},
|
||||
|
||||
setupNonAdminFieldsAccess: function () {
|
||||
if (this.getUser().isAdmin()) return;
|
||||
if (this.getUser().isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nonAdminReadOnlyFieldList = [
|
||||
'userName',
|
||||
@@ -138,9 +140,30 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
'portalRoles',
|
||||
'contact',
|
||||
'accounts',
|
||||
'type'
|
||||
'type',
|
||||
'emailAddress',
|
||||
];
|
||||
|
||||
nonAdminReadOnlyFieldList = nonAdminReadOnlyFieldList.filter(
|
||||
function (item) {
|
||||
if (!this.model.hasField(item)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var aclDefs = this.getMetadata().get(['entityAcl', 'User', 'fields', item]);
|
||||
|
||||
if (!aclDefs) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (aclDefs.nonAdminReadOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}.bind(this),
|
||||
);
|
||||
|
||||
nonAdminReadOnlyFieldList.forEach(function (field) {
|
||||
this.setFieldReadOnly(field, true);
|
||||
}, this);
|
||||
|
||||
+7
-3
@@ -31,6 +31,11 @@ if (substr(php_sapi_name(), 0, 3) != 'cli') exit;
|
||||
|
||||
include "bootstrap.php";
|
||||
|
||||
use Espo\Core\{
|
||||
Application,
|
||||
ApplicationRunners\Rebuild,
|
||||
};
|
||||
|
||||
$arg = isset($_SERVER['argv'][1]) ? trim($_SERVER['argv'][1]) : '';
|
||||
|
||||
if (empty($arg)) {
|
||||
@@ -46,7 +51,7 @@ if (!isset($pathInfo['extension']) || $pathInfo['extension'] !== 'zip' || !is_fi
|
||||
die("Unsupported package.\n");
|
||||
}
|
||||
|
||||
$app = new \Espo\Core\Application();
|
||||
$app = new Application();
|
||||
$app->setupSystemUser();
|
||||
|
||||
$config = $app->getContainer()->get('config');
|
||||
@@ -67,8 +72,7 @@ try {
|
||||
}
|
||||
|
||||
try {
|
||||
$app = new \Espo\Core\Application();
|
||||
$app->runRebuild();
|
||||
(new Application())->run(Rebuild::class);
|
||||
} catch (\Exception $e) {}
|
||||
|
||||
echo "Extension installation is complete.\n";
|
||||
|
||||
@@ -82,12 +82,24 @@ class SystemHelper extends \Espo\Core\Utils\System
|
||||
|
||||
public function getBaseUrl()
|
||||
{
|
||||
$pageUrl = ($_SERVER["HTTPS"] == 'on') ? 'https://' : 'http://';
|
||||
$pageUrl = 'http://';
|
||||
|
||||
if ($_SERVER["SERVER_PORT"] != "80") {
|
||||
$pageUrl .= $_SERVER["SERVER_NAME"].":".$_SERVER["SERVER_PORT"].$_SERVER["REQUEST_URI"];
|
||||
} else {
|
||||
if (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] == 'https') {
|
||||
$pageUrl = 'https://';
|
||||
}
|
||||
|
||||
if (isset($_SERVER['HTTPS']) && $_SERVER["HTTPS"] == 'on') {
|
||||
$pageUrl = 'https://';
|
||||
}
|
||||
|
||||
if ($_SERVER["SERVER_PORT"] == '443') {
|
||||
$pageUrl = 'https://';
|
||||
}
|
||||
|
||||
if (in_array($_SERVER["SERVER_PORT"], ['80', '443'])) {
|
||||
$pageUrl .= $_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"];
|
||||
} else {
|
||||
$pageUrl .= $_SERVER["SERVER_NAME"].":".$_SERVER["SERVER_PORT"].$_SERVER["REQUEST_URI"];
|
||||
}
|
||||
|
||||
$baseUrl = str_ireplace('/install/index.php', '', $pageUrl);
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
{foreach from=$phpRequirementList key=name item=value}
|
||||
<tr class="list-row">
|
||||
<td class="cell col-md-5">
|
||||
{if $langs['systemRequirements'][$name] eq ''}
|
||||
{$name}
|
||||
{if isset($langs['systemRequirements'][$name])}
|
||||
{$langs['systemRequirements'][{$name}]}
|
||||
{else}
|
||||
{$langs['systemRequirements'][{$name}]}
|
||||
{$name}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="cell col-md-3">{$value['actual']}</td>
|
||||
@@ -41,10 +41,10 @@
|
||||
{foreach from=$mysqlRequirementList key=name item=value}
|
||||
<tr class="list-row">
|
||||
<td class="cell col-md-5">
|
||||
{if $langs['systemRequirements'][$name] eq ''}
|
||||
{$name}
|
||||
{if isset($langs['systemRequirements'][$name])}
|
||||
{$langs['systemRequirements'][{$name}]}
|
||||
{else}
|
||||
{$langs['systemRequirements'][{$name}]}
|
||||
{$name}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="cell col-md-3">{$value['actual']}</td>
|
||||
@@ -68,10 +68,10 @@
|
||||
{foreach from=$permissionRequirementList key=name item=value}
|
||||
<tr class="list-row">
|
||||
<td class="cell col-md-5">
|
||||
{if $langs['systemRequirements'][$name] eq ''}
|
||||
{$name}
|
||||
{if isset($langs['systemRequirements'][$name])}
|
||||
{$langs['systemRequirements'][{$name}]}
|
||||
{else}
|
||||
{$langs['systemRequirements'][{$name}]}
|
||||
{$name}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="cell col-md-3">{$langs['systemRequirements'][{$value['type']}]}</td>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user