ErrorPage   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 415
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 168
dl 0
loc 415
rs 9.6
c 5
b 0
f 0
wmc 35

15 Methods

Rating   Name   Duplication   Size   Complexity  
A hasStaticPage() 0 11 2
A canAddChildren() 0 3 1
A get_content_for_errorcode() 0 13 2
A getCodes() 0 30 1
A getDefaultRecords() 0 25 1
A get_asset_handler() 0 3 1
B response_for() 0 43 6
A get_error_filename() 0 9 2
A writeStaticPage() 0 37 4
A publishSingle() 0 6 2
A fieldLabels() 0 6 1
A requireDefaultRecords() 0 13 4
B requireDefaultRecordFixture() 0 39 6
A getCMSFields() 0 15 1
A getErrorFilename() 0 3 1
1
<?php
2
3
namespace SilverStripe\ErrorPage;
4
5
use Page;
0 ignored issues
show
Bug introduced by
The type Page was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use SilverStripe\Assets\File;
7
use SilverStripe\Assets\Storage\GeneratedAssetHandler;
8
use SilverStripe\CMS\Controllers\ModelAsController;
9
use SilverStripe\CMS\Model\SiteTree;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Control\HTTPRequest;
13
use SilverStripe\Control\HTTPResponse;
14
use SilverStripe\Control\HTTPResponse_Exception;
15
use SilverStripe\Core\Injector\Injector;
16
use SilverStripe\Dev\Debug;
17
use SilverStripe\Forms\DropdownField;
18
use SilverStripe\Forms\FieldList;
19
use SilverStripe\ORM\DB;
20
use SilverStripe\ORM\ValidationException;
21
use SilverStripe\Security\Member;
22
use SilverStripe\View\Requirements;
23
use SilverStripe\View\SSViewer;
24
use SilverStripe\Core\Convert;
25
use SilverStripe\ORM\FieldType\DBField;
26
27
/**
28
 * ErrorPage holds the content for the page of an error response.
29
 * Renders the page on each publish action into a static HTML file
30
 * within the assets directory, after the naming convention
31
 * /assets/error-<statuscode>.html.
32
 * This enables us to show errors even if PHP experiences a recoverable error.
33
 * ErrorPages
34
 *
35
 * @see Debug::friendlyError()
36
 *
37
 * @property int $ErrorCode HTTP Error code
38
 */
