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