Completed
Pull Request — master (#60)
by
unknown
05:01
created

CacheHeadersCheck::getMessage()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 9.0328
c 0
b 0
f 0
cc 5
nc 4
nop 0
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 Configurable;
24
    use Fetcher;
25
26
    /**
27
     * URL to check
28
     *
29
     * @var string
30
     */
31
    protected $url;
32
33
    /**
34
     * Settings that must be included in the Cache-Control header
35
     *
36
     * @var array
37
     */
38
    protected $mustInclude = [];
39
40
    /**
41
     * Settings that must be excluded in the Cache-Control header
42
     *
43
     * @var array
44
     */
45
    protected $mustExclude = [];
46
47
    /**
48
     * Result to keep track of status and messages for all checks, reuses
49
     * ValidationResult for convenience.
50
     *
51
     * @var ValidationResult
52
     */
53
    protected $result;
54
55
    /**
56
     * Set up with URL, arrays of header settings to check.
57
     *
58
     * @param string $url
59
     * @param array $mustInclude Settings that must be included in Cache-Control
60
     * @param array $mustExclude Settings that must be excluded in Cache-Control
61
     */
62
    public function __construct($url = '', $mustInclude = [], $mustExclude = [])
63
    {
64
        $this->url = $url;
65
        $this->mustInclude = $mustInclude;
66
        $this->mustExclude = $mustExclude;
67
68
        $this->clientConfig = [
69
            'base_uri' => Director::absoluteBaseURL(),
70
            'timeout' => 10.0,
71
        ];
72
73
        // Using a validation result to capture messages
74
        $this->result = new ValidationResult();
75
    }
76
77
    /**
78
     * Check that correct caching headers are present.
79
     *
80
     * @return void
81
     */
82
    public function check()
83
    {
84
        $response = $this->fetchResponse($this->url);
85
        $fullURL = Controller::join_links(Director::absoluteBaseURL(), $this->url);
86
        if ($response === null) {
87
            return [
88
                EnvironmentCheck::ERROR,
89
                "Cache headers check request failed for $fullURL",
90
            ];
91
        }
92
93
        //Check that Etag exists
94
        $this->checkEtag($response);
95
96
        // Check Cache-Control settings
97
        $this->checkCacheControl($response);
98
99
        if ($this->result->isValid()) {
100
            return [
101
                EnvironmentCheck::OK,
102
                $this->getMessage(),
103
            ];
104
        } else {
105
            // @todo Ability to return a warning
106
            return [
107
                EnvironmentCheck::ERROR,
108
                $this->getMessage(),
109
            ];
110
        }
111
    }
112
113
    /**
114
     * Collate messages from ValidationResult so that it is clear which parts
115
     * of the check passed and which failed.
116
     *
117
     * @return string
118
     */
119
    private function getMessage(): string
120
    {
121
        $ret = '';
122
        // Filter good messages
123
        $goodTypes = [ValidationResult::TYPE_GOOD, ValidationResult::TYPE_INFO];
124
        $good = array_filter(
125
            $this->result->getMessages(),
126
            function ($val, $key) use ($goodTypes) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

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

Loading history...
127
                if (in_array($val['messageType'], $goodTypes)) {
128
                    return true;
129
                }
130
                return false;
131
            },
132
            ARRAY_FILTER_USE_BOTH
133
        );
134
        if (!empty($good)) {
135
            $ret .= "GOOD: " . implode('; ', array_column($good, 'message')) . " ";
136
        }
137
138
        // Filter bad messages
139
        $badTypes = [ValidationResult::TYPE_ERROR, ValidationResult::TYPE_WARNING];
140
        $bad = array_filter(
141
            $this->result->getMessages(),
142
            function ($val, $key) use ($badTypes) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

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

Loading history...
143
                if (in_array($val['messageType'], $badTypes)) {
144
                    return true;
145
                }
146
                return false;
147
            },
148
            ARRAY_FILTER_USE_BOTH
149
        );
150
        if (!empty($bad)) {
151
            $ret .= "BAD: " . implode('; ', array_column($bad, 'message'));
152
        }
153
        return $ret;
154
    }
155
156
    /**
157
     * Check that ETag header exists
158
     *
159
     * @param ResponseInterface $response
160
     * @return void
161
     */
162
    private function checkEtag(ResponseInterface $response): void
163
    {
164
        $eTag = $response->getHeaderLine('ETag');
165
        $fullURL = Controller::join_links(Director::absoluteBaseURL(), $this->url);
166
167
        if ($eTag) {
168
            $this->result->addMessage(
169
                "$fullURL includes an Etag header in response",
170
                ValidationResult::TYPE_GOOD
171
            );
172
            return;
173
        }
174
        $this->result->addError(
175
            "$fullURL is missing an Etag header",
176
            ValidationResult::TYPE_WARNING
177
        );
178
    }
179
180
    /**
181
     * Check that the correct header settings are either included or excluded.
182
     *
183
     * @param ResponseInterface $response
184
     * @return void
185
     */
186
    private function checkCacheControl(ResponseInterface $response): void
187
    {
188
        $cacheControl = $response->getHeaderLine('Cache-Control');
189
        $vals = array_map('trim', explode(',', $cacheControl));
190
        $fullURL = Controller::join_links(Director::absoluteBaseURL(), $this->url);
191
192
        // All entries from must contain should be present
193 View Code Duplication
        if ($this->mustInclude == array_intersect($this->mustInclude, $vals)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
194
            $matched = implode(",", $this->mustInclude);
195
            $this->result->addMessage(
196
                "$fullURL includes all settings: {$matched}",
197
                ValidationResult::TYPE_GOOD
198
            );
199
        } else {
200
            $missing = implode(",", array_diff($this->mustInclude, $vals));
201
            $this->result->addError(
202
                "$fullURL is excluding some settings: {$missing}"
203
            );
204
        }
205
206
        // All entries from must exclude should not be present
207 View Code Duplication
        if (empty(array_intersect($this->mustExclude, $vals))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
208
            $missing = implode(",", $this->mustExclude);
209
            $this->result->addMessage(
210
                "$fullURL excludes all settings: {$missing}",
211
                ValidationResult::TYPE_GOOD
212
            );
213
        } else {
214
            $matched = implode(",", array_intersect($this->mustExclude, $vals));
215
            $this->result->addError(
216
                "$fullURL is including some settings: {$matched}"
217
            );
218
        }
219
    }
220
}
221