client headers

This commit is contained in:
Yuri Kuznetsov
2022-08-09 14:30:01 +03:00
parent b707bf70b8
commit 95ce8645f2
8 changed files with 164 additions and 20 deletions

View File

@@ -29,6 +29,8 @@
namespace Espo\Core\Utils\Client;
use Espo\Core\Api\Response;
use Espo\Core\Utils\Client\ActionRenderer\Params;
use Espo\Core\Utils\Json;
use Espo\Core\Utils\ClientManager;
@@ -45,6 +47,18 @@ class ActionRenderer
}
/**
* Writes to a body.
*/
public function write(Response $response, Params $params): void
{
$body = $this->render($params->getController(), $params->getAction(), $params->getData());
$this->clientManager->writeHeaders($response);
$response->writeBody($body);
}
/**
* @deprecated Use`write`.
* @param ?array<string,mixed> $data
*/
public function render(string $controller, string $action, ?array $data = null): string

View File

@@ -0,0 +1,66 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2022 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.
************************************************************************/
namespace Espo\Core\Utils\Client\ActionRenderer;
class Params
{
private string $controller;
private string $action;
/** @var ?array<string,mixed> */
private ?array $data;
/**
* @param ?array<string,mixed> $data
*/
public function __construct(string $controller, string $action, ?array $data = null)
{
$this->controller = $controller;
$this->action = $action;
$this->data = $data;
}
public function getController(): string
{
return $this->controller;
}
public function getAction(): string
{
return $this->action;
}
/**
* @return ?array<string,mixed>
*/
public function getData(): ?array
{
return $this->data;
}
}

View File

@@ -30,12 +30,17 @@
namespace Espo\Core\Utils;
use Espo\Core\{
Api\Response,
Api\ResponseWrapper,
Utils\File\Manager as FileManager,
Utils\Client\DevModeJsFileListProvider,
Utils\Module,
Utils\Json,
};
use Slim\Psr7\Response as Psr7Response;
use Slim\ResponseEmitter;
/**
* Renders the main HTML page.
*/
@@ -61,6 +66,8 @@ class ClientManager
private Module $module;
private string $nonce;
private const APP_DESCRIPTION = "EspoCRM - Open Source CRM application.";
public function __construct(
@@ -77,6 +84,8 @@ class ClientManager
$this->fileManager = $fileManager;
$this->devModeJsFileListProvider = $devModeJsFileListProvider;
$this->module = $module;
$this->nonce = Util::generateKey();
}
public function setBasePath(string $basePath): void
@@ -98,12 +107,65 @@ class ClientManager
return $this->config->get('cacheTimestamp', 0);
}
public function writeHeaders(Response $response): void
{
$this->writeContentSecurityPolicyHeader($response);
$this->writeStrictTransportSecurityHeader($response);
/** @var array<string,?string> $headers */
$headers = $this->config->get('clientHttpHeaders') ?? [];
foreach ($headers as $name => $value) {
if ($value === null) {
continue;
}
$response->setHeader($name, $value);
}
}
/**
* @todo Move to a separate class.
*/
private function writeContentSecurityPolicyHeader(Response $response): void
{
if ($this->config->get('clientCspDisabled')) {
return;
}
$scriptSrc = "script-src 'self' 'nonce-{$this->nonce}' 'unsafe-eval'";
$scriptSourceList = $this->config->get('clientCspScriptSourceList') ?? [];
foreach ($scriptSourceList as $src) {
$scriptSrc .= ' ' . $src;
}
$response->setHeader('Content-Security-Policy', $scriptSrc);
}
private function writeStrictTransportSecurityHeader(Response $response)
{
$siteUrl =$this->config->get('siteUrl') ?? '';
if (strpos($siteUrl, 'https://') === 0) {
$response->setHeader('Strict-Transport-Security', 'max-age=10368000');
}
}
/**
* @param array<string,mixed> $vars
*/
public function display(?string $runScript = null, ?string $htmlFilePath = null, array $vars = []): void
{
echo $this->render($runScript, $htmlFilePath, $vars);
$body = $this->render($runScript, $htmlFilePath, $vars);
$response = new ResponseWrapper(new Psr7Response());
$this->writeHeaders($response);
$response->writeBody($body);
(new ResponseEmitter())->emit($response->getResponse());
}
/**
@@ -205,6 +267,7 @@ class ClientManager
'libsConfigPath' => $this->libsConfigPath,
'internalModuleList' => Json::encode($internalModuleList),
'applicationDescription' => $this->config->get('applicationDescription') ?? self::APP_DESCRIPTION,
'nonce' => $this->nonce,
];
$html = $this->fileManager->getContents($htmlFilePath);

View File

@@ -92,8 +92,8 @@ class ChangePassword implements EntryPoint
'notFound' => !$passwordChangeRequest,
];
$html = $this->actionRenderer->render('controllers/password-change-request', 'passwordChange', $options);
$params = new ActionRenderer\Params('controllers/password-change-request', 'passwordChange', $options);
$response->writeBody($html);
$this->actionRenderer->write($response, $params);
}
}

View File

@@ -36,23 +36,21 @@ use Espo\Core\{
Exceptions\Error,
EntryPoint\EntryPoint,
EntryPoint\Traits\NoAuth,
Utils\ClientManager,
Utils\Client\ActionRenderer,
Api\Request,
Api\Response,
};
Api\Response};
class ConfirmOptIn implements EntryPoint
{
use NoAuth;
private $clientManager;
private Service $service;
private ActionRenderer $actionRenderer;
private $service;
public function __construct(ClientManager $clientManager, Service $service)
public function __construct(Service $service, ActionRenderer $actionRenderer)
{
$this->clientManager = $clientManager;
$this->service = $service;
$this->actionRenderer = $actionRenderer;
}
/**
@@ -76,14 +74,8 @@ class ConfirmOptIn implements EntryPoint
$action = 'optInConfirmationSuccess';
}
$runScript = "
require('controllers/lead-capture-opt-in-confirmation', Controller => {
var controller = new Controller(app.baseController.params, app.getControllerInjection());
controller.masterView = app.masterView;
controller.doAction('{$action}', " . json_encode($data) . ");
});
";
$params = new ActionRenderer\Params('controllers/lead-capture-opt-in-confirmation', $action, $data);
$this->clientManager->display($runScript);
$this->actionRenderer->write($response, $params);
}
}

View File

@@ -215,5 +215,11 @@ return [
'passwordGenerateLength' => 10,
'massActionIdleCountThreshold' => 100,
'exportIdleCountThreshold' => 1000,
'clientHttpHeaders' => [
'X-Frame-Options' => 'SAMEORIGIN',
'X-Content-Type-Options' => 'nosniff',
],
'clientCspDisabled' => false,
'clientCspScriptSourceList' => [],
'isInstalled' => false,
];

View File

@@ -98,6 +98,9 @@ return [
'webSocketMessager',
'actualDatabaseType',
'actualDatabaseVersion',
'clientHttpHeaders',
'clientCspDisabled',
'clientCspScriptSourceList',
],
'adminItems' => [
'devMode',

View File

@@ -12,7 +12,7 @@
<link rel="shortcut icon" sizes="196x196" href="{{basePath}}{{favicon196Path}}">
<link rel="icon" href="{{basePath}}{{faviconPath}}" type="image/x-icon">
<link rel="shortcut icon" href="{{basePath}}{{faviconPath}}" type="image/x-icon">
<script type="text/javascript">
<script type="text/javascript" nonce="{{nonce}}">
window.addEventListener('DOMContentLoaded', () => {
Espo.loader.setCacheTimestamp({{loaderCacheTimestamp}});
Espo.loader.setBasePath('{{basePath}}');