Completed
Push — master ( 128ddb...c5783a )
by
unknown
14s queued 10s
created

CacheHeadersCheck   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 183
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 78
dl 0
loc 183
rs 10
c 0
b 0
f 0
wmc 14

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getMessage() 0 35 5
A check() 0 30 3
A checkEtag() 0 15 2
A checkCacheControl() 0 31 3
1
<?php
2
3
namespace SilverStripe\EnvironmentCheck\Checks;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\ORM\ValidationResult;
8
use Psr\Http\Message\ResponseInterface;
9
use SilverStripe\Core\Config\Configurable;
10
use SilverStripe\EnvironmentCheck\Traits\Fetcher;
11
use SilverStripe\EnvironmentCheck\EnvironmentCheck;
12
13
/**
14
 * Check cache headers for any response, can specify directives that must be included and
15
 * also must be excluded from Cache-Control headers in response. Also checks for
16
 * existence of ETag.
17
 *
18
 * @example SilverStripe\EnvironmentCheck\Checks\CacheHeadersCheck("/",["must-revalidate", "max-age=120"],["no-store"])
19
 * @package environmentcheck
20
 */
21
class CacheHeadersCheck implements EnvironmentCheck
22
{
23
    use Fetcher;
24
25
    /**
26
     * Settings that must be included in the Cache-Control header
27
     *
28
     * @var array
29
     */
30
    protected $mustInclude = [];
31
32
    /**
33
     * Settings that must be excluded in the Cache-Control header
34
     *
35
     * @var array
36
     */
37
    protected $mustExclude = [];
38
39
    /**
40
     * Result to keep track of status and messages for all checks, reuses
41
     * ValidationResult for convenience.
42
     *
43
     * @var ValidationResult
44
     */
45
    protected $result;
46
47
    /**
48
     * Set up with URL, arrays of header settings to check.
49
     *
50
     * @param string $url
51
     * @param array $mustInclude Settings that must be included in Cache-Control
52
     * @param array $mustExclude Settings that must be excluded in Cache-Control
53
     */
54
    public function __construct($url = '', $mustInclude = [], $mustExclude = [])
55
    {
56
        $this->setURL($url);
57
        $this->mustInclude = $mustInclude;
58
        $this->mustExclude = $mustExclude;
59
    }
60
61
    /**
62
     * Check that correct caching headers are present.
63
     *
64
     * @return void
65
     */
66
    public function check()
67
    {
68
        // Using a validation result to capture messages
69
        $this->result = new ValidationResult();
70
71
        $response = $this->client->get($this->getURL());
72
        $fullURL = $this->getURL();
73
        if ($response === null) {
74
            return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array(SilverStrip... failed for '.$fullURL) returns the type array<integer,integer|string> which is incompatible with the documented return type void.
Loading history...
75
                EnvironmentCheck::ERROR,
76
                "Cache headers check request failed for $fullURL",
77
            ];
78
        }
79
80
        //Check that Etag exists
81
        $this->checkEtag($response);
82
83
        // Check Cache-Control settings
84
        $this->checkCacheControl($response);
85
86
        if ($this->result->isValid()) {
87
            return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array(SilverStrip...K, $this->getMessage()) returns the type array<integer,integer|string> which is incompatible with the documented return type void.
Loading history...
88
                EnvironmentCheck::OK,
89
                $this->getMessage(),
90
            ];
91
        } else {
92
            // @todo Ability to return a warning
93
            return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array(SilverStrip...R, $this->getMessage()) returns the type array<integer,integer|string> which is incompatible with the documented return type void.
Loading history...
94
                EnvironmentCheck::ERROR,
95
                $this->getMessage(),
96
            ];
97
        }
98
    }
99
100
    /**
101
     * Collate messages from ValidationResult so that it is clear which parts
102
     * of the check passed and which failed.
103
     *
104
     * @return string
105
     */
106
    private function getMessage()
107
    {
108
        $ret = '';
109
        // Filter good messages
110
        $goodTypes = [ValidationResult::TYPE_GOOD, ValidationResult::TYPE_INFO];
111
        $good = array_filter(
112
            $this->result->getMessages(),
113
            function ($val, $key) use ($goodTypes) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

113
            function ($val, /** @scrutinizer ignore-unused */ $key) use ($goodTypes) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
114
                if (in_array($val['messageType'], $goodTypes)) {
115
                    return true;
116
                }
117
                return false;
118
            },
119
            ARRAY_FILTER_USE_BOTH
120
        );
121
        if (!empty($good)) {
122
            $ret .= "GOOD: " . implode('; ', array_column($good, 'message')) . " ";
123
        }
124
125
        // Filter bad messages
126
        $badTypes = [ValidationResult::TYPE_ERROR, ValidationResult::TYPE_WARNING];
127
        $bad = array_filter(
128
            $this->result->getMessages(),
129
            function ($val, $key) use ($badTypes) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

129
            function ($val, /** @scrutinizer ignore-unused */ $key) use ($badTypes) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
130
                if (in_array($val['messageType'], $badTypes)) {
131
                    return true;
132
                }
133
                return false;
134
            },
135
            ARRAY_FILTER_USE_BOTH
136
        );
137
        if (!empty($bad)) {
138
            $ret .= "BAD: " . implode('; ', array_column($bad, 'message'));
139
        }
140
        return $ret;
141
    }
142
143
    /**
144
     * Check that ETag header exists
145
     *
146
     * @param ResponseInterface $response
147
     * @return void
148
     */
149
    private function checkEtag(ResponseInterface $response)
150
    {
151
        $eTag = $response->getHeaderLine('ETag');
152
        $fullURL = Controller::join_links(Director::absoluteBaseURL(), $this->url);
153
154
        if ($eTag) {
155
            $this->result->addMessage(
156
                "$fullURL includes an Etag header in response",
157
                ValidationResult::TYPE_GOOD
158
            );
159
            return;
160
        }
161
        $this->result->addError(
162
            "$fullURL is missing an Etag header",
163
            ValidationResult::TYPE_WARNING
164
        );
165
    }
166
167
    /**
168
     * Check that the correct header settings are either included or excluded.
169
     *
170
     * @param ResponseInterface $response
171
     * @return void
172
     */
173
    private function checkCacheControl(ResponseInterface $response)
174
    {
175
        $cacheControl = $response->getHeaderLine('Cache-Control');
176
        $vals = array_map('trim', explode(',', $cacheControl));
177
        $fullURL = Controller::join_links(Director::absoluteBaseURL(), $this->url);
178
179
        // All entries from must contain should be present
180
        if ($this->mustInclude == array_intersect($this->mustInclude, $vals)) {
181
            $matched = implode(",", $this->mustInclude);
182
            $this->result->addMessage(
183
                "$fullURL includes all settings: {$matched}",
184
                ValidationResult::TYPE_GOOD
185
            );
186
        } else {
187
            $missing = implode(",", array_diff($this->mustInclude, $vals));
188
            $this->result->addError(
189
                "$fullURL is excluding some settings: {$missing}"
190
            );
191
        }
192
193
        // All entries from must exclude should not be present
194
        if (empty(array_intersect($this->mustExclude, $vals))) {
195
            $missing = implode(",", $this->mustExclude);
196
            $this->result->addMessage(
197
                "$fullURL excludes all settings: {$missing}",
198
                ValidationResult::TYPE_GOOD
199
            );
200
        } else {
201
            $matched = implode(",", array_intersect($this->mustExclude, $vals));
202
            $this->result->addError(
203
                "$fullURL is including some settings: {$matched}"
204
            );
205
        }
206
    }
207
}
208