Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66a6f70a30 | |||
| d7a629c5a3 | |||
| 506e96e333 | |||
| 76a9177c46 | |||
| a8baac3f92 | |||
| 8d9d073c16 | |||
| 177ecdb70b | |||
| 89ce80d5b2 | |||
| 7f07175bee | |||
| 8bca35934c | |||
| 65bef1df65 | |||
| 922a2e835c |
@@ -55,7 +55,8 @@ class TwoFactorEmail
|
||||
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
!$this->user->isRegular()
|
||||
!$this->user->isRegular() &&
|
||||
!$this->user->isPortal()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ class TwoFactorSms
|
||||
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
!$this->user->isRegular()
|
||||
!$this->user->isRegular() &&
|
||||
!$this->user->isPortal()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ class UserSecurity
|
||||
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
!$this->user->isRegular()
|
||||
!$this->user->isRegular() &&
|
||||
!$this->user->isPortal()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ use Espo\ORM\Defs\RelationDefs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Metadata\Helper as MetadataHelper;
|
||||
use LogicException;
|
||||
|
||||
class Converter
|
||||
{
|
||||
@@ -160,11 +161,11 @@ class Converter
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($ormMetadata as $entityOrmMetadata) {
|
||||
foreach ($ormMetadata as $entityType => $entityOrmMetadata) {
|
||||
/** @var array<string, array<string, mixed>> $ormMetadata */
|
||||
$ormMetadata = Util::merge(
|
||||
$ormMetadata,
|
||||
$this->createEntityTypesFromRelations($entityOrmMetadata)
|
||||
$this->createEntityTypesFromRelations($entityType, $entityOrmMetadata)
|
||||
);
|
||||
|
||||
/** @var array<string, array<string, mixed>> $ormMetadata */
|
||||
@@ -846,7 +847,7 @@ class Converter
|
||||
* @param array<string, mixed> $defs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function createEntityTypesFromRelations(array $defs): array
|
||||
private function createEntityTypesFromRelations(string $entityType, array $defs): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
@@ -873,6 +874,11 @@ class Converter
|
||||
],
|
||||
];
|
||||
|
||||
if (!$relationDefs->hasMidKey()) {
|
||||
throw new LogicException(
|
||||
"Bad manyMany relation {$name} in {$entityType}. Might be not defined on the other side.");
|
||||
}
|
||||
|
||||
$key1 = $relationDefs->getMidKey();
|
||||
$key2 = $relationDefs->getForeignMidKey();
|
||||
|
||||
|
||||
@@ -33,12 +33,15 @@ use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
|
||||
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
|
||||
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
|
||||
use Espo\Core\Utils\Database\Orm\LinkConverter;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\ORM\Defs\RelationDefs as LinkDefs;
|
||||
use Espo\ORM\Type\AttributeType;
|
||||
use Espo\ORM\Type\RelationType;
|
||||
|
||||
class HasMany implements LinkConverter
|
||||
{
|
||||
public function __construct(private Log $log) {}
|
||||
|
||||
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
|
||||
{
|
||||
if (!$linkDefs->hasForeignRelationName() && $linkDefs->getParam('disabled')) {
|
||||
@@ -52,9 +55,20 @@ class HasMany implements LinkConverter
|
||||
$foreignRelationName = $linkDefs->getForeignRelationName();
|
||||
$hasField = $linkDefs->getParam('hasField');
|
||||
|
||||
$type = $linkDefs->hasRelationshipName() ?
|
||||
$type = RelationType::HAS_MANY;
|
||||
|
||||
/*$type = $linkDefs->hasRelationshipName() ?
|
||||
RelationType::MANY_MANY : // Revise.
|
||||
RelationType::HAS_MANY;
|
||||
RelationType::HAS_MANY;*/
|
||||
|
||||
if ($linkDefs->hasRelationshipName()) {
|
||||
$this->log->warning(
|
||||
"Issue with the link '{$name}' in '{$entityType}' entity type. Might be the foreign link " .
|
||||
"'{$foreignRelationName}' in '{$foreignEntityType}' entity type is missing. " .
|
||||
"Remove the problem link manually.");
|
||||
|
||||
return EntityDefs::create();
|
||||
}
|
||||
|
||||
return EntityDefs::create()
|
||||
->withAttribute(
|
||||
|
||||
@@ -36,11 +36,13 @@ use Espo\Core\Utils\Database\Orm\LinkConverters\HasChildren;
|
||||
use Espo\Core\Utils\Database\Orm\LinkConverters\HasMany;
|
||||
use Espo\Core\Utils\Database\Orm\LinkConverters\HasOne;
|
||||
use Espo\Core\Utils\Database\Orm\LinkConverters\ManyMany;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs\RelationDefs;
|
||||
use Espo\ORM\Type\AttributeType;
|
||||
use Espo\ORM\Type\RelationType;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class RelationConverter
|
||||
@@ -59,7 +61,8 @@ class RelationConverter
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Log $log
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -87,9 +90,15 @@ class RelationConverter
|
||||
$params['relationName'] = $relationshipName;
|
||||
}
|
||||
|
||||
$linkType = $params['type'];
|
||||
$linkType = $params['type'] ?? null;
|
||||
$foreignLinkType = $foreignParams ? $foreignParams['type'] : null;
|
||||
|
||||
if (!$linkType) {
|
||||
$this->log->warning("Link {$entityType}.{$name} has no type.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$params['hasField'] = (bool) $this->metadata
|
||||
->get(['entityDefs', $entityType, 'fields', $name]);
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ return [
|
||||
'useWebSocket' => false,
|
||||
'webSocketMessager' => 'ZeroMQ',
|
||||
'auth2FAMethodList' => ['Totp'],
|
||||
'auth2FAInPortal' => false,
|
||||
'personNameFormat' => 'firstLast',
|
||||
'newNotificationCountInTitle' => false,
|
||||
'pdfEngine' => 'Dompdf',
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
"auth2FA": "Enable 2-Factor Authentication",
|
||||
"auth2FAForced": "Force regular users to set up 2FA",
|
||||
"auth2FAMethodList": "Available 2FA methods",
|
||||
"auth2FAInPortal": "Allow 2FA in portals",
|
||||
"workingTimeCalendar": "Working Time Calendar",
|
||||
"oidcClientId": "OIDC Client ID",
|
||||
"oidcClientSecret": "OIDC Client Secret",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"label": "2-Factor Authentication",
|
||||
"rows": [
|
||||
[{"name": "auth2FA"}, {"name": "auth2FAMethodList"}],
|
||||
[{"name": "auth2FAForced"}, false]
|
||||
[{"name": "auth2FAForced"}, {"name": "auth2FAInPortal"}]
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -289,6 +289,9 @@
|
||||
"auth2FAForced": {
|
||||
"type": "bool"
|
||||
},
|
||||
"auth2FAInPortal": {
|
||||
"type": "bool"
|
||||
},
|
||||
"passwordRecoveryDisabled": {
|
||||
"type": "bool"
|
||||
},
|
||||
|
||||
@@ -38,9 +38,7 @@
|
||||
],
|
||||
"filter": true,
|
||||
"notCreatable": true,
|
||||
"fieldDefs": {
|
||||
"notStorable": true
|
||||
},
|
||||
|
||||
"valueFactoryClassName": "Espo\\Core\\Field\\LinkParent\\LinkParentFactory",
|
||||
"attributeExtractorClassName": "Espo\\Core\\Field\\LinkParent\\LinkParentAttributeExtractor"
|
||||
}
|
||||
|
||||
@@ -76,7 +76,12 @@ class Service
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && !$user->isRegular()) {
|
||||
$allow =
|
||||
$user->isAdmin() ||
|
||||
$user->isRegular() ||
|
||||
$user->isPortal() && $this->config->get('auth2FAInPortal');
|
||||
|
||||
if (!$allow) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -116,7 +121,15 @@ class Service
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && !$user->isRegular()) {
|
||||
$allow =
|
||||
$this->config->get('auth2FA') &&
|
||||
(
|
||||
$user->isAdmin() ||
|
||||
$user->isRegular() ||
|
||||
$user->isPortal() && $this->config->get('auth2FAInPortal')
|
||||
);
|
||||
|
||||
if (!$allow) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -178,7 +191,12 @@ class Service
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && !$user->isRegular()) {
|
||||
$allow =
|
||||
$user->isAdmin() ||
|
||||
$user->isRegular() ||
|
||||
$user->isPortal() && $this->config->get('auth2FAInPortal');
|
||||
|
||||
if (!$allow) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -219,7 +237,11 @@ class Service
|
||||
if (
|
||||
$userData->get('auth2FA') &&
|
||||
$userData->get('auth2FAMethod') &&
|
||||
($userData->isAttributeChanged('auth2FA') || $userData->isAttributeChanged('auth2FAMethod'))
|
||||
($userData->isAttributeChanged('auth2FA') || $userData->isAttributeChanged('auth2FAMethod')) &&
|
||||
(
|
||||
!$user->isPortal() ||
|
||||
$this->config->get('auth2FAInPortal')
|
||||
)
|
||||
) {
|
||||
$auth2FAMethod = $userData->get('auth2FAMethod');
|
||||
|
||||
|
||||
@@ -93,6 +93,10 @@ class EmailService
|
||||
throw new Forbidden("2FA is not enabled.");
|
||||
}
|
||||
|
||||
if ($this->user->isPortal() && !$this->config->get('auth2FAInPortal')) {
|
||||
throw new Forbidden("2FA is not enabled in portals.");
|
||||
}
|
||||
|
||||
$methodList = $this->config->get('auth2FAMethodList') ?? [];
|
||||
|
||||
if (!in_array(EmailLogin::NAME, $methodList)) {
|
||||
|
||||
@@ -93,6 +93,10 @@ class SmsService
|
||||
throw new Forbidden("2FA is not enabled.");
|
||||
}
|
||||
|
||||
if ($this->user->isPortal() && !$this->config->get('auth2FAInPortal')) {
|
||||
throw new Forbidden("2FA is not enabled in portals.");
|
||||
}
|
||||
|
||||
$methodList = $this->config->get('auth2FAMethodList') ?? [];
|
||||
|
||||
if (!in_array(SmsLogin::NAME, $methodList)) {
|
||||
|
||||
@@ -155,6 +155,7 @@ define('views/admin/authentication', ['views/settings/record/edit'], function (D
|
||||
if (this.model.get('auth2FA')) {
|
||||
this.showField('auth2FAForced');
|
||||
this.showField('auth2FAMethodList');
|
||||
this.showField('auth2FAInPortal');
|
||||
this.setFieldRequired('auth2FAMethodList');
|
||||
|
||||
return;
|
||||
@@ -162,6 +163,7 @@ define('views/admin/authentication', ['views/settings/record/edit'], function (D
|
||||
|
||||
this.hideField('auth2FAForced');
|
||||
this.hideField('auth2FAMethodList');
|
||||
this.hideField('auth2FAInPortal');
|
||||
this.setFieldNotRequired('auth2FAMethodList');
|
||||
},
|
||||
|
||||
|
||||
@@ -119,6 +119,12 @@ function (Dep, /** module:ui/select*/Select) {
|
||||
showWarnings: false,
|
||||
formulaMode: true,
|
||||
};
|
||||
|
||||
if (this.decimalPlaces === null) {
|
||||
this.autoNumericOptions.decimalPlaces = this.decimalPlacesRawValue;
|
||||
this.autoNumericOptions.decimalPlacesRawValue = this.decimalPlacesRawValue;
|
||||
this.autoNumericOptions.allowDecimalPadding = false;
|
||||
}
|
||||
},
|
||||
|
||||
getCurrencyFormat: function () {
|
||||
@@ -149,13 +155,12 @@ function (Dep, /** module:ui/select*/Select) {
|
||||
},
|
||||
|
||||
formatNumber: function (value) {
|
||||
if (this.isReadMode()) {
|
||||
return this.formatNumberDetail(value);
|
||||
}
|
||||
|
||||
return this.formatNumberEdit(value);
|
||||
return this.formatNumberDetail(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo Remove. Used in range.
|
||||
*/
|
||||
formatNumberEdit: function (value) {
|
||||
let currencyDecimalPlaces = this.decimalPlaces;
|
||||
|
||||
|
||||
@@ -60,6 +60,14 @@ define('views/fields/float', ['views/fields/int'], function (Dep) {
|
||||
else if (this.getConfig().has('decimalMark')) {
|
||||
this.decimalMark = this.getConfig().get('decimalMark');
|
||||
}
|
||||
|
||||
if (!this.decimalMark) {
|
||||
this.decimalMark = '.';
|
||||
}
|
||||
|
||||
if (this.decimalMark === this.thousandSeparator) {
|
||||
this.thousandSeparator = '';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -90,13 +98,12 @@ define('views/fields/float', ['views/fields/int'], function (Dep) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (this.isReadMode()) {
|
||||
return this.formatNumberDetail(value);
|
||||
}
|
||||
|
||||
return this.formatNumberEdit(value);
|
||||
return this.formatNumberDetail(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo Remove. Used in range.
|
||||
*/
|
||||
formatNumberEdit: function (value) {
|
||||
if (value === null) {
|
||||
return '';
|
||||
|
||||
@@ -99,8 +99,16 @@ define('views/fields/int', ['views/fields/base', 'lib!autonumeric'], function (D
|
||||
* @protected
|
||||
*/
|
||||
setupAutoNumericOptions: function () {
|
||||
let separator = (!this.disableFormatting ? this.thousandSeparator : null) || '';
|
||||
let decimalCharacter = '.';
|
||||
|
||||
if (separator === '.') {
|
||||
decimalCharacter = ',';
|
||||
}
|
||||
|
||||
this.autoNumericOptions = {
|
||||
digitGroupSeparator: (!this.disableFormatting ? this.thousandSeparator : null) || '',
|
||||
digitGroupSeparator: separator,
|
||||
decimalCharacter: decimalCharacter,
|
||||
modifyValueOnWheel: false,
|
||||
decimalPlaces: 0,
|
||||
selectOnFocus: false,
|
||||
@@ -162,6 +170,10 @@ define('views/fields/int', ['views/fields/base', 'lib!autonumeric'], function (D
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isEditMode()) {
|
||||
data.value = this.model.get(this.name);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -176,11 +188,7 @@ define('views/fields/int', ['views/fields/base', 'lib!autonumeric'], function (D
|
||||
return value;
|
||||
}
|
||||
|
||||
if (this.isReadMode()) {
|
||||
return this.formatNumberDetail(value);
|
||||
}
|
||||
|
||||
return this.formatNumberEdit(value);
|
||||
return this.formatNumberDetail(value);
|
||||
},
|
||||
|
||||
formatNumberDetail: function (value) {
|
||||
@@ -195,6 +203,9 @@ define('views/fields/int', ['views/fields/base', 'lib!autonumeric'], function (D
|
||||
return stringValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo Remove. Used in range.
|
||||
*/
|
||||
formatNumberEdit: function (value) {
|
||||
if (value === null) {
|
||||
return '';
|
||||
|
||||
@@ -51,10 +51,16 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
}
|
||||
}
|
||||
|
||||
let isPortalUser = this.model.isPortal() ||
|
||||
this.model.id === this.getUser().id && this.getUser().isPortal();
|
||||
|
||||
if (
|
||||
(this.model.id == this.getUser().id || this.getUser().isAdmin()) &&
|
||||
(this.model.isRegular() || this.model.isAdmin()) &&
|
||||
this.getConfig().get('auth2FA')
|
||||
(this.model.id === this.getUser().id || this.getUser().isAdmin()) &&
|
||||
this.getConfig().get('auth2FA') &&
|
||||
(
|
||||
(this.model.isRegular() || this.model.isAdmin()) ||
|
||||
isPortalUser && this.getConfig().get('auth2FAInPortal')
|
||||
)
|
||||
) {
|
||||
this.addButton({
|
||||
name: 'viewSecurity',
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "7.4.0",
|
||||
"version": "7.4.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "espocrm",
|
||||
"version": "7.4.0",
|
||||
"version": "7.4.3",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "7.4.0",
|
||||
"version": "7.4.3",
|
||||
"description": "Open-source CRM.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
use Espo\Core\Container;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class AfterUpgrade
|
||||
{
|
||||
public function run(Container $container): void
|
||||
{
|
||||
$this->updateMetadata($container->get('metadata'));
|
||||
}
|
||||
|
||||
private function updateMetadata(Metadata $metadata): void
|
||||
{
|
||||
$this->fixParent($metadata);
|
||||
|
||||
$metadata->save();
|
||||
}
|
||||
|
||||
private function fixParent(Metadata $metadata): void
|
||||
{
|
||||
foreach ($metadata->get(['entityDefs']) as $scope => $defs) {
|
||||
foreach ($metadata->get(['entityDefs', $scope, 'fields']) as $field => $fieldDefs) {
|
||||
$custom = $metadata->getCustom('entityDefs', $scope);
|
||||
|
||||
if (!$custom) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
($fieldDefs['type'] ?? null) === 'linkParent' &&
|
||||
($fieldDefs['notStorable'] ?? false)
|
||||
) {
|
||||
if ($custom?->fields?->$field?->notStorable) {
|
||||
$metadata->delete('entityDefs', $scope, "fields.{$field}.notStorable");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,30 @@ class AfterUpgrade
|
||||
'length' => 24,
|
||||
]);
|
||||
|
||||
$this->fixParent($metadata);
|
||||
|
||||
$metadata->save();
|
||||
}
|
||||
|
||||
private function fixParent(Metadata $metadata): void
|
||||
{
|
||||
foreach ($metadata->get(['entityDefs']) as $scope => $defs) {
|
||||
foreach ($metadata->get(['entityDefs', $scope, 'fields']) as $field => $fieldDefs) {
|
||||
$custom = $metadata->getCustom('entityDefs', $scope);
|
||||
|
||||
if (!$custom) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
($fieldDefs['type'] ?? null) === 'linkParent' &&
|
||||
($fieldDefs['notStorable'] ?? false)
|
||||
) {
|
||||
if ($custom?->fields?->$field?->notStorable) {
|
||||
$metadata->delete('entityDefs', $scope, "fields.{$field}.notStorable");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user