Completed
Push — master ( 5a7b70...9384ae )
by Ingo
02:40
created

ErrorPage::getCMSFields()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 41
Code Lines 35

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 41
rs 8.8571
cc 1
eloc 35
nc 1
nop 0
1
<?php
2
3
use SilverStripe\Filesystem\Storage\GeneratedAssetHandler;
4
/**
5
 * ErrorPage holds the content for the page of an error response.
6
 * Renders the page on each publish action into a static HTML file
7
 * within the assets directory, after the naming convention
8
 * /assets/error-<statuscode>.html.
9
 * This enables us to show errors even if PHP experiences a recoverable error.
10
 * ErrorPages
11
 *
12
 * @see Debug::friendlyError()
13
 *
14
 * @property int $ErrorCode HTTP Error code
15
 * @package cms
16
 */
17
class ErrorPage extends Page {
18
19
	private static $db = array(
20
		"ErrorCode" => "Int",
21
	);
22
23
	private static $defaults = array(
24
		"ShowInMenus" => 0,
25
		"ShowInSearch" => 0
26
	);
27
28
	private static $allowed_children = array();
29
30
	private static $description = 'Custom content for different error cases (e.g. "Page not found")';
31
32
	/**
33
	 * Allows control over writing directly to the configured `GeneratedAssetStore`.
34
	 *
35
	 * @config
36
	 * @var bool
37
	 */
38
	private static $enable_static_file = true;
39
40
	/**
41
	 * Prefix for storing error files in the {@see GeneratedAssetHandler} store.
42
	 * Defaults to empty (top level directory)
43
	 *
44
	 * @config
45
	 * @var string
46
	 */
47
	private static $store_filepath = null;
48
	/**
49
	 * @param $member
50
	 *
51
	 * @return boolean
52
	 */
53
	public function canAddChildren($member = null) {
54
		return false;
55
	}
56
57
	/**
58
	 * Get a {@link SS_HTTPResponse} to response to a HTTP error code if an
59
	 * {@link ErrorPage} for that code is present. First tries to serve it
60
	 * through the standard SilverStripe request method. Falls back to a static
61
	 * file generated when the user hit's save and publish in the CMS
62
	 *
63
	 * @param int $statusCode
64
	 * @return SS_HTTPResponse
65
	 */
66
	public static function response_for($statusCode) {
67
		// first attempt to dynamically generate the error page
68
		$errorPage = ErrorPage::get()
69
			->filter(array(
70
				"ErrorCode" => $statusCode
71
			))->first();
72
73
		if($errorPage) {
74
			Requirements::clear();
75
			Requirements::clear_combined_files();
76
77
			return ModelAsController::controller_for($errorPage)
78
				->handleRequest(
79
					new SS_HTTPRequest('GET', ''),
80
					DataModel::inst()
81
				);
82
		}
83
84
		// then fall back on a cached version
85
		$content = self::get_content_for_errorcode($statusCode);
86
		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...
87
			$response = new SS_HTTPResponse();
88
			$response->setStatusCode($statusCode);
89
			$response->setBody($content);
90
			return $response;
91
		}
92
	}
93
94
	/**
95
	 * Ensures that there is always a 404 page by checking if there's an
96
	 * instance of ErrorPage with a 404 and 500 error code. If there is not,
97
	 * one is created when the DB is built.
98
	 */
99
	public function requireDefaultRecords() {
100
		parent::requireDefaultRecords();
101
102
		if ($this->class === 'ErrorPage' && SiteTree::config()->create_default_pages) {
103
104
			$defaultPages = $this->getDefaultRecords();
105
106
			foreach($defaultPages as $defaultData) {
107
				$code = $defaultData['ErrorCode'];
108
				$page = ErrorPage::get()->filter('ErrorCode', $code)->first();
109
				$pageExists = !empty($page);
110
				if(!$pageExists) {
111
					$page = new ErrorPage($defaultData);
112
					$page->write();
113
					$page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
114
				}
115
116
				// Check if static files are enabled
117
				if(!self::config()->enable_static_file) {
118
					continue;
119
				}
120
121
				// Ensure this page has cached error content
122
				$success = true;
123
				if(!$page->hasStaticPage()) {
124
					// Update static content
125
					$success = $page->writeStaticPage();
126
				} elseif($pageExists) {
127
					// If page exists and already has content, no alteration_message is displayed
128
					continue;
129
				}
130
131
				if($success) {
132
					DB::alteration_message(
133
						sprintf('%s error page created', $code),
134
						'created'
135
					);
136
				} else {
137
					DB::alteration_message(
138
						sprintf('%s error page could not be created. Please check permissions', $code),
139
						'error'
140
					);
141
				}
142
			}
143
		}
144
	}
145
146
	/**
147
	 * Returns an array of arrays, each of which defines properties for a new
148
	 * ErrorPage record.
149
	 *
150
	 * @return array
151
	 */
152
	protected function getDefaultRecords() {
153
		$data = array(
154
			array(
155
				'ErrorCode' => 404,
156
				'Title' => _t('ErrorPage.DEFAULTERRORPAGETITLE', 'Page not found'),
157
				'Content' => _t(
158
					'ErrorPage.DEFAULTERRORPAGECONTENT',
159
					'<p>Sorry, it seems you were trying to access a page that doesn\'t exist.</p>'
160
					. '<p>Please check the spelling of the URL you were trying to access and try again.</p>'
161
				)
162
			),
163
			array(
164
				'ErrorCode' => 500,
165
				'Title' => _t('ErrorPage.DEFAULTSERVERERRORPAGETITLE', 'Server error'),
166
				'Content' => _t(
167
					'ErrorPage.DEFAULTSERVERERRORPAGECONTENT',
168
					'<p>Sorry, there was a problem with handling your request.</p>'
169
				)
170
			)
171
		);
172
173
		$this->extend('getDefaultRecords', $data);
174
175
		return $data;
176
	}
177
178
	/**
179
	 * @return FieldList
180
	 */
181
	public function getCMSFields() {
182
		$fields = parent::getCMSFields();
183
184
		$fields->addFieldToTab(
185
			"Root.Main",
186
			new DropdownField(
187
				"ErrorCode",
188
				$this->fieldLabel('ErrorCode'),
189
				array(
190
					400 => _t('ErrorPage.400', '400 - Bad Request'),
191
					401 => _t('ErrorPage.401', '401 - Unauthorized'),
192
					403 => _t('ErrorPage.403', '403 - Forbidden'),
193
					404 => _t('ErrorPage.404', '404 - Not Found'),
194
					405 => _t('ErrorPage.405', '405 - Method Not Allowed'),
195
					406 => _t('ErrorPage.406', '406 - Not Acceptable'),
196
					407 => _t('ErrorPage.407', '407 - Proxy Authentication Required'),
197
					408 => _t('ErrorPage.408', '408 - Request Timeout'),
198
					409 => _t('ErrorPage.409', '409 - Conflict'),
199
					410 => _t('ErrorPage.410', '410 - Gone'),
200
					411 => _t('ErrorPage.411', '411 - Length Required'),
201
					412 => _t('ErrorPage.412', '412 - Precondition Failed'),
202
					413 => _t('ErrorPage.413', '413 - Request Entity Too Large'),
203
					414 => _t('ErrorPage.414', '414 - Request-URI Too Long'),
204
					415 => _t('ErrorPage.415', '415 - Unsupported Media Type'),
205
					416 => _t('ErrorPage.416', '416 - Request Range Not Satisfiable'),
206
					417 => _t('ErrorPage.417', '417 - Expectation Failed'),
207
					422 => _t('ErrorPage.422', '422 - Unprocessable Entity'),
208
					429 => _t('ErrorPage.429', '429 - Too Many Requests'),
209
					500 => _t('ErrorPage.500', '500 - Internal Server Error'),
210
					501 => _t('ErrorPage.501', '501 - Not Implemented'),
211
					502 => _t('ErrorPage.502', '502 - Bad Gateway'),
212
					503 => _t('ErrorPage.503', '503 - Service Unavailable'),
213
					504 => _t('ErrorPage.504', '504 - Gateway Timeout'),
214
					505 => _t('ErrorPage.505', '505 - HTTP Version Not Supported'),
215
				)
216
			),
217
			"Content"
218
		);
219
220
		return $fields;
221
	}
222
223
	/**
224
	 * When an error page is published, create a static HTML page with its
225
	 * content, so the page can be shown even when SilverStripe is not
226
	 * functioning correctly before publishing this page normally.
227
	 *
228
	 * @return bool True if published
229
	 */
230
	public function publishSingle() {
231
		if (!parent::publishSingle()) {
232
			return false;
233
		}
234
		return $this->writeStaticPage();
235
	}
236
237
	/**
238
	 * Determine if static content is cached for this page
239
	 *
240
	 * @return bool
241
	 */
242 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...
243
		if(!self::config()->enable_static_file) {
244
			return false;
245
		}
246
247
		// Attempt to retrieve content from generated file handler
248
		$filename = $this->getErrorFilename();
249
		$storeFilename = File::join_paths(self::config()->store_filepath, $filename);
250
		$result = self::get_asset_handler()->getContent($storeFilename);
251
		return !empty($result);
252
	}
253
254
	/**
255
	 * Write out the published version of the page to the filesystem
256
	 *
257
	 * @return true if the page write was successful
258
	 */
259
	public function writeStaticPage() {
260
		if(!self::config()->enable_static_file) {
261
			return false;
262
		}
263
264
		// Run the page (reset the theme, it might've been disabled by LeftAndMain::init())
265
		Config::nest();
266
		Config::inst()->update('SSViewer', 'theme_enabled', true);
267
		$response = Director::test(Director::makeRelative($this->Link()));
268
		Config::unnest();
269
		$errorContent = $response->getBody();
270
271
		// Store file content in the default store
272
		$storeFilename = File::join_paths(
273
			self::config()->store_filepath,
274
			$this->getErrorFilename()
275
		);
276
		self::get_asset_handler()->setContent($storeFilename, $errorContent);
277
278
		// Success
279
		return true;
280
	}
281
282
	/**
283
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
284
	 *
285
	 * @return array
286
	 */
287
	public function fieldLabels($includerelations = true) {
288
		$labels = parent::fieldLabels($includerelations);
289
		$labels['ErrorCode'] = _t('ErrorPage.CODE', "Error code");
290
291
		return $labels;
292
	}
293
294
	/**
295
	 * Returns statically cached content for a given error code
296
	 *
297
	 * @param int $statusCode A HTTP Statuscode, typically 404 or 500
298
	 * @return string|null
299
	 */
300 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...
301
		if(!self::config()->enable_static_file) {
302
			return null;
303
		}
304
305
		// Attempt to retrieve content from generated file handler
306
		$filename = self::get_error_filename($statusCode);
307
		$storeFilename = File::join_paths(
308
			self::config()->store_filepath,
309
			$filename
310
		);
311
		return self::get_asset_handler()->getContent($storeFilename);
312
	}
313
314
	/**
315
	 * Gets the filename identifier for the given error code.
316
	 * Used when handling responses under error conditions.
317
	 *
318
	 * @param int $statusCode A HTTP Statuscode, typically 404 or 500
319
	 * @param ErrorPage $instance Optional instance to use for name generation
320
	 * @return string
321
	 */
322
	protected static function get_error_filename($statusCode, $instance = null) {
323
		if(!$instance) {
324
			$instance = ErrorPage::singleton();
325
		}
326
		// Allow modules to extend this filename (e.g. for multi-domain, translatable)
327
		$name = "error-{$statusCode}.html";
328
		$instance->extend('updateErrorFilename', $name, $statusCode);
329
		return $name;
330
	}
331
332
	/**
333
	 * Get filename identifier for this record.
334
	 * Used for generating the filename for the current record.
335
	 *
336
	 * @return string
337
	 */
338
	protected function getErrorFilename() {
339
		return self::get_error_filename($this->ErrorCode, $this);
340
	}
341
342
	/**
343
	 * @return GeneratedAssetHandler
344
	 */
345
	protected static function get_asset_handler() {
346
		return Injector::inst()->get('GeneratedAssetHandler');
347
	}
348
}
349
350
/**
351
 * Controller for ErrorPages.
352
 *
353
 * @package cms
354
 */
355
class ErrorPage_Controller extends Page_Controller {
356
357
	/**
358
	 * Overload the provided {@link Controller::handleRequest()} to append the
359
	 * correct status code post request since otherwise permission related error
360
	 * pages such as 401 and 403 pages won't be rendered due to
361
	  * {@link SS_HTTPResponse::isFinished() ignoring the response body.
362
	 *
363
	 * @param SS_HTTPRequest $request
364
	 * @param DataModel $model
365
	 * @return SS_HTTPResponse
366
	 */
367
	public function handleRequest(SS_HTTPRequest $request, DataModel $model = NULL) {
368
		$response = parent::handleRequest($request, $model);
369
		$response->setStatusCode($this->ErrorCode);
370
		return $response;
371
	}
372
}
373
374