39
class ErrorPage extends Page
40
{
41
    private static $db = array(
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
42
        "ErrorCode" => "Int",
43
    );
44
45
    private static $defaults = array(
0 ignored issues
show
introduced by
The private property $defaults is not used, and could be removed.
Loading history...
46
        "ShowInMenus" => 0,
47
        "ShowInSearch" => 0,
48
        "ErrorCode" => 400
49
    );
50
51
    private static $table_name = 'ErrorPage';
0 ignored issues
show
introduced by
The private property $table_name is not used, and could be removed.
Loading history...
52
53
    private static $allowed_children = array();
0 ignored issues
show
introduced by
The private property $allowed_children is not used, and could be removed.
Loading history...
54
55
    private static $description = 'Custom content for different error cases (e.g. "Page not found")';
0 ignored issues
show
introduced by
The private property $description is not used, and could be removed.
Loading history...
56
57
    private static $icon_class = 'font-icon-p-error';
0 ignored issues
show
introduced by
The private property $icon_class is not used, and could be removed.
Loading history...
58
59
    /**
60
     * Allow developers to opt out of dev messaging using Config
61
     *
62
     * @var boolean
63
     */
64
    private static $dev_append_error_message = true;
65
66
    /**
67
     * Allows control over writing directly to the configured `GeneratedAssetStore`.
68
     *
69
     * @config
70
     * @var bool
71
     */
72
    private static $enable_static_file = true;
0 ignored issues
show
introduced by
The private property $enable_static_file is not used, and could be removed.
Loading history...
73
74
    /**
75
     * Prefix for storing error files in the {@see GeneratedAssetHandler} store.
76
     * Defaults to empty (top level directory)
77
     *
78
     * @config
79
     * @var string
80
     */
81
    private static $store_filepath = null;
0 ignored issues
show
introduced by
The private property $store_filepath is not used, and could be removed.
Loading history...
82
83
    /**
84
     * @param $member
85
     *
86
     * @return boolean
87
     */
88
    public function canAddChildren($member = null)
89
    {
90
        return false;
91
    }
92
93
    /**
94
     * Get a {@link HTTPResponse} to response to a HTTP error code if an
95
     * {@link ErrorPage} for that code is present. First tries to serve it
96
     * through the standard SilverStripe request method. Falls back to a static
97
     * file generated when the user hit's save and publish in the CMS
98
     *
99
     * @param int $statusCode
100
     * @param string|null $errorMessage A developer message to put in the response on dev envs
101
     * @return HTTPResponse
102
     * @throws HTTPResponse_Exception
103
     */
104
    public static function response_for($statusCode, $errorMessage = null)
105
    {
106
        // first attempt to dynamically generate the error page
107
        /** @var ErrorPage $errorPage */
108
        $errorPage = ErrorPage::get()
109
            ->filter(array(
110
                "ErrorCode" => $statusCode
111
            ))->first();
112
113
        if ($errorPage) {
0 ignored issues
show
introduced by
$errorPage is of type SilverStripe\ErrorPage\ErrorPage, thus it always evaluated to true.
Loading history...
114
            Requirements::clear();
115
            Requirements::clear_combined_files();
116
117
            //set @var dev_append_error_message to false to opt out of dev message
118
            $showDevMessage = (self::config()->dev_append_error_message === true);
119
120
            if ($errorMessage) {
121
                // Dev environments will have the error message added regardless of template changes
122
                if (Director::isDev() && $showDevMessage === true) {
123
                    $errorPage->Content .= "\n<p><b>Error detail: "
124
                        . Convert::raw2xml($errorMessage) ."</b></p>";
125
                }
126
127
                // On test/live environments, developers can opt to put $ResponseErrorMessage in their template
128
                $errorPage->ResponseErrorMessage = DBField::create_field('Varchar', $errorMessage);
0 ignored issues
show
Bug Best Practice introduced by
The property ResponseErrorMessage does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
129
            }
130
131
            $request = new HTTPRequest('GET', '');
132
            $request->setSession(Controller::curr()->getRequest()->getSession());
133
            return ModelAsController::controller_for($errorPage)
134
                ->handleRequest($request);
135
        }
136
137
        // then fall back on a cached version
138
        $content = self::get_content_for_errorcode($statusCode);
139
        if ($content) {
140
            $response = new HTTPResponse();
141
            $response->setStatusCode($statusCode);
142
            $response->setBody($content);
143
            return $response;
144
        }
145
146
        return null;
147
    }
148
149
    /**
150
     * Ensures that there is always a 404 page by checking if there's an
151
     * instance of ErrorPage with a 404 and 500 error code. If there is not,
152
     * one is created when the DB is built.
153
     *
154
     * @throws ValidationException
155
     */
156
    public function requireDefaultRecords()
157
    {
158
        parent::requireDefaultRecords();
159
160
        // Only run on ErrorPage class directly, not subclasses
161
        if (static::class !== self::class || !SiteTree::config()->get('create_default_pages')) {
162
            return;
163
        }
164
165
        $defaultPages = $this->getDefaultRecords();
166
167
        foreach ($defaultPages as $defaultData) {
168
            $this->requireDefaultRecordFixture($defaultData);
169
        }
170
    }
171
172
    /**
173
     * Build default record from specification fixture
174
     *
175
     * @param array $defaultData
176
     * @throws ValidationException
177
     */
178
    protected function requireDefaultRecordFixture($defaultData)
179
    {
180
        $code = $defaultData['ErrorCode'];
181
182
        /** @var ErrorPage $page */
183
        $page = ErrorPage::get()->find('ErrorCode', $code);
184
        if (!$page) {
0 ignored issues
show
introduced by
$page is of type SilverStripe\ErrorPage\ErrorPage, thus it always evaluated to true.
Loading history...
185
            $page = static::create();
186
            $page->update($defaultData);
187
            $page->write();
188
        }
189
190
        // Ensure page is published at latest version
191
        if (!$page->isLiveVersion()) {
192
            $page->publishSingle();
193
        }
194
195
        // Check if static files are enabled
196
        if (!self::config()->get('enable_static_file')) {
197
            return;
198
        }
199
200
        // Force create or refresh of static page
201
        $staticExists = $page->hasStaticPage();
202
        $success = $page->writeStaticPage();
203
        if (!$success) {
0 ignored issues
show
introduced by
The condition $success is always true.
Loading history...
204
            DB::alteration_message(
205
                sprintf('%s error page could not be created. Please check permissions', $code),
206
                'error'
207
            );
208
        } elseif ($staticExists) {
209
            DB::alteration_message(
210
                sprintf('%s error page refreshed', $code),
211
                'created'
212
            );
213
        } else {
214
            DB::alteration_message(
215
                sprintf('%s error page created', $code),
216
                'created'
217
            );
218
        }
219
    }
220
221
    /**
222
     * Returns an array of arrays, each of which defines properties for a new
223
     * ErrorPage record.
224
     *
225
     * @return array
226
     */
227
    protected function getDefaultRecords()
228
    {
229
        $data = array(
230
            array(
231
                'ErrorCode' => 404,
232
                'Title' => _t('SilverStripe\\ErrorPage\\ErrorPage.DEFAULTERRORPAGETITLE', 'Page not found'),
233
                'Content' => _t(
234
                    'SilverStripe\\ErrorPage\\ErrorPage.DEFAULTERRORPAGECONTENT',
235
                    '<p>Sorry, it seems you were trying to access a page that doesn\'t exist.</p>'
236
                    . '<p>Please check the spelling of the URL you were trying to access and try again.</p>'
237
                )
238
            ),
239
            array(
240
                'ErrorCode' => 500,
241
                'Title' => _t('SilverStripe\\ErrorPage\\ErrorPage.DEFAULTSERVERERRORPAGETITLE', 'Server error'),
242
                'Content' => _t(
243
                    'SilverStripe\\ErrorPage\\ErrorPage.DEFAULTSERVERERRORPAGECONTENT',
244
                    '<p>Sorry, there was a problem with handling your request.</p>'
245
                )
246
            )
247
        );
248
249
        $this->extend('getDefaultRecords', $data);
250
251
        return $data;
252
    }
253
254
    /**
255
     * @return FieldList
256
     */
257
    public function getCMSFields()
258
    {
259
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
260
            $fields->addFieldToTab(
261
                'Root.Main',
262
                new DropdownField(
263
                    'ErrorCode',
264
                    $this->fieldLabel('ErrorCode'),
265
                    $this->getCodes()
266
                ),
267
                'Content'
268
            );
269
        });
270
271
        return parent::getCMSFields();
272
    }
273
274
    /**
275
     * When an error page is published, create a static HTML page with its
276
     * content, so the page can be shown even when SilverStripe is not
277
     * functioning correctly before publishing this page normally.
278
     *
279
     * @return bool True if published
280
     */
281
    public function publishSingle()
282
    {
283
        if (!parent::publishSingle()) {
284
            return false;
285
        }
286
        return $this->writeStaticPage();
287
    }
288
289
    /**
290
     * Determine if static content is cached for this page
291
     *
292
     * @return bool
293
     */
294
    protected function hasStaticPage()
295
    {
296
        if (!self::config()->get('enable_static_file')) {
297
            return false;
298
        }
299
300
        // Attempt to retrieve content from generated file handler
301
        $filename = $this->getErrorFilename();
302
        $storeFilename = File::join_paths(self::config()->get('store_filepath'), $filename);
303
        $result = self::get_asset_handler()->getContent($storeFilename);
304
        return !empty($result);
305
    }
306
307
    /**
308
     * Write out the published version of the page to the filesystem.
309
     *
310
     * @return true if the page write was successful
311
     */
312
    public function writeStaticPage()
313
    {
314
        if (!self::config()->get('enable_static_file')) {
315
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type true.
Loading history...
316
        }
317
318
        // Run the page (reset the theme, it might've been disabled by LeftAndMain::init())
319
        $originalThemes = SSViewer::get_themes();
320
        try {
321
            // Restore front-end themes from config
322
            $themes = SSViewer::config()->get('themes') ?: $originalThemes;
323
            SSViewer::set_themes($themes);
324
325
            // Render page as non-member in live mode
326
            $response = Member::actAs(null, function () {
327
                $response = Director::test(Director::makeRelative($this->getAbsoluteLiveLink()));
328
                return $response;
329
            });
330
331
            $errorContent = $response->getBody();
332
        } finally {
333
            // Restore themes
334
            SSViewer::set_themes($originalThemes);
335
        }
336
337
        // Make sure we have content to save
338
        if ($errorContent) {
339
            // Store file content in the default store
340
            $storeFilename = File::join_paths(
341
                self::config()->get('store_filepath'),
342
                $this->getErrorFilename()
343
            );
344
            self::get_asset_handler()->setContent($storeFilename, $errorContent);
345
346
            return true;
347
        } else {
348
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type true.
Loading history...
349
        }
350
    }
351
352
    /**
353
     * @param bool $includerelations a boolean value to indicate if the labels returned include relation fields
354
     * @return array
355
     */
356
    public function fieldLabels($includerelations = true)
357
    {
358
        $labels = parent::fieldLabels($includerelations);
359
        $labels['ErrorCode'] = _t('SilverStripe\\ErrorPage\\ErrorPage.CODE', "Error code");
360
361
        return $labels;
362
    }
363
364
    /**
365
     * Returns statically cached content for a given error code
366
     *
367
     * @param int $statusCode A HTTP Statuscode, typically 404 or 500
368
     * @return string|null
369
     */
370
    public static function get_content_for_errorcode($statusCode)
371
    {
372
        if (!self::config()->get('enable_static_file')) {
373
            return null;
374
        }
375
376
        // Attempt to retrieve content from generated file handler
377
        $filename = self::get_error_filename($statusCode);
378
        $storeFilename = File::join_paths(
379
            self::config()->get('store_filepath'),
380
            $filename
381
        );
382
        return self::get_asset_handler()->getContent($storeFilename);
383
    }
384
385
    protected function getCodes()
386
    {
387
        return [
388
            400 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_400', '400 - Bad Request'),
389
            401 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_401', '401 - Unauthorized'),
390
            402 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_402', '402 - Payment Required'),
391
            403 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_403', '403 - Forbidden'),
392
            404 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_404', '404 - Not Found'),
393
            405 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_405', '405 - Method Not Allowed'),
394
            406 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_406', '406 - Not Acceptable'),
395
            407 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_407', '407 - Proxy Authentication Required'),
396
            408 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_408', '408 - Request Timeout'),
397
            409 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_409', '409 - Conflict'),
398
            410 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_410', '410 - Gone'),
399
            411 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_411', '411 - Length Required'),
400
            412 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_412', '412 - Precondition Failed'),
401
            413 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_413', '413 - Request Entity Too Large'),
402
            414 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_414', '414 - Request-URI Too Long'),
403
            415 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_415', '415 - Unsupported Media Type'),
404
            416 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_416', '416 - Request Range Not Satisfiable'),
405
            417 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_417', '417 - Expectation Failed'),
406
            422 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_422', '422 - Unprocessable Entity'),
407
            429 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_429', '429 - Too Many Requests'),
408
            451 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_451', '451 - Unavailable For Legal Reasons'),
409
            500 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_500', '500 - Internal Server Error'),
410
            501 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_501', '501 - Not Implemented'),
411
            502 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_502', '502 - Bad Gateway'),
412
            503 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_503', '503 - Service Unavailable'),
413
            504 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_504', '504 - Gateway Timeout'),
414
            505 => _t('SilverStripe\\ErrorPage\\ErrorPage.CODE_505', '505 - HTTP Version Not Supported'),
415
        ];
416
    }
417
418
    /**
419
     * Gets the filename identifier for the given error code.
420
     * Used when handling responses under error conditions.
421
     *
422
     * @param int $statusCode A HTTP Statuscode, typically 404 or 500
423
     * @param ErrorPage $instance Optional instance to use for name generation
424
     * @return string
425
     */
426
    protected static function get_error_filename($statusCode, $instance = null)
427
    {
428
        if (!$instance) {
429
            $instance = ErrorPage::singleton();
430
        }
431
        // Allow modules to extend this filename (e.g. for multi-domain, translatable)
432
        $name = "error-{$statusCode}.html";
433
        $instance->extend('updateErrorFilename', $name, $statusCode);
434
        return $name;
435
    }
436
437
    /**
438
     * Get filename identifier for this record.
439
     * Used for generating the filename for the current record.
440
     *
441
     * @return string
442
     */
443
    protected function getErrorFilename()
444
    {
445
        return self::get_error_filename($this->ErrorCode, $this);
446
    }
447
448
    /**
449
     * @return GeneratedAssetHandler
450
     */
451
    protected static function get_asset_handler()
452
    {
453
        return Injector::inst()->get(GeneratedAssetHandler::class);
454
    }
455
}
456