Completed
Push — master ( c68630...fbafe1 )
by
unknown
20:48
created

ReferrerEnforcer::resolveAbsoluteWebPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Core\Http\Security;
19
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use TYPO3\CMS\Core\Http\HtmlResponse;
23
use TYPO3\CMS\Core\Http\NormalizedParams;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
use TYPO3\CMS\Core\Utility\PathUtility;
26
27
/**
28
 * @internal
29
 */
30
class ReferrerEnforcer
31
{
32
    private const TYPE_REFERRER_EMPTY = 1;
33
    private const TYPE_REFERRER_SAME_SITE = 2;
34
    private const TYPE_REFERRER_SAME_ORIGIN = 4;
35
36
    /**
37
     * @var ServerRequestInterface
38
     */
39
    protected $request;
40
41
    /**
42
     * @var string
43
     */
44
    protected $requestHost;
45
46
    /**
47
     * @var string
48
     */
49
    protected $requestDir;
50
51
    public function __construct(ServerRequestInterface $request)
52
    {
53
        $this->request = $request;
54
        $this->requestHost = rtrim($this->resolveRequestHost($request), '/') . '/';
55
        $this->requestDir = $this->resolveRequestDir($request);
56
    }
57
58
    public function handle(array $options = null): ?ResponseInterface
59
    {
60
        $referrerType = $this->resolveReferrerType();
61
        // valid referrer, no more actions required
62
        if ($referrerType & self::TYPE_REFERRER_SAME_ORIGIN) {
63
            return null;
64
        }
65
        $flags = $options['flags'] ?? [];
66
        $expiration = $options['expiration'] ?? 5;
67
        // referrer is missing and route requested to refresh
68
        // (created HTML refresh to enforce having referrer)
69
        if (($this->request->getQueryParams()['referrer-refresh'] ?? 0) <= time()
70
            && (
71
                in_array('refresh-always', $flags, true)
72
                || ($referrerType & self::TYPE_REFERRER_EMPTY && in_array('refresh-empty', $flags, true))
73
                || ($referrerType & self::TYPE_REFERRER_SAME_SITE && in_array('refresh-same-site', $flags, true))
74
            )
75
        ) {
76
            $refreshUri = $this->request->getUri();
77
            $refreshUri = $refreshUri->withQuery(
78
                $refreshUri->getQuery() . '&referrer-refresh=' . (time() + $expiration)
79
            );
80
            $scriptUri = $this->resolveAbsoluteWebPath(
81
                'EXT:core/Resources/Public/JavaScript/ReferrerRefresh.js'
82
            );
83
            // simulating navigate event by clicking anchor link
84
            // since meta-refresh won't change `document.referrer` in e.g. Firefox
85
            return new HtmlResponse(sprintf(
86
                '<html>'
87
                . '<head><link rel="icon" href="data:image/svg+xml,"></head>'
88
                . '<body><a href="%1$s" id="referrer-refresh">&nbsp;</a>'
89
                . '<script src="%2$s"></script></body>'
90
                . '</html>',
91
                htmlspecialchars((string)$refreshUri),
92
                htmlspecialchars($scriptUri)
93
            ));
94
        }
95
        $subject = $options['subject'] ?? '';
96
        if ($referrerType & self::TYPE_REFERRER_EMPTY) {
97
            // still empty referrer or invalid referrer, deny route invocation
98
            throw new MissingReferrerException(
99
                sprintf('Missing referrer%s', $subject !== '' ? ' for ' . $subject : ''),
100
                1588095935
101
            );
102
        }
103
        // referrer is given, but does not match current base URL
104
        throw new InvalidReferrerException(
105
            sprintf('Invalid referrer%s', $subject !== '' ? ' for ' . $subject : ''),
106
            1588095936
107
        );
108
    }
109
110
    protected function resolveAbsoluteWebPath(string $target): string
111
    {
112
        return PathUtility::getAbsoluteWebPath(
113
            GeneralUtility::getFileAbsFileName($target)
114
        );
115
    }
116
117
    protected function resolveReferrerType(): int
118
    {
119
        $referrer = $this->request->getServerParams()['HTTP_REFERER'] ?? '';
120
        if ($referrer === '') {
121
            return self::TYPE_REFERRER_EMPTY;
122
        }
123
        if (strpos($referrer, $this->requestDir) === 0) {
124
            // same-origin implies same-site
125
            return self::TYPE_REFERRER_SAME_ORIGIN | self::TYPE_REFERRER_SAME_SITE;
126
        }
127
        if (strpos($referrer, $this->requestHost) === 0) {
128
            return self::TYPE_REFERRER_SAME_SITE;
129
        }
130
        return 0;
131
    }
132
133
    protected function resolveRequestHost(ServerRequestInterface $request): string
134
    {
135
        $normalizedParams = $request->getAttribute('normalizedParams');
136
        if ($normalizedParams instanceof NormalizedParams) {
137
            return $normalizedParams->getRequestHost();
138
        }
139
        return GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST');
140
    }
141
142
    protected function resolveRequestDir(ServerRequestInterface $request): string
143
    {
144
        $normalizedParams = $request->getAttribute('normalizedParams');
145
        if ($normalizedParams instanceof NormalizedParams) {
146
            return $normalizedParams->getRequestDir();
147
        }
148
        return GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR');
149
    }
150
}
151