app/Controller/Headers.php
<?php
/**
* Headers controller file.
*
* @package Controller
*
* @copyright YetiForce S.A.
* @license YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
* @author Mariusz Krzaczkowski <m.krzaczkowski@yetiforce.com>
* @author Radosław Skrzypczak <r.skrzypczak@yetiforce.com>
*/
namespace App\Controller;
/**
* Headers controller class.
*/
class Headers
{
/** Default CSP img-src */
/** @todo remove one addres */
private const CSP_IMG = ['https://api.yetiforce.eu'];
/**
* Default CSP header values.
*
* @var string[]
*/
public $csp = [
'default-src' => '\'self\' blob:',
'img-src' => '\'self\' data:',
'font-src' => '\'self\' data:',
'script-src' => '\'self\' \'unsafe-inline\' blob:',
'form-action' => '\'self\'',
'frame-ancestors' => '\'self\'',
'frame-src' => '\'self\' mailto: tel:',
'style-src' => '\'self\' \'unsafe-inline\'',
'connect-src' => '\'self\'',
];
/**
* Headers instance..
*
* @var self
*/
public static $instance;
/**
* Default header values.
*
* @var string[]
*/
protected $headers = [
'Access-Control-Allow-Methods' => 'GET, POST',
'Access-Control-Allow-Origin' => '*',
'Expires' => '-',
'Last-Modified' => '-',
'Pragma' => 'no-cache',
'Cache-Control' => 'private, no-cache, no-store, must-revalidate, post-check=0, pre-check=0',
'Content-Type' => 'text/html; charset=UTF-8',
'Referrer-Policy' => 'no-referrer',
'Expect-Ct' => 'enforce; max-age=3600',
'X-Frame-Options' => 'sameorigin',
'X-Xss-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff',
'X-Robots-Tag' => 'none',
'X-Permitted-Cross-Domain-Policies' => 'none',
];
/**
* Headers to delete.
*
* @var string[]
*/
protected $headersToDelete = ['X-Powered-By', 'Server'];
/**
* Construct, loads default headers depending on the browser and environment.
*/
public function __construct()
{
$browser = \App\RequestUtil::getBrowserInfo();
$this->headers['Expires'] = gmdate('D, d M Y H:i:s') . ' GMT';
$this->headers['Last-Modified'] = gmdate('D, d M Y H:i:s') . ' GMT';
if ($browser->ie) {
$this->headers['X-Ua-Compatible'] = 'IE=11,edge';
if ($browser->https) {
$this->headers['Pragma'] = 'private';
$this->headers['Cache-Control'] = 'private, must-revalidate';
}
}
if ($browser->https) {
$this->headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload';
}
if (\App\Config::security('cspHeaderActive')) {
$this->loadCsp();
}
if ($keys = \App\Config::security('hpkpKeysHeader')) {
$this->headers['Public-Key-Pins'] = 'pin-sha256="' . implode('"; pin-sha256="', $keys) . '"; max-age=10000;';
}
}
/**
* Get headers instance.
*
* @return \self
*/
public static function getInstance()
{
if (isset(self::$instance)) {
return self::$instance;
}
return self::$instance = new self();
}
/**
* Set header.
*
* @param string $key
* @param string $value
*/
public function setHeader(string $key, string $value)
{
$this->headers[$key] = $value;
}
/**
* Send headers.
*
* @return void
*/
public function send()
{
if (headers_sent()) {
return;
}
foreach ($this->getHeaders() as $value) {
header($value);
}
foreach ($this->headersToDelete as $name) {
header_remove($name);
}
}
/**
* Get headers string.
*
* @return string[]
*/
public function getHeaders(): array
{
if (\App\Config::security('cspHeaderActive')) {
$this->headers['Content-Security-Policy'] = $this->getCspHeader();
}
$return = [];
foreach ($this->headers as $name => $value) {
$return[] = "$name: $value";
}
return $return;
}
/**
* Load CSP directive.
*
* @return void
*/
public function loadCsp()
{
if (\Config\Security::$generallyAllowedDomains) {
$this->csp['default-src'] .= ' ' . implode(' ', \Config\Security::$generallyAllowedDomains);
}
if (self::CSP_IMG) {
$this->csp['img-src'] .= ' ' . implode(' ', self::CSP_IMG);
}
if (\Config\Security::$allowedImageDomains) {
$this->csp['img-src'] .= ' ' . implode(' ', \Config\Security::$allowedImageDomains);
}
if (\Config\Security::$allowedScriptDomains) {
$this->csp['script-src'] .= ' ' . implode(' ', \Config\Security::$allowedScriptDomains);
}
if (\Config\Security::$allowedFormDomains) {
$this->csp['form-action'] .= ' ' . implode(' ', \Config\Security::$allowedFormDomains);
}
if (\Config\Security::$allowedFrameDomains) {
$this->csp['frame-ancestors'] .= ' ' . implode(' ', \Config\Security::$allowedFrameDomains);
}
if (\Config\Security::$allowedConnectDomains) {
$this->csp['connect-src'] .= ' ' . implode(' ', \Config\Security::$allowedConnectDomains);
}
if (\Config\Security::$allowedDomainsLoadInFrame) {
$this->csp['frame-src'] .= ' ' . implode(' ', \Config\Security::$allowedDomainsLoadInFrame);
}
}
/**
* Get CSP headers string.
*
* @return string
*/
public function getCspHeader(): string
{
$scp = '';
foreach ($this->csp as $key => $value) {
$scp .= "$key $value; ";
}
return $scp;
}
/**
* Generate Content Security Policy token.
*
* @return void
*/
public static function generateCspToken(): void
{
\App\Session::set('CSP_TOKEN', hash('sha256', \App\Encryption::generatePassword(10)));
}
}