1<?php
2/**
3 * DokuWiki Content Security Policy (CSP) plugin
4 *
5 * Configure via config manager
6 *
7 * host-expr examples: http://*.foo.com, mail.foo.com:443, https://store.foo.com
8 * Besides FQDNs there are some keywords which are allowed 'self', 'none' or data:-URIs
9 * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
10 *
11 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
12 * @author     Matthias Schulte <post@lupo49.de>
13 * @link       https://www.dokuwiki.org/plugin:cspheader
14 */
15class action_plugin_cspheader extends DokuWiki_Action_Plugin
16{
17    /**
18     * CSP HTTP Header
19     */
20    const CSP_HEADER = 'Content-Security-Policy:';
21
22    /**
23     * @var array CSP directives names
24     * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives
25     */
26    const DIRECTIVES = [
27        'base-uri',
28        //'block-all-mixed-content', // this is a yes/no field and should be handled separately
29        'child-src',
30        'connect-src',
31        'default-src',
32        'font-src',
33        'form-action',
34        'frame-ancestors',
35        'frame-src',
36        'img-src',
37        'manifest-src',
38        'media-src',
39        'navigate-to',
40        'object-src',
41        'plugin-types',
42        'prefetch-src',
43        //'referrer', // deprecated
44        //'report-to', // this one isn't widely supported and expects a more complicated setup, skip for now
45        'report-uri',
46        //'require-sri-for', // obsolete
47        'sandbox',
48        'script-src',
49        'script-src-attr',
50        'script-src-elem',
51        'style-src',
52        'style-src-attr',
53        'style-src-elem',
54        'trusted-types',
55        //'upgrade-insecure-requests', // this is a yes/no field and should be handled separately
56        'worker-src',
57    ];
58
59    public function register(Doku_Event_Handler $controller)
60    {
61        $controller->register_hook('ACTION_HEADERS_SEND', 'BEFORE', $this, 'handleHeadersSend');
62    }
63
64    /**
65     * Handler for the ACTION_HEADERS_SEND event
66     *
67     * @param Doku_Event $event
68     * @param $params
69     *
70     * @noinspection PhpUnused, PhpUnusedParameterInspection
71     */
72    public function handleHeadersSend(Doku_Event $event, $params)
73    {
74        $policies = [];
75        foreach (self::DIRECTIVES as $directive) {
76            $option = str_replace('-', '', $directive) . 'Value';
77            $values = $this->getConf($option);
78            $values = explode("\n", $values);
79            $values = array_map('trim', $values);
80            $values = array_unique($values);
81            $values = array_filter($values);
82            if (!count($values)) continue;
83
84            $policies[$directive] = join(' ', $values);
85        }
86
87        $cspheader = self::CSP_HEADER;
88        foreach ($policies as $directive => $value) {
89            $cspheader .= " $directive $value;";
90        }
91
92        // create nonce for inline scripts and replace placeholder in header
93        $nonce = bin2hex(random_bytes(16));
94        putenv("NONCE=$nonce");
95        $cspheader = str_replace('NONCE', $nonce, $cspheader);
96
97        array_push($event->data, $cspheader);
98    }
99}
100