Completed
Pull Request — master (#1833)
by
unknown
02:39
created

ErrorPage::getCodes()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 30
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 27
nc 1
nop 0
1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use SilverStripe\Assets\Storage\GeneratedAssetHandler;
6
use SilverStripe\Forms\FieldList;
7
use SilverStripe\ORM\DataModel;
8
use SilverStripe\Versioned\Versioned;
9
use SilverStripe\ORM\DB;
10
use SilverStripe\CMS\Controllers\ModelAsController;
11
use SilverStripe\View\Requirements;
12
use SilverStripe\Control\HTTPRequest;
13
use SilverStripe\Control\HTTPResponse;
14
use SilverStripe\Forms\DropdownField;
15
use SilverStripe\Assets\File;
16
use SilverStripe\Core\Config\Config;
17
use SilverStripe\Control\Director;
18
use SilverStripe\Core\Injector\Injector;
19
use Page;
20
21
/**
22
 * ErrorPage holds the content for the page of an error response.
23
 * Renders the page on each publish action into a static HTML file
24
 * within the assets directory, after the naming convention
25
 * /assets/error-<statuscode>.html.
26
 * This enables us to show errors even if PHP experiences a recoverable error.
27
 * ErrorPages
28
 *
29
 * @see Debug::friendlyError()
30
 *
31
 * @property int $ErrorCode HTTP Error code
32
 */
33
class ErrorPage extends Page
34
{
35
36
    private static $db = array(
37
        "ErrorCode" => "Int",
38
    );
39
40
    private static $defaults = array(
41
        "ShowInMenus" => 0,
42
        "ShowInSearch" => 0
43
    );
44
45
    private static $table_name = 'ErrorPage';
46
47
    private static $allowed_children = array();
48
49
    private static $description = 'Custom content for different error cases (e.g. "Page not found")';
50
51
    /**
52
     * Allows control over writing directly to the configured `GeneratedAssetStore`.
53
     *
54
     * @config
55
     * @var bool
56
     */
57
    private static $enable_static_file = true;
58
59
    /**
60
     * Prefix for storing error files in the {@see GeneratedAssetHandler} store.
61
     * Defaults to empty (top level directory)
62
     *
63
     * @config
64
     * @var string
65
     */
66
    private static $store_filepath = null;
67
    /**
68
     * @param $member
69
     *
70
     * @return boolean
71
     */
72
    public function canAddChildren($member = null)
73
    {
74
        return false;
75
    }
76
77
    /**
78
     * Get a {@link HTTPResponse} to response to a HTTP error code if an
79
     * {@link ErrorPage} for that code is present. First tries to serve it
80
     * through the standard SilverStripe request method. Falls back to a static
81
     * file generated when the user hit's save and publish in the CMS
82
     *
83
     * @param int $statusCode
84
     * @return HTTPResponse
85
     */
86
    public static function response_for($statusCode)
87
    {
88
        // first attempt to dynamically generate the error page
89
        /** @var ErrorPage $errorPage */
90
        $errorPage = ErrorPage::get()
91
            ->filter(array(
92
                "ErrorCode" => $statusCode
93
            ))->first();
94
95
        if ($errorPage) {
96
            Requirements::clear();
97
            Requirements::clear_combined_files();
98
99
            return ModelAsController::controller_for($errorPage)
100
                ->handleRequest(
101
                    new HTTPRequest('GET', ''),
102
                    DataModel::inst()
103
                );
104
        }
105
106
        // then fall back on a cached version
107
        $content = self::get_content_for_errorcode($statusCode);
108
        if ($content) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $content of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
109
            $response = new HTTPResponse();
110
            $response->setStatusCode($statusCode);
111
            $response->setBody($content);
112
            return $response;
113
        }
114
115
        return null;
116
    }
117
118
    /**
119
     * Ensures that there is always a 404 page by checking if there's an
120
     * instance of ErrorPage with a 404 and 500 error code. If there is not,
121
     * one is created when the DB is built.
122
     */
123
    public function requireDefaultRecords()
124
    {
125
        parent::requireDefaultRecords();
126
127
        // Only run on ErrorPage class directly, not subclasses
128
        if (static::class !== self::class || !SiteTree::config()->create_default_pages) {
129
            return;
130
        }
131
132
        $defaultPages = $this->getDefaultRecords();
133
134
        foreach ($defaultPages as $defaultData) {
135
            $this->requireDefaultRecordFixture($defaultData);
136
        }
137
    }
138
139
    /**
140
     * Build default record from specification fixture
141
     *
142
     * @param array $defaultData
143
     */
144
    protected function requireDefaultRecordFixture($defaultData)
145
    {
146
        $code = $defaultData['ErrorCode'];
147
        $page = ErrorPage::get()->filter('ErrorCode', $code)->first();
148
        $pageExists = !empty($page);
149
        if (!$pageExists) {
150
            $page = new ErrorPage($defaultData);
151
            $page->write();
152
            $page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
153
        }
154
155
        // Check if static files are enabled
156
        if (!self::config()->enable_static_file) {
157
            return;
158
        }
159
160
        // Ensure this page has cached error content
161
        $success = true;
162
        if (!$page->hasStaticPage()) {
163
            // Update static content
164
            $success = $page->writeStaticPage();
165
        } elseif ($pageExists) {
166
            // If page exists and already has content, no alteration_message is displayed
167
            return;
168
        }
169
170
        if ($success) {
171
            DB::alteration_message(
172
                sprintf('%s error page created', $code),
173
                'created'
174
            );
175
        } else {
176
            DB::alteration_message(
177
                sprintf('%s error page could not be created. Please check permissions', $code),
178
                'error'
179
            );
180
        }
181
    }
182
183
    /**
184
     * Returns an array of arrays, each of which defines properties for a new
185
     * ErrorPage record.
186
     *
187
     * @return array
188
     */
189
    protected function getDefaultRecords()
190
    {
191
        $data = array(
192
            array(
193
                'ErrorCode' => 404,
194
                'Title' => _t('SilverStripe\\CMS\\Model\\ErrorPage.DEFAULTERRORPAGETITLE', 'Page not found'),
195
                'Content' => _t(
196
                    'SilverStripe\\CMS\\Model\\ErrorPage.DEFAULTERRORPAGECONTENT',
197
                    '<p>Sorry, it seems you were trying to access a page that doesn\'t exist.</p>'
198
                    . '<p>Please check the spelling of the URL you were trying to access and try again.</p>'
199
                )
200
            ),
201
            array(
202
                'ErrorCode' => 500,
203
                'Title' => _t('SilverStripe\\CMS\\Model\\ErrorPage.DEFAULTSERVERERRORPAGETITLE', 'Server error'),
204
                'Content' => _t(
205
                    'SilverStripe\\CMS\\Model\\ErrorPage.DEFAULTSERVERERRORPAGECONTENT',
206
                    '<p>Sorry, there was a problem with handling your request.</p>'
207
                )
208
            )
209
        );
210
211
        $this->extend('getDefaultRecords', $data);
212
213
        return $data;
214
    }
215
216
    /**
217
     * @return FieldList
218
     */
219
    public function getCMSFields()
220
    {
221
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
222
            $fields->addFieldToTab(
223
                'Root.Main',
224
                new DropdownField(
225
                    'ErrorCode',
226
                    $this->fieldLabel('ErrorCode'),
227
                    $this->getCodes(),
228
                    'Content'
229
                )
230
            );
231
        });
