Compare commits

...

27 Commits
7.2.4 ... 7.2.6

Author SHA1 Message Date
Yuri Kuznetsov
eb6f9b602f upgrade 2022-10-23 16:40:40 +03:00
Yuri Kuznetsov
cf508a540e Merge branch 'fix' of https://github.com/espocrm/espocrm into fix 2022-10-23 16:36:19 +03:00
Yuri Kuznetsov
5278e3bf06 v 2022-10-23 16:25:17 +03:00
David
5763f5b58e fixed require promise (#2473)
Co-authored-by: David Moškoř <david.moskor@apertia.cz>
2022-10-20 18:55:50 +03:00
Taras Machyshyn
0666880786 IIS web.config fixes 2022-10-19 13:18:45 +03:00
Yuri Kuznetsov
e0113388d2 typo fix 2022-10-11 13:04:26 +03:00
Yuri Kuznetsov
384f28ecae cleanup 2022-10-11 13:02:25 +03:00
Yuri Kuznetsov
d7596c208c fix 2022-10-05 09:35:07 +03:00
Yuri Kuznetsov
6d1ab5870f fix avatar 2022-10-04 08:52:40 +03:00
Yuri Kuznetsov
73dbfa38ec css fix 2022-09-29 14:46:37 +03:00
Yuri Kuznetsov
4adb068699 fix lead capture source 2022-09-29 09:02:55 +03:00
Yuri Kuznetsov
3e4c738ab1 fix link multiple cloning fetch 2022-09-28 15:08:12 +03:00
Yuri Kuznetsov
cd92e4fcd8 fix util array to object to preserve lists 2022-09-27 09:49:14 +03:00
Yuri Kuznetsov
fba191f22c global search label 2022-09-27 08:57:51 +03:00
Yuri Kuznetsov
8874c8827a fix 2022-09-24 13:07:22 +03:00
Yuri Kuznetsov
34529a8ed9 numpad enter 2022-09-23 10:40:05 +03:00
Yuri Kuznetsov
8fd44acae2 entry point 404 2022-09-21 14:40:28 +03:00
Yuri Kuznetsov
4dd540ffc7 cleanup 2022-09-21 11:27:36 +03:00
Yuri Kuznetsov
9c20116c9b v 2022-09-20 20:21:47 +03:00
Yuri Kuznetsov
926410d58f upgrade file 2022-09-20 20:21:11 +03:00
Yuri Kuznetsov
c64a107ad9 dif docs 2022-09-20 20:20:54 +03:00
Yuri Kuznetsov
5dcd25946b diff mandatory files 2022-09-20 20:09:15 +03:00
Yuri Kuznetsov
d12865bbcb lf command 2022-09-20 18:10:38 +03:00
Yuri Kuznetsov
3fed415437 clnup 2022-09-20 17:18:22 +03:00
Yuri Kuznetsov
a03a13d3b9 fix auth token error 500 2022-09-20 17:05:11 +03:00
Yuri Kuznetsov
e625951831 array link-list fix 2022-09-20 12:14:28 +03:00
Yuri Kuznetsov
f533c68c9b fix enum empty value not selected 2022-09-19 15:37:52 +03:00
34 changed files with 198 additions and 156 deletions

5
.gitattributes vendored
View File

@@ -8,4 +8,9 @@
*.tpl text eol=crlf
*.html text eol=crlf
bin/command text eol=lf
.gitattributes text eol=crlf
.gitignore text eol=crlf
*.png binary

View File

@@ -32,6 +32,7 @@ namespace Espo\Core\EntryPoint;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\{
Exceptions\NotFoundSilent,
InjectableFactory,
Utils\ClassFinder,
Api\Request,
@@ -44,7 +45,6 @@ use Espo\Core\{
class EntryPointManager
{
private InjectableFactory $injectableFactory;
private ClassFinder $classFinder;
public function __construct(InjectableFactory $injectableFactory, ClassFinder $classFinder)
@@ -53,12 +53,15 @@ class EntryPointManager
$this->classFinder = $classFinder;
}
/**
* @throws NotFound
*/
public function checkAuthRequired(string $name): bool
{
$className = $this->getClassName($name);
if (!$className) {
throw new NotFound("Entry point '{$name}' not found.");
throw new NotFoundSilent("Entry point '{$name}' not found.");
}
$noAuth = false;
@@ -75,23 +78,29 @@ class EntryPointManager
return $className::$authRequired ?? true;
}
/**
* @throws NotFound
*/
public function checkNotStrictAuth(string $name): bool
{
$className = $this->getClassName($name);
if (!$className) {
throw new NotFound("Entry point '{$name}' not found.");
throw new NotFoundSilent("Entry point '{$name}' not found.");
}
return $className::$notStrictAuth ?? false;
}
/**
* @throws NotFound
*/
public function run(string $name, Request $request, Response $response): void
{
$className = $this->getClassName($name);
if (!$className) {
throw new NotFound("Entry point '{$name}' not found.");
throw new NotFoundSilent("Entry point '{$name}' not found.");
}
$entryPoint = $this->injectableFactory->create($className);

View File

@@ -33,6 +33,7 @@ use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Application\Runner\Params as RunnerParams;
use Espo\Core\EntryPoint\EntryPointManager;
use Espo\Core\ApplicationUser;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Portal\Application as PortalApplication;
use Espo\Core\Authentication\AuthenticationFactory;
use Espo\Core\Authentication\AuthToken\Manager as AuthTokenManager;
@@ -89,7 +90,7 @@ class Starter
/**
* @throws BadRequest
* @throws \Espo\Core\Exceptions\NotFound
* @throws NotFound
*/
public function start(?string $entryPoint = null, bool $final = false): void
{
@@ -110,8 +111,19 @@ class Starter
throw new BadRequest("No 'entryPoint' param.");
}
$authRequired = $this->entryPointManager->checkAuthRequired($entryPoint);
$authNotStrict = $this->entryPointManager->checkNotStrictAuth($entryPoint);
$responseWrapped = new ResponseWrapper(new Response());
try {
$authRequired = $this->entryPointManager->checkAuthRequired($entryPoint);
$authNotStrict = $this->entryPointManager->checkNotStrictAuth($entryPoint);
}
catch (NotFound $exception) {
$this->errorOutput->processWithBodyPrinting($requestWrapped, $responseWrapped, $exception);
(new ResponseEmitter())->emit($responseWrapped->getResponse());
return;
}
if ($authRequired && !$authNotStrict && !$final) {
$portalId = $this->detectPortalId($requestWrapped);
@@ -123,8 +135,6 @@ class Starter
}
}
$responseWrapped = new ResponseWrapper(new Response());
$this->processRequest(
$entryPoint,
$requestWrapped,

View File

@@ -305,8 +305,17 @@ class Util
*/
private static function arrayToObjectInternal($value)
{
if (is_array($value)) {
return (object) array_map(fn($v) => self::arrayToObjectInternal($v), $value);
if (!is_array($value)) {
return $value;
}
// @todo Change to `array_is_list` when PHP 8.1 is the min supported.
$isList = $value === array_values($value);
$value = array_map(fn($v) => self::arrayToObjectInternal($v), $value);
if (!$isList) {
$value = (object) $value;
}
return $value;

View File

@@ -75,10 +75,14 @@ class Avatar extends Image implements Di\MetadataAware
$sum += ord($hash[$i]);
}
$x = intval($sum % 128) + 1;
$x = $sum % 128 + 1;
$colorList = $this->metadata->get(['app', 'avatars', 'colorList']) ?? $this->colorList;
if ($x === 128) {
$x--;
}
$index = intval($x * count($colorList) / 128);
return $colorList[$index];

View File

@@ -147,7 +147,7 @@ class RelationDefs
{
if (!$this->hasForeignEntityType()) {
throw new RuntimeException(
"No 'entity' paramater defined in the relation '{$this->name}'."
"No 'entity' parameter defined in the relation '{$this->name}'."
);
}
@@ -170,7 +170,7 @@ class RelationDefs
{
if (!$this->hasForeignRelationName()) {
throw new RuntimeException(
"No 'foreign' paramater defined in the relation '{$this->name}'."
"No 'foreign' parameter defined in the relation '{$this->name}'."
);
}
@@ -193,7 +193,7 @@ class RelationDefs
{
if (!$this->hasForeignKey()) {
throw new RuntimeException(
"No 'foreignKey' paramater defined in the relation '{$this->name}'."
"No 'foreignKey' parameter defined in the relation '{$this->name}'."
);
}
@@ -216,7 +216,7 @@ class RelationDefs
{
if (!$this->hasKey()) {
throw new RuntimeException(
"No 'key' paramater defined in the relation '{$this->name}'."
"No 'key' parameter defined in the relation '{$this->name}'."
);
}
@@ -224,7 +224,7 @@ class RelationDefs
}
/**
* Whether a mid key is defined. For Many-to-Many relationships only.
* Whether a mid-key is defined. For Many-to-Many relationships only.
*/
public function hasMidKey(): bool
{
@@ -232,14 +232,14 @@ class RelationDefs
}
/**
* Get a mid key. For Many-to-Many relationships only.
* Get a mid-key. For Many-to-Many relationships only.
* @throws RuntimeException
*/
public function getMidKey(): string
{
if (!$this->hasMidKey()) {
throw new RuntimeException(
"No 'midKey' paramater defined in the relation '{$this->name}'."
"No 'midKey' parameter defined in the relation '{$this->name}'."
);
}
@@ -247,7 +247,7 @@ class RelationDefs
}
/**
* Whether a foreign mid key is defined. For Many-to-Many relationships only.
* Whether a foreign mid-key is defined. For Many-to-Many relationships only.
* @throws RuntimeException
*/
public function hasForeignMidKey(): bool
@@ -256,14 +256,14 @@ class RelationDefs
}
/**
* Get a foreign mid key. For Many-to-Many relationships only.
* Get a foreign mid-key. For Many-to-Many relationships only.
* @throws RuntimeException
*/
public function getForeignMidKey(): string
{
if (!$this->hasForeignMidKey()) {
throw new RuntimeException(
"No 'foreignMidKey' paramater defined in the relation '{$this->name}'."
"No 'foreignMidKey' parameter defined in the relation '{$this->name}'."
);
}
@@ -286,7 +286,7 @@ class RelationDefs
{
if (!$this->hasRelationshipName()) {
throw new RuntimeException(
"No 'relationName' paramater defined in the relation '{$this->name}'."
"No 'relationName' parameter defined in the relation '{$this->name}'."
);
}

View File

@@ -271,7 +271,8 @@
"Field": "Field",
"Resolution": "Resolution",
"Resolve Conflict": "Resolve Conflict",
"Download": "Download"
"Download": "Download",
"Global Search": "Global Search"
},
"messages": {
"pleaseWait": "Please wait...",

View File

@@ -68,8 +68,9 @@
},
"leadSource": {
"type": "enum",
"view": "crm:views/opportunity/fields/lead-source",
"customizationOptionsDisabled": true,
"optionsPath": "entityDefs.Lead.fields.source.options",
"translation": "Lead.options.source",
"default": "Web Site"
},
"apiKey": {

View File

@@ -1,12 +1,4 @@
{
"assignmentNotificatorClassName": "Espo\\Classes\\AssignmentNotificators\\Email",
"readLoaderClassNameList": [
"Espo\\Classes\\FieldProcessing\\Email\\AddressDataLoader",
"Espo\\Classes\\FieldProcessing\\Email\\UserColumnsLoader"
],
"listLoaderClassNameList": [
"Espo\\Classes\\FieldProcessing\\Email\\StringDataLoader"
],
"massActions": {
"update": {
"allowed": true

View File

@@ -0,0 +1,6 @@
<a
href="#{{scope}}/view/{{model.id}}"
class="link"
data-id="{{model.id}}"
title="{{value}}"
>{{#if value}}{{{value}}}{{else}}{{translate 'None'}}{{/if}}</a>

View File

@@ -970,7 +970,7 @@ function (
resolve(options);
return;
};
}
this.requestUserData(data => {
options = data;

View File

@@ -766,7 +766,7 @@
return new Promise((resolve, reject) => {
this.require(
subject,
() => resolve(),
(...args) => resolve(...args),
() => reject()
);
});

View File

@@ -599,6 +599,8 @@ define('utils', [], function () {
getKeyFromKeyEvent: function (e) {
let key = e.code;
key = keyMap[key] || key;
if (e.shiftKey) {
key = 'Shift+' + key;
}
@@ -615,6 +617,10 @@ define('utils', [], function () {
},
};
const keyMap = {
'NumpadEnter': 'Enter',
};
/**
* @deprecated Use `Espo.Utils`.
*/

View File

@@ -437,7 +437,7 @@ function (marked, DOMPurify, /** typeof Handlebars */Handlebars) {
return value.indexOf(name) !== -1;
}
return value === name;
return value === name || !value && !name;
};
options.hash = options.hash || {};

View File

@@ -271,7 +271,7 @@ define(
.attr('tabindex', '0')
.attr('data-action', 'addToContact')
.attr('data-address', address)
.text(this.translate('Add to Lead', 'labels', 'Email'))
.text(this.translate('Add to Contact', 'labels', 'Email'))
)
);
}

View File

@@ -43,6 +43,8 @@ function (Dep, RegExpPattern, /** module:ui/multi-select*/MultiSelect) {
listTemplate: 'fields/array/list',
listLinkTemplate: 'fields/array/list-link',
detailTemplate: 'fields/array/detail',
editTemplate: 'fields/array/edit',

View File

@@ -770,8 +770,8 @@ define('views/fields/link-multiple', ['views/fields/base', 'helpers/record-modal
fetch: function () {
let data = {};
data[this.idsName] = this.ids;
data[this.nameHashName] = this.nameHash;
data[this.idsName] = Espo.Utils.clone(this.ids);
data[this.nameHashName] = Espo.Utils.clone(this.nameHash);
return data;
},

View File

@@ -36,7 +36,7 @@ define('views/global-search/global-search', ['view'], function (Dep) {
'keydown input.global-search-input': function (e) {
let key = Espo.Utils.getKeyFromKeyEvent(e);
if (e.code === 'Enter') {
if (e.code === 'Enter' || key === 'Enter' || key === 'Control+Enter') {
this.runSearch();
return;

View File

@@ -748,6 +748,8 @@ define('views/modal', ['view'], function (Dep) {
}
this.$el.find('footer button[data-name="'+name+'"]').removeClass('hidden');
this.adjustButtons();
},
/**

View File

@@ -326,7 +326,9 @@ define('views/record/search', ['view'], function (Dep) {
events: {
'keydown input[data-name="textFilter"]': function (e) {
if (e.code === 'Enter') {
let key = Espo.Utils.getKeyFromKeyEvent(e);
if (e.code === 'Enter' || key === 'Enter' || key === 'Control+Enter') {
this.search();
this.hideApplyFiltersButton();
@@ -449,8 +451,6 @@ define('views/record/search', ['view'], function (Dep) {
if (this.isSearchedWithAdvancedFilter) {
this.showResetFiltersButton();
console.log(this.$applyFilters.get(0));
this.$applyFilters.focus();
return;

13
diff.js
View File

@@ -31,8 +31,19 @@
* From a specified version to the current version or all packages needed for a release.
* Examples:
* * `node diff 5.9.0` - builds an upgrade from 5.9.0 to the current version;
* * `node diff 5.9.0` - builds an upgrade from 5.9.0 to the current version;
* * `node diff --all` - builds all upgrades needed for a release.
*
* Data for upgrade packages is defined in `upgrades/{x.x|x.x.x-x.x.x}/data.json`.
*
* Parameters:
* * `mandatoryFiles` {string[]} mandatory files to include in upgrade
* (even files that were not changed in version control);
* * `beforeUpgradeFiles` {string[]} files to copy in the beginning of the upgrade process;
* * `manifest` {object} upgrade manifest parameters.
*
* Manifest parameters:
* * `delete` {string[]} additional files to be deleted (usually those that are not in version control).
*/
const Diff = require('./js/diff');

View File

@@ -1153,6 +1153,12 @@ input.global-search-input {
color: var(--text-gray-color);
}
.list-container.list-container-panel {
> .no-data {
padding: @panel-padding;
}
}
.panel-body .list-container > .no-data {
color: var(--text-muted-color);
}

View File

@@ -253,7 +253,7 @@ class Diff
process.chdir(buildPath);
let fileList = [];
let fileList = upgradeData.mandatoryFiles || [];
let stdout = cp.execSync('git diff --name-only ' + versionFrom).toString();

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "7.2.4",
"version": "7.2.6",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "7.2.4",
"version": "7.2.6",
"description": "Open-source CRM.",
"repository": {
"type": "git",

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="rule 1G" stopProcessing="true">
<match url="^" />
<action type="Rewrite" url="index.php" appendQueryString="true" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="rule 1G" stopProcessing="true">
<match url="^" />
<action type="Rewrite" url="index.php" appendQueryString="true" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="rule 1G" stopProcessing="true">
<match url="^" />
<action type="Rewrite" url="index.php" appendQueryString="true" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

42
public/web.config Normal file
View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<defaultDocument>
<files>
<clear />
<add value="index.php" />
<add value="index.html" />
</files>
</defaultDocument>
<security>
<requestFiltering>
<verbs allowUnlisted="false">
<add verb="GET" allowed="true" />
<add verb="POST" allowed="true" />
<add verb="PUT" allowed="true" />
<add verb="PATCH" allowed="true" />
<add verb="DELETE" allowed="true" />
</verbs>
</requestFiltering>
</security>
<rewrite>
<rules>
<rule name="rule 1G" stopProcessing="true">
<match url="^api/v1/portal-access/(.*)$" />
<action type="Rewrite" url="api/v1/portal-access/index.php" appendQueryString="true" />
</rule>
<rule name="rule 2G" stopProcessing="true">
<match url="^api/v1/(.*)$" />
<action type="Rewrite" url="api/v1/index.php" appendQueryString="true" />
</rule>
<rule name="rule 3G" stopProcessing="true">
<match url="^portal/(.*)$" />
<action type="Rewrite" url="portal/index.php" appendQueryString="true" />
</rule>
</rules>
</rewrite>
<staticContent>
<mimeMap fileExtension=".tpl" mimeType="text/plain" />
</staticContent>
</system.webServer>
</configuration>

View File

@@ -1072,27 +1072,35 @@ class UtilTest extends \PHPUnit\Framework\TestCase
$this->assertEquals($result, Util::concatPath($input));
}
public function testArrayToObject()
public function testArrayToObject(): void
{
$testArr= array(
$testArr= [
'useCache' => true,
'sub' => array (
'sub' => [
'subV' => '125',
'subO' => array(
'subO' => [
'subOV' => '125',
),
),
);
],
'subList' => [
'0',
'1'
],
],
];
$testResult= (object) array(
$testResult= (object) [
'useCache' => true,
);
$testResult->sub = (object) array (
'subV' => '125',
);
$testResult->sub->subO = (object) array (
'subOV' => '125',
);
];
$testResult->sub = (object) [
'subV' => '125',
];
$testResult->sub->subO = (object) [
'subOV' => '125',
];
$testResult->sub->subList = ['0', '1'];
$this->assertEquals($testResult, Util::arrayToObject($testArr));
}

View File

@@ -0,0 +1,5 @@
{
"mandatoryFiles": [
"bin/command"
]
}

View File

@@ -0,0 +1,14 @@
{
"mandatoryFiles": [
"application/Espo/Core/Authentication/LDAP/Client.php",
"application/Espo/Core/Authentication/LDAP/ClientFactory.php",
"application/Espo/Core/Authentication/LDAP/Utils.php"
],
"manifest": {
"delete": [
"application/Espo/Core/Authentication/Ldap/Client.php",
"application/Espo/Core/Authentication/Ldap/ClientFactory.php",
"application/Espo/Core/Authentication/Ldap/Utils.php"
]
}
}

View File

@@ -1,4 +1,7 @@
{
"mandatoryFiles": [
"bin/command"
],
"manifest": {
"delete": [
"client/css/espo/espo-vertical.css",

View File

@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="RequestBlocking1" stopProcessing="true">
<match url="^/?data/config\.php$" />
<action type="CustomResponse" statusCode="403" statusReason="Forbidden: Access is denied." />
</rule>
<rule name="RequestBlocking2" stopProcessing="true">
<match url="^/?data/logs/" />
<action type="CustomResponse" statusCode="403" statusReason="Forbidden: Access is denied." />
</rule>
<rule name="RequestBlocking3" stopProcessing="true">
<match url="^/?data/cache/" />
<action type="CustomResponse" statusCode="403" statusReason="Forbidden: Access is denied." />
</rule>
<rule name="RequestBlocking4" stopProcessing="true">
<match url="^/?data/upload/" />
<action type="CustomResponse" statusCode="403" statusReason="Forbidden: Access is denied." />
</rule>
<rule name="RequestBlocking5" stopProcessing="true">
<match url="^/?application/" />
<action type="CustomResponse" statusCode="403" statusReason="Forbidden: Access is denied." />
</rule>
<rule name="RequestBlocking6" stopProcessing="true">
<match url="^/?custom/" />
<action type="CustomResponse" statusCode="403" statusReason="Forbidden: Access is denied." />
</rule>
<rule name="RequestBlocking7" stopProcessing="true">
<match url="^/?vendor/" />
<action type="CustomResponse" statusCode="403" statusReason="Forbidden: Access is denied." />
</rule>
</rules>
</rewrite>
<defaultDocument>
<files>
<clear />
<add value="index.php" />
<add value="index.html" />
</files>
</defaultDocument>
<staticContent>
<mimeMap fileExtension=".tpl" mimeType="text/plain" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
</staticContent>
</system.webServer>
</configuration>