Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff8e8d23a8 | |||
| ddb636239b | |||
| 917dbdd3fe | |||
| f645af4347 | |||
| be228fa48a | |||
| 5d443dda10 | |||
| 74db03090b | |||
| 03ac0a57c8 | |||
| 1f251eecb3 | |||
| 83439e5384 | |||
| 1df75fcece |
@@ -1303,7 +1303,7 @@ class Service implements Crud,
|
||||
|
||||
$this->getRepository()
|
||||
->getRelation($entity, $link)
|
||||
->relate($foreignEntity, [SaveOption::API => true]);
|
||||
->relate($foreignEntity, null, [SaveOption::API => true]);
|
||||
|
||||
$countRelated++;
|
||||
}
|
||||
|
||||
@@ -1358,16 +1358,22 @@ class ItemGeneralConverter implements ItemConverter
|
||||
$nearKey = $defs->getMidKey();
|
||||
$middleEntityType = ucfirst($defs->getRelationshipName());
|
||||
|
||||
$conditions = [
|
||||
"$alias.$nearKey:" => Attribute::ID,
|
||||
"$alias.deleted" => false,
|
||||
];
|
||||
|
||||
foreach ($defs->getConditions() as $k => $v) {
|
||||
$conditions["$alias.$k"] = $v;
|
||||
}
|
||||
|
||||
// The foreign table is not joined as it would perform much slower.
|
||||
// Trade off is that if a foreign record is deleted but the middle table
|
||||
// is not yet deleted, it will give a non-actual result.
|
||||
$subQuery = QueryBuilder::create()
|
||||
->select(Attribute::ID)
|
||||
->from($this->entityType)
|
||||
->leftJoin($middleEntityType, $alias, [
|
||||
"$alias.$nearKey:" => Attribute::ID,
|
||||
"$alias.deleted" => false,
|
||||
])
|
||||
->leftJoin($middleEntityType, $alias, $conditions)
|
||||
->where(["$alias.$key" => null])
|
||||
->build();
|
||||
|
||||
@@ -1410,16 +1416,22 @@ class ItemGeneralConverter implements ItemConverter
|
||||
$nearKey = $defs->getMidKey();
|
||||
$middleEntityType = ucfirst($defs->getRelationshipName());
|
||||
|
||||
$conditions = [
|
||||
"$alias.$nearKey:" => Attribute::ID,
|
||||
"$alias.deleted" => false,
|
||||
];
|
||||
|
||||
foreach ($defs->getConditions() as $k => $v) {
|
||||
$conditions["$alias.$k"] = $v;
|
||||
}
|
||||
|
||||
// The foreign table is not joined as it would perform much slower.
|
||||
// Trade off is that if a foreign record is deleted but the middle table
|
||||
// is not yet deleted, it will give a non-actual result.
|
||||
$subQuery = QueryBuilder::create()
|
||||
->select(Attribute::ID)
|
||||
->from($this->entityType)
|
||||
->leftJoin($middleEntityType, $alias, [
|
||||
"$alias.$nearKey:" => Attribute::ID,
|
||||
"$alias.deleted" => false,
|
||||
])
|
||||
->leftJoin($middleEntityType, $alias, $conditions)
|
||||
->where(["$alias.$key!=" => null])
|
||||
->build();
|
||||
|
||||
@@ -1476,7 +1488,6 @@ class ItemGeneralConverter implements ItemConverter
|
||||
|
||||
if ($relationType == Entity::MANY_MANY) {
|
||||
$key = $defs->getForeignMidKey();
|
||||
$nearKey = $defs->getMidKey();
|
||||
|
||||
// IN-sub-query performs faster than EXISTS on MariaDB when multiple IDs.
|
||||
// Left-join performs faster than inner-join.
|
||||
@@ -1489,12 +1500,6 @@ class ItemGeneralConverter implements ItemConverter
|
||||
->from($this->entityType)
|
||||
->leftJoin(
|
||||
Join::create($link, $alias)
|
||||
->withConditions(
|
||||
Cond::equal(
|
||||
Cond::column("$alias.$nearKey"),
|
||||
Cond::column(Attribute::ID)
|
||||
)
|
||||
)
|
||||
->withOnlyMiddle()
|
||||
)
|
||||
->where(["$alias.$key" => $value])
|
||||
|
||||
@@ -34,7 +34,7 @@ use Doctrine\DBAL\Platforms\Keywords\MariaDBKeywords;
|
||||
/**
|
||||
* 'LEAD' happened to be a reserved words on some environments.
|
||||
*/
|
||||
final class MariaDb102Keywords extends MariaDBKeywords
|
||||
class MariaDb102Keywords extends MariaDBKeywords
|
||||
{
|
||||
/** @deprecated */
|
||||
public function getName(): string
|
||||
|
||||
@@ -121,7 +121,11 @@ class RecordService
|
||||
$ids = [];
|
||||
$actionIds = [];
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
foreach ($collection as $i => $entity) {
|
||||
if ($i === $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
$ids[] = $entity->getId();
|
||||
|
||||
$groupedCount = null;
|
||||
|
||||
@@ -48,7 +48,7 @@ class UrlValidator
|
||||
{
|
||||
$siteUrl = rtrim($this->config->get('siteUrl') ?? '', '/');
|
||||
|
||||
if (str_starts_with($url, $siteUrl)) {
|
||||
if (UrlValidatorUtil::validate($url, $siteUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class UrlValidator
|
||||
foreach ($portals as $portal) {
|
||||
$siteUrl = rtrim($portal->getUrl() ?? '', '/');
|
||||
|
||||
if (str_starts_with($url, $siteUrl)) {
|
||||
if (UrlValidatorUtil::validate($url, $siteUrl)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\UserSecurity\Password\Recovery;
|
||||
|
||||
use const FILTER_VALIDATE_URL;
|
||||
use const PHP_URL_HOST;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class UrlValidatorUtil
|
||||
{
|
||||
public static function validate(string $url, string $siteUrl): bool
|
||||
{
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
$siteHost = parse_url($siteUrl, PHP_URL_HOST);
|
||||
|
||||
if ($host !== $siteHost) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!str_starts_with($url, $siteUrl)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,13 @@ class CurrencyFieldView extends FloatFieldView {
|
||||
'range',
|
||||
]
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {string}
|
||||
* @since 9.2.6
|
||||
*/
|
||||
currencyAttribute
|
||||
|
||||
/** @inheritDoc */
|
||||
data() {
|
||||
const currencyValue = this.model.get(this.currencyFieldName) ||
|
||||
@@ -123,7 +130,7 @@ class CurrencyFieldView extends FloatFieldView {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.currencyFieldName = this.name + 'Currency';
|
||||
this.currencyFieldName = this.currencyAttribute ?? this.name + 'Currency';
|
||||
this.defaultCurrency = this.getConfig().get('defaultCurrency');
|
||||
this.currencyList = this.getConfig().get('currencyList') || [this.defaultCurrency];
|
||||
this.decimalPlaces = this.getConfig().get('currencyDecimalPlaces');
|
||||
|
||||
@@ -42,7 +42,7 @@ class ComposeEmailModalView extends EditModalView {
|
||||
shortcutKeys = {
|
||||
/** @this ComposeEmailModalView */
|
||||
'Control+Enter': function (e) {
|
||||
if (this.buttonList.findIndex(item => item.name === 'send' && !item.hidden) === -1) {
|
||||
if (this.buttonList.findIndex(item => item.name === 'send' && !item.hidden && !item.disabled) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class EditModalView extends ModalView {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.buttonList.findIndex(item => item.name === 'save' && !item.hidden) === -1) {
|
||||
if (this.buttonList.findIndex(item => item.name === 'save' && !item.hidden && !item.disabled) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ class EditModalView extends ModalView {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.buttonList.findIndex(item => item.name === 'save' && !item.hidden) === -1) {
|
||||
if (this.buttonList.findIndex(item => item.name === 'save' && !item.hidden && !item.disabled) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ class RelatedListModalView extends ModalView {
|
||||
this.$header.append(
|
||||
title ||
|
||||
$('<span>').text(
|
||||
this.getLanguage().translate(this.link, 'links', this.entityType)
|
||||
this.getLanguage().translate(this.link, 'links', this.scope)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -139,12 +139,17 @@ export default class extends View {
|
||||
|
||||
const url = data.url || this.baseUrl;
|
||||
|
||||
const msg = this.translate('passwordChangedByRequest', 'messages', 'User') +
|
||||
' <a href="' + url + '">' + this.translate('Login', 'labels', 'User') + '</a>.';
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.innerText = this.translate('Login', 'labels', 'User');
|
||||
|
||||
const message = this.translate('passwordChangedByRequest', 'messages', 'User');
|
||||
|
||||
const html = this.getHelper().escapeString(message) + ' ' + a.outerHTML;
|
||||
|
||||
this.$el.find('.msg-box')
|
||||
.removeClass('hidden')
|
||||
.html('<span class="text-success">' + msg + '</span>');
|
||||
.html('<span class="text-success">' + html + '</span>');
|
||||
})
|
||||
.catch(() => {
|
||||
return $submit.removeClass('disabled');
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "9.2.5",
|
||||
"version": "9.2.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "espocrm",
|
||||
"version": "9.2.5",
|
||||
"version": "9.2.7",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "9.2.5",
|
||||
"version": "9.2.7",
|
||||
"description": "Open-source CRM.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -372,9 +372,8 @@ class ConverterTest extends TestCase
|
||||
[
|
||||
'test',
|
||||
$alias,
|
||||
[$alias . '.localId=:' => 'id'],
|
||||
null,
|
||||
[
|
||||
'noLeftAlias' => true,
|
||||
'onlyMiddle' => true,
|
||||
'type' => JoinType::left,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace tests\unit\Espo\Tools\UserSecurity\Password;
|
||||
|
||||
use Espo\Tools\UserSecurity\Password\Recovery\UrlValidatorUtil;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UrlValidatorUtilTest extends TestCase
|
||||
{
|
||||
public function testValidate(): void
|
||||
{
|
||||
$this->assertTrue(
|
||||
UrlValidatorUtil::validate('https://test.com', 'https://test.com')
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
UrlValidatorUtil::validate('https://test.com/test', 'https://test.com')
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
UrlValidatorUtil::validate('https://test.com/test', 'https://test.com/test')
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
UrlValidatorUtil::validate('https://test.com.test', 'https://test.com')
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
UrlValidatorUtil::validate('https://test.com.test<test', 'https://test.com')
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user