232
233
        return parent::getCMSFields();
234
    }
235
236
    /**
237
     * When an error page is published, create a static HTML page with its
238
     * content, so the page can be shown even when SilverStripe is not
239
     * functioning correctly before publishing this page normally.
240
     *
241
     * @return bool True if published
242
     */
243
    public function publishSingle()
244
    {
245
        if (!parent::publishSingle()) {
246
            return false;
247
        }
248
        return $this->writeStaticPage();
249
    }
250
251
    /**
252
     * Determine if static content is cached for this page
253
     *
254
     * @return bool
255
     */
256 View Code Duplication
    protected function hasStaticPage()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
257
    {
258
        if (!self::config()->enable_static_file) {
259
            return false;
260
        }
261
262
        // Attempt to retrieve content from generated file handler
263
        $filename = $this->getErrorFilename();
264
        $storeFilename = File::join_paths(self::config()->store_filepath, $filename);
265
        $result = self::get_asset_handler()->getContent($storeFilename);
266
        return !empty($result);
267
    }
268
269
    /**
270
     * Write out the published version of the page to the filesystem
271
     *
272
     * @return true if the page write was successful
273
     */
274
    public function writeStaticPage()
275
    {
276
        if (!self::config()->enable_static_file) {
277
            return false;
278
        }
279
280
        // Run the page (reset the theme, it might've been disabled by LeftAndMain::init())
281
        Config::nest();
282
        Config::inst()->update('SilverStripe\\View\\SSViewer', 'theme_enabled', true);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
283
        $response = Director::test(Director::makeRelative($this->Link()));
284
        Config::unnest();
285
        $errorContent = $response->getBody();
286
287
        // Store file content in the default store
288
        $storeFilename = File::join_paths(
289
            self::config()->store_filepath,
290
            $this->getErrorFilename()
291
        );
292
        self::get_asset_handler()->setContent($storeFilename, $errorContent);
293
294
        // Success
295
        return true;
296
    }
297
298
    /**
299
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
300
     *
301
     * @return array
302
     */
303
    public function fieldLabels($includerelations = true)
304
    {
305
        $labels = parent::fieldLabels($includerelations);
306
        $labels['ErrorCode'] = _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE', "Error code");
307
308
        return $labels;
309
    }
310
311
    /**
312
     * Returns statically cached content for a given error code
313
     *
314
     * @param int $statusCode A HTTP Statuscode, typically 404 or 500
315
     * @return string|null
316
     */
317 View Code Duplication
    public static function get_content_for_errorcode($statusCode)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
318
    {
319
        if (!self::config()->enable_static_file) {
320
            return null;
321
        }
322
323
        // Attempt to retrieve content from generated file handler
324
        $filename = self::get_error_filename($statusCode);
325
        $storeFilename = File::join_paths(
326
            self::config()->store_filepath,
327
            $filename
328
        );
329
        return self::get_asset_handler()->getContent($storeFilename);
330
    }
331
332
    protected function getCodes()
333
    {
334
        return [
335
            400 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_400', '400 - Bad Request'),
336
            401 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_401', '401 - Unauthorized'),
337
            403 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_403', '403 - Forbidden'),
338
            404 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_404', '404 - Not Found'),
339
            405 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_405', '405 - Method Not Allowed'),
340
            406 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_406', '406 - Not Acceptable'),
341
            407 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_407', '407 - Proxy Authentication Required'),
342
            408 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_408', '408 - Request Timeout'),
343
            409 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_409', '409 - Conflict'),
344
            410 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_410', '410 - Gone'),
345
            411 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_411', '411 - Length Required'),
346
            412 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_412', '412 - Precondition Failed'),
347
            413 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_413', '413 - Request Entity Too Large'),
348
            414 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_414', '414 - Request-URI Too Long'),
349
            415 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_415', '415 - Unsupported Media Type'),
350
            416 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_416', '416 - Request Range Not Satisfiable'),
351
            417 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_417', '417 - Expectation Failed'),
352
            422 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_422', '422 - Unprocessable Entity'),
353
            429 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_429', '429 - Too Many Requests'),
354
            500 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_500', '500 - Internal Server Error'),
355
            501 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_501', '501 - Not Implemented'),
356
            502 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_502', '502 - Bad Gateway'),
357
            503 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_503', '503 - Service Unavailable'),
358
            504 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_504', '504 - Gateway Timeout'),
359
            505 => _t('SilverStripe\\CMS\\Model\\ErrorPage.CODE_505', '505 - HTTP Version Not Supported'),
360
        ];
361
    }
362
    
363
    /**
364
     * Gets the filename identifier for the given error code.
365
     * Used when handling responses under error conditions.
366
     *
367
     * @param int $statusCode A HTTP Statuscode, typically 404 or 500
368
     * @param ErrorPage $instance Optional instance to use for name generation
369
     * @return string
370
     */
371
    protected static function get_error_filename($statusCode, $instance = null)
372
    {
373
        if (!$instance) {
374
            $instance = ErrorPage::singleton();
375
        }
376
        // Allow modules to extend this filename (e.g. for multi-domain, translatable)
377
        $name = "error-{$statusCode}.html";
378
        $instance->extend('updateErrorFilename', $name, $statusCode);
379
        return $name;
380
    }
381
382
    /**
383
     * Get filename identifier for this record.
384
     * Used for generating the filename for the current record.
385
     *
386
     * @return string
387
     */
388
    protected function getErrorFilename()
389
    {
390
        return self::get_error_filename($this->ErrorCode, $this);
391
    }
392
393
    /**
394
     * @return GeneratedAssetHandler
395
     */
396
    protected static function get_asset_handler()
397
    {
398
        return Injector::inst()->get(GeneratedAssetHandler::class);
399
    }
400
}
401