The Framework Code

class/framework/web/csp.php

File List

<?php
/**
 * A trait that implements the CSP handling for the Web class
 *
 * @author Lindsay Marshall <lindsay.marshall@ncl.ac.uk>
 * @copyright 2019-2021 Newcastle University
 * @package Framework
 */
    namespace Framework\Web;

    use \Config\Framework as FW;
/**
 * Adds functions for adding and removinng CSP
 */
    trait CSP
    {
/**
 * @var array<array<string>>  Holds values that need to be added to CSP headers.
 */
        private array $csp        = [];
/**
 * @var array<array<string>>   Holds values that need to be removed from CSP headers.
 */
        private array $nocsp      = [];
/**
 * @var array<string> which  CSP fields to check for hostnames
 */
        private static array $cspFields = ['css' => 'style-src', 'js' => 'script-src', 'font' => 'font-src', 'img' => 'img-src'];
/**
 * @var array<array<string>>   Default CSP settings
 */
        private static array $defaultCSP = [
            'default-src' => ["'self'"],
            'font-src' => ["'self'", '*.fontawesome.com'],
            'img-src' => ["'self'", "data:", "*.amuniversal.com"],
            'script-src' => ["'self'", "stackpath.bootstrapcdn.com", "cdnjs.cloudflare.com", "code.jquery.com"],
            'style-src' => ["'self'", "*.fontawesome.com", "stackpath.bootstrapcdn.com"],
        ];
/**
 * compute, save and return a hash for use in a CSP header
 *
 * @param string  $type    What the hash is for (script-src, css-src etc.)
 * @param string  $data    The data to be hashed
 *
 * @psalm-suppress PossiblyUnusedMethod
 */
        public function saveCSP(string $type, string $data) : string
        {
            $hash = \Framework\Support\Security::getinstance()->hash($data);
            $this->addCSP([$type => ["'".$hash."'"]]);
            return $hash;
        }
/**
 * Add an item for use in a CSP header - could be 'unsafe-inline', a domain or other stuff
 *
 * @param string|array<mixed>  $type    What the item is for (script-src, style-src etc.)
 * @param string               $host    The host to add
 */
        public function addCSP($type, string $host = '') : void
        {
            if (!is_array($type))
            {
                \assert($host !== '');
                $type = [$type => [$host]];
            }
            foreach ($type as $t => $h)
            {
                if (!\is_array($h))
                {
                    $h = [$h];
                }
                if (!isset($this->csp[$t]))
                {
                    $this->csp[$t] = $h;
                }
                else
                {
                    $this->csp[$t] = \array_merge($this->csp[$t], $h);
                }
            }
        }
/**
 * Remove an item from a CSP header - could be 'unsafe-inline', a domain or other stuff
 *
 * @param string|array<string>  $type    What the item is for (script-src, style-src etc.)
 * @param string        $host    The item to remove
 *
 * @psalm-suppress PossiblyUnusedMethod
 */
        public function removeCSP($type, string $host = '') : void
        {
            if (!\is_array($type))
            {
                \assert($host !== '');
                $type = [$type => $host];
            }
            foreach ($type as $t => $h)
            {
                if (isset($this->csp[$t]))
                {
                    $this->csp[$t] = \array_diff($this->csp[$t], \is_array($h) ? $h : [$h]);
                }
            }
        }
/**
 * Set up default CSP headers for a page
 *
 * There will be a basic set of default CSP permissions for the site to function,
 * but individual pages may wish to extend or restrict these.
 *
 * @psalm-suppress PossiblyUnusedMethod
 */
        public function setCSP() : void
        {
            $local = $this->context->local();
            if ($local->configval('usecsp'))
            {
                $csp = '';
                foreach ($this->csp as $key => $val)
                {
                    if (!empty($val))
                    {
                        $csp .= ' '.$key.' '.\implode(' ', $val).';';
                    }
                }
                if ($local->configval('reportcsp'))
                {
                    $edp = $local->base().'/cspreport/';
                    $csp .= ' report-uri '.$edp.';'; // This is deprecated but widely supported
                    $csp .= ' report-to csp-report;';
                    $this->addheader([
                        'Report-To' => 'Report-To: { "group": "csp-report", "max-age": 10886400, "endpoints": [ { "url": "'.$edp.'" } ] }',
                    ]);
                }
                $this->addheader([
                    'Content-Security-Policy'   => $csp,
                ]);
            }
        }
/**
 * Initialise CSP
 *
 * If the data is in the database then use that, if not thensetup the table from Config::$defaultCSP
 *
 * @return void
 */
        public function initCSP() : void
        {
            $local = $this->context->local();
            if ($local->configval('usecsp'))
            {
                if (\Support\SiteInfo::tableExists(\Config\Framework::CSP))
                { // we have the table
                    $this->csp = [];
                    foreach (\R::findAll(\Config\Framework::CSP) as $csp)
                    {
                        $this->csp[$csp->type][] = $csp->host;
                    }
                }
                else
                { // copy the default set
                    $this->csp = self::$defaultCSP;
                    foreach ($this->csp as $type => $host)
                    { // now set up the database for future working...
                        foreach ($host as $h)
                        {
                            $bn = \R::dispense(\Config\Framework::CSP);
                            $bn->type = $type;
                            $bn->host = $h;
                            $bn->essential = 1;
                            \R::store($bn);
                        }
                    }
                }
            }
        }
/**
 * Get the CSP values
 */
        public function getCSP() : array
        {
            return $this->csp;
        }
/**
 * Check to see if we need to update the CSP data for a new host
 * Returns TRUE if source was added
 *
 * @param string $url       The url for the resource
 * @param string $type      js, css etc.
 * @param bool   $essential If TRUE this is essential to site functioning
 */
        public function checkCSP(string $url, string $type, bool $essential = TRUE) : bool
        {
            if (isset(self::$cspFields[$type]))
            {
                $host = \parse_url($url, PHP_URL_HOST);
                if ($host !== '' && \R::findOne(FW::CSP, 'type=? and host=?', [self::$cspFields[$type], $host]) === NULL)
                { // it might be hidden behind a pattern
                    $x = \explode('.', $host);
                    if (\count($x) >= 3)
                    {
                        $x[0] = '*';
                        $x = \implode('.', $x);
                        if (\R::findOne(FW::CSP, 'type=? and host=?', [self::$cspFields[$type], $x]) === NULL)
                        { // doesn't seem to be in there
                            $bn = \R::dispense(\Config\Framework::CSP);
                            $bn->type = self::$cspFields[$type];
                            $bn->host = $host;
                            $bn->essential = $essential ? 1 : 0;
                            \R::store($bn);
                            return TRUE;
                        }
                    }
                }
            }
            return FALSE;
        }
    }
?>