Passed
Pull Request — 4 (#8209)
by Ingo
09:07
created

CanonicalURLMiddleware   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 393
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 393
rs 6.96
c 0
b 0
f 0
wmc 53

22 Methods

Rating   Name   Duplication   Size   Complexity  
A throwRedirectIfNeeded() 0 9 3
A getForceBasicAuthToSSL() 0 8 2
B getRedirect() 0 30 8
A setRedirectType() 0 4 1
A getForceSSL() 0 3 1
A process() 0 18 5
A setForceSSLPatterns() 0 4 1
A setForceSSLDomain() 0 4 1
A getForceSSLPatterns() 0 3 2
A hasBasicAuthPrompt() 0 6 3
A setEnabledEnvs() 0 4 1
A redirectToScheme() 0 13 2
A getRedirectType() 0 3 1
A getForceWWW() 0 3 1
B isEnabled() 0 20 7
A setForceSSL() 0 4 1
A setForceBasicAuthToSSL() 0 4 1
A setForceWWW() 0 4 1
A getEnabledEnvs() 0 3 1
B requiresSSL() 0 28 6
A getForceSSLDomain() 0 3 1
A getOrValidateRequest() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like CanonicalURLMiddleware often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CanonicalURLMiddleware, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Control\Middleware;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Control\HTTP;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Control\HTTPResponse;
10
use SilverStripe\Control\HTTPResponse_Exception;
11
use SilverStripe\Core\CoreKernel;
12
use SilverStripe\Core\Injector\Injectable;
13
use SilverStripe\Core\Injector\Injector;
14
15
/**
16
 * Allows events to be registered and passed through middleware.
17
 * Useful for event registered prior to the beginning of a middleware chain.
18
 */
19
class CanonicalURLMiddleware implements HTTPMiddleware
20
{
21
    use Injectable;
22
23
    /**
24
     * Set if we should redirect to WWW
25
     *
26
     * @var bool
27
     */
28
    protected $forceWWW = false;
29
30
    /**
31
     * Set if we should force SSL
32
     *
33
     * @var bool
34
     */
35
    protected $forceSSL = false;
36
37
    /**
38
     * Set if we should automatically redirect basic auth requests to HTTPS. A null value (default) will
39
     * cause this property to return the value of the forceSSL property.
40
     *
41
     * @var bool|null
42
     */
43
    protected $forceBasicAuthToSSL = null;
44
45
    /**
46
     * Redirect type
47
     *
48
     * @var int
49
     */
50
    protected $redirectType = 301;
51
52
    /**
53
     * Environment variables this middleware is enabled in, or a fixed boolean flag to
54
     * apply to all environments. cli is disabled unless present here as `cli`, or set to true
55
     * to force enabled.
56
     *
57
     * @var array|bool
58
     */
59
    protected $enabledEnvs = [
60
        CoreKernel::LIVE
61
    ];
62
63
    /**
64
     * If forceSSL is enabled, this is the list of patterns that the url must match (at least one)
65
     *
66
     * @var array Array of regexps to match against relative url
67
     */
68
    protected $forceSSLPatterns = [];
69
70
    /**
71
     * SSL Domain to use
72
     *
73
     * @var string
74
     */
75
    protected $forceSSLDomain = null;
76
77
    /**
78
     * @return array
79
     */
80
    public function getForceSSLPatterns()
81
    {
82
        return $this->forceSSLPatterns ?: [];
83
    }
84
85
    /**
86
     * @param array $forceSSLPatterns
87
     * @return $this
88
     */
89
    public function setForceSSLPatterns($forceSSLPatterns)
90
    {
91
        $this->forceSSLPatterns = $forceSSLPatterns;
92
        return $this;
93
    }
94
95
    /**
96
     * @return string
97
     */
98
    public function getForceSSLDomain()
99
    {
100
        return $this->forceSSLDomain;
101
    }
102
103
    /**
104
     * @param string $forceSSLDomain
105
     * @return $this
106
     */
107
    public function setForceSSLDomain($forceSSLDomain)
108
    {
109
        $this->forceSSLDomain = $forceSSLDomain;
110
        return $this;
111
    }
112
113
    /**
114
     * @return bool
115
     */
116
    public function getForceWWW()
117
    {
118
        return $this->forceWWW;
119
    }
120
121
    /**
122
     * @param bool $forceWWW
123
     * @return $this
124
     */
125
    public function setForceWWW($forceWWW)
126
    {
127
        $this->forceWWW = $forceWWW;
128
        return $this;
129
    }
130
131
    /**
132
     * @return bool
133
     */
134
    public function getForceSSL()
135
    {
136
        return $this->forceSSL;
137
    }
138
139
    /**
140
     * @param bool $forceSSL
141
     * @return $this
142
     */
143
    public function setForceSSL($forceSSL)
144
    {
145
        $this->forceSSL = $forceSSL;
146
        return $this;
147
    }
148
149
    /**
150
     * @param bool|null $forceBasicAuth
151
     * @return $this
152
     */
153
    public function setForceBasicAuthToSSL($forceBasicAuth)
154
    {
155
        $this->forceBasicAuthToSSL = $forceBasicAuth;
156
        return $this;
157
    }
158
159
    /**
160
     * @return bool
161
     */
162
    public function getForceBasicAuthToSSL()
163
    {
164
        // Check if explicitly set
165
        if (isset($this->forceBasicAuthToSSL)) {
166
            return $this->forceBasicAuthToSSL;
167
        }
168
        // If not explicitly set, default to on if ForceSSL is on
169
        return $this->getForceSSL();
170
    }
171
172
    /**
173
     * Generate response for the given request
174
     *
175
     * @param HTTPRequest $request
176
     * @param callable $delegate
177
     * @return HTTPResponse
178
     */
179
    public function process(HTTPRequest $request, callable $delegate)
180
    {
181
        // Handle any redirects
182
        $redirect = $this->getRedirect($request);
183
        if ($redirect) {
184
            return $redirect;
185
        }
186
187
        /** @var HTTPResponse $response */
188
        $response = $delegate($request);
189
        if ($this->hasBasicAuthPrompt($response)
190
            && $request->getScheme() !== 'https'
191
            && $this->getForceBasicAuthToSSL()
192
        ) {
193
            return $this->redirectToScheme($request, 'https');
194
        }
195
196
        return $response;
197
    }
198
199
    /**
200
     * Given request object determine if we should redirect.
201
     *
202
     * @param HTTPRequest $request Pre-validated request object
203
     * @return HTTPResponse|null If a redirect is needed return the response
204
     */
205
    protected function getRedirect(HTTPRequest $request)
206
    {
207
        // Check global disable
208
        if (!$this->isEnabled()) {
209
            return null;
210
        }
211
212
        // Get properties of current request
213
        $host = $request->getHost();
214
        $scheme = $request->getScheme();
215
216
        // Check https
217
        if ($this->requiresSSL($request)) {
218
            $scheme = 'https';
219
220
            // Promote ssl domain if configured
221
            $host = $this->getForceSSLDomain() ?: $host;
222
        }
223
224
        // Check www.
225
        if ($this->getForceWWW() && strpos($host, 'www.') !== 0) {
226
            $host = "www.{$host}";
227
        }
228
229
        // No-op if no changes
230
        if ($request->getScheme() === $scheme && $request->getHost() === $host) {
231
            return null;
232
        }
233
234
        return $this->redirectToScheme($request, $scheme, $host);
235
    }
236
237
    /**
238
     * Handles redirection to canonical urls outside of the main middleware chain
239
     * using HTTPResponseException.
240
     * Will not do anything if a current HTTPRequest isn't available
241
     *
242
     * @param HTTPRequest|null $request Allow HTTPRequest to be used for the base comparison
243
     * @throws HTTPResponse_Exception
244
     */
245
    public function throwRedirectIfNeeded(HTTPRequest $request = null)
246
    {
247
        $request = $this->getOrValidateRequest($request);
248
        if (!$request) {
249
            return;
250
        }
251
        $response = $this->getRedirect($request);
252
        if ($response) {
253
            throw new HTTPResponse_Exception($response);
254
        }
255
    }
256
257
    /**
258
     * Return a valid request, if one is available, or null if none is available
259
     *
260
     * @param HTTPRequest $request
261
     * @return HTTPRequest|null
262
     */
263
    protected function getOrValidateRequest(HTTPRequest $request = null)
264
    {
265
        if ($request instanceof HTTPRequest) {
266
            return $request;
267
        }
268
        if (Injector::inst()->has(HTTPRequest::class)) {
269
            return Injector::inst()->get(HTTPRequest::class);
270
        }
271
        return null;
272
    }
273
274
    /**
275
     * Check if a redirect for SSL is necessary
276
     *
277
     * @param HTTPRequest $request
278
     * @return bool
279
     */
280
    protected function requiresSSL(HTTPRequest $request)
281
    {
282
        // Check if force SSL is enabled
283
        if (!$this->getForceSSL()) {
284
            return false;
285
        }
286
287
        // Already on SSL
288
        if ($request->getScheme() === 'https') {
289
            return false;
290
        }
291
292
        // Veto if any existing patterns fail
293
        $patterns = $this->getForceSSLPatterns();
294
        if (!$patterns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $patterns of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
295
            return true;
296
        }
297
298
        // Filter redirect based on url
299
        $relativeURL = $request->getURL(true);
300
        foreach ($patterns as $pattern) {
301
            if (preg_match($pattern, $relativeURL)) {
302
                return true;
303
            }
304
        }
305
306
        // No patterns match
307
        return false;
308
    }
309
310
    /**
311
     * @return int
312
     */
313
    public function getRedirectType()
314
    {
315
        return $this->redirectType;
316
    }
317
318
    /**
319
     * @param int $redirectType
320
     * @return $this
321
     */
322
    public function setRedirectType($redirectType)
323
    {
324
        $this->redirectType = $redirectType;
325
        return $this;
326
    }
327
328
    /**
329
     * Get enabled flag, or list of environments to enable in.
330
     *
331
     * @return array|bool
332
     */
333
    public function getEnabledEnvs()
334
    {
335
        return $this->enabledEnvs;
336
    }
337
338
    /**
339
     * Set enabled flag, or list of environments to enable in.
340
     * Note: CLI is disabled by default, so `"cli"(string)` or `true(bool)` should be specified if you wish to
341
     * enable for testing.
342
     *
343
     * @param array|bool $enabledEnvs
344
     * @return $this
345
     */
346
    public function setEnabledEnvs($enabledEnvs)
347
    {
348
        $this->enabledEnvs = $enabledEnvs;
349
        return $this;
350
    }
351
352
    /**
353
     * Ensure this middleware is enabled
354
     */
355
    protected function isEnabled()
356
    {
357
        // At least one redirect must be enabled
358
        if (!$this->getForceWWW() && !$this->getForceSSL()) {
359
            return false;
360
        }
361
362
        // Filter by env vars
363
        $enabledEnvs = $this->getEnabledEnvs();
364
        if (is_bool($enabledEnvs)) {
365
            return $enabledEnvs;
366
        }
367
368
        // If CLI, EnabledEnvs must contain CLI
369
        if (Director::is_cli() && !in_array('cli', $enabledEnvs)) {
370
            return false;
371
        }
372
373
        // Check other envs
374
        return empty($enabledEnvs) || in_array(Director::get_environment_type(), $enabledEnvs);
375
    }
376
377
    /**
378
     * Determine whether the executed middlewares have added a basic authentication prompt
379
     *
380
     * @param HTTPResponse $response
381
     * @return bool
382
     */
383
    protected function hasBasicAuthPrompt(HTTPResponse $response = null)
384
    {
385
        if (!$response) {
386
            return false;
387
        }
388
        return ($response->getStatusCode() === 401 && $response->getHeader('WWW-Authenticate'));
389
    }
390
391
    /**
392
     * Redirect the current URL to the specified HTTP scheme
393
     *
394
     * @param HTTPRequest $request
395
     * @param string $scheme
396
     * @param string $host
397
     * @return HTTPResponse
398
     */
399
    protected function redirectToScheme(HTTPRequest $request, $scheme, $host = null)
400
    {
401
        if (!$host) {
402
            $host = $request->getHost();
403
        }
404
405
        $url = Controller::join_links("{$scheme}://{$host}", Director::baseURL(), $request->getURL(true));
406
407
        // Force redirect
408
        $response = HTTPResponse::create();
409
        $response->redirect($url, $this->getRedirectType());
410
411
        return $response;
412
    }
413
}
414