Merge pull request #1300 from open-sausages/features/dbfile-generated-files

Generated files API
This commit is contained in:
Ingo Schommer 2015-10-23 16:54:18 +13:00
commit 79f23631c9
5 changed files with 255 additions and 141 deletions

9
_config/cmslogging.yml Normal file
View File

@ -0,0 +1,9 @@
---
Name: cms-logging
After: '#live-logging'
Except:
environment: dev
---
Injector:
FriendlyErrorFormatter:
class: SilverStripe\Cms\Logging\ErrorPageErrorFormatter

View File

@ -0,0 +1,28 @@
<?php
namespace SilverStripe\Cms\Logging;
use ErrorPage;
use SilverStripe\Framework\Logging\DebugViewFriendlyErrorFormatter;
/**
* Provides {@see ErrorPage}-gnostic error handling
*/
class ErrorPageErrorFormatter extends DebugViewFriendlyErrorFormatter {
public function output($statusCode) {
// Ajax content is plain-text only
if(\Director::is_ajax()) {
return $this->getTitle();
}
// Determine if cached ErrorPage content is available
$content = ErrorPage::get_content_for_errorcode($statusCode);
if($content) {
return $content;
}
// Fallback to default output
return parent::output($statusCode);
}
}

View File

@ -1,4 +1,6 @@
<?php <?php
use SilverStripe\Filesystem\Storage\GeneratedAssetHandler;
/** /**
* ErrorPage holds the content for the page of an error response. * ErrorPage holds the content for the page of an error response.
* Renders the page on each publish action into a static HTML file * Renders the page on each publish action into a static HTML file
@ -8,7 +10,8 @@
* ErrorPages * ErrorPages
* *
* @see Debug::friendlyError() * @see Debug::friendlyError()
* *
* @property int $ErrorCode HTTP Error code
* @package cms * @package cms
*/ */
class ErrorPage extends Page { class ErrorPage extends Page {
@ -25,11 +28,30 @@ class ErrorPage extends Page {
private static $allowed_children = array(); private static $allowed_children = array();
private static $description = 'Custom content for different error cases (e.g. "Page not found")'; private static $description = 'Custom content for different error cases (e.g. "Page not found")';
/**
* Allows control over writing directly to the static_filepath directory. Only required if relying on
* apache ErrorDocument, and can be turned off otherwise.
*
* @config
* @var bool
*/
private static $enable_static_file = true;
/** /**
* @config * @config
* @var string
*/ */
private static $static_filepath = ASSETS_PATH; private static $static_filepath = ASSETS_PATH;
/**
* 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 * @param $member
@ -47,36 +69,32 @@ class ErrorPage extends Page {
* file generated when the user hit's save and publish in the CMS * file generated when the user hit's save and publish in the CMS
* *
* @param int $statusCode * @param int $statusCode
*
* @return SS_HTTPResponse * @return SS_HTTPResponse
*/ */
public static function response_for($statusCode) { public static function response_for($statusCode) {
// first attempt to dynamically generate the error page // first attempt to dynamically generate the error page
$errorPage = ErrorPage::get()->filter(array( $errorPage = ErrorPage::get()
"ErrorCode" => $statusCode ->filter(array(
))->first(); "ErrorCode" => $statusCode
))->first();
if($errorPage) { if($errorPage) {
Requirements::clear(); Requirements::clear();
Requirements::clear_combined_files(); Requirements::clear_combined_files();
return ModelAsController::controller_for($errorPage)->handleRequest( return ModelAsController::controller_for($errorPage)
new SS_HTTPRequest('GET', ''), DataModel::inst() ->handleRequest(
); new SS_HTTPRequest('GET', ''),
DataModel::inst()
);
} }
// then fall back on a cached version // then fall back on a cached version
$cachedPath = self::get_filepath_for_errorcode( $content = self::get_content_for_errorcode($statusCode);
$statusCode, if($content) {
class_exists('Translatable') ? Translatable::get_current_locale() : null $response = new SS_HTTPResponse();
);
if(file_exists($cachedPath)) {
$response = new SS_HTTPResponse();
$response->setStatusCode($statusCode); $response->setStatusCode($statusCode);
$response->setBody(file_get_contents($cachedPath)); $response->setBody($content);
return $response; return $response;
} }
} }
@ -89,52 +107,40 @@ class ErrorPage extends Page {
public function requireDefaultRecords() { public function requireDefaultRecords() {
parent::requireDefaultRecords(); parent::requireDefaultRecords();
if ($this->class == 'ErrorPage' && SiteTree::config()->create_default_pages) { if ($this->class === 'ErrorPage' && SiteTree::config()->create_default_pages) {
// Ensure that an assets path exists before we do any error page creation
if(!file_exists(ASSETS_PATH)) {
mkdir(ASSETS_PATH);
}
$defaultPages = $this->getDefaultRecords(); $defaultPages = $this->getDefaultRecords();
foreach($defaultPages as $defaultData) { foreach($defaultPages as $defaultData) {
$code = $defaultData['ErrorCode']; $code = $defaultData['ErrorCode'];
$page = DataObject::get_one( $page = ErrorPage::get()->filter('ErrorCode', $code)->first();
'ErrorPage', $pageExists = !empty($page);
sprintf("\"ErrorPage\".\"ErrorCode\" = '%s'", $code) if(!$pageExists) {
); $page = new ErrorPage($defaultData);
$pageExists = ($page && $page->exists()); $page->write();
$pagePath = self::get_filepath_for_errorcode($code); $page->publish('Stage', 'Live');
if(!($pageExists && file_exists($pagePath))) { }
if(!$pageExists) {
$page = new ErrorPage($defaultData);
$page->write();
$page->publish('Stage', 'Live');
}
// Ensure a static error page is created from latest error page content // Ensure this page has cached error content
$response = Director::test(Director::makeRelative($page->Link())); $success = true;
$written = null; if(!$page->hasStaticPage()) {
if($fh = fopen($pagePath, 'w')) { // Update static content
$written = fwrite($fh, $response->getBody()); $success = $page->writeStaticPage();
fclose($fh); } elseif($pageExists) {
} // If page exists and already has content, no alteration_message is displayed
continue;
}
if($written) { if($success) {
DB::alteration_message( DB::alteration_message(
sprintf('%s error page created', $code), sprintf('%s error page created', $code),
'created' 'created'
); );
} else { } else {
DB::alteration_message( DB::alteration_message(
sprintf( sprintf('%s error page could not be created. Please check permissions', $code),
'%s error page could not be created at %s. Please check permissions', 'error'
$code, );
$pagePath
),
'error'
);
}
} }
} }
} }
@ -222,44 +228,65 @@ class ErrorPage extends Page {
* content, so the page can be shown even when SilverStripe is not * content, so the page can be shown even when SilverStripe is not
* functioning correctly before publishing this page normally. * functioning correctly before publishing this page normally.
* *
* @return void * @return bool True if published
*/ */
public function doPublish() { public function doPublish() {
parent::doPublish(); $result = parent::doPublish();
$this->writeStaticPage();
return $result;
}
return $this->writeStaticPage(); /**
* Determine if static content is cached for this page
*
* @return bool
*/
protected function hasStaticPage() {
// 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()->getGeneratedContent($storeFilename, 0);
if($result) {
return true;
}
// Fallback to physical store
if(self::config()->enable_static_file) {
$staticPath = self::config()->static_filepath . "/" . $filename;
return file_exists($staticPath);
}
return false;
} }
/** /**
* Write out the published version of the page to the filesystem * Write out the published version of the page to the filesystem
* *
* @return mixed Either true, or an error * @return true if the page write was successful
*/ */
public function writeStaticPage() { public function writeStaticPage() {
// Run the page (reset the theme, it might've been disabled by LeftAndMain::init()) // Run the page (reset the theme, it might've been disabled by LeftAndMain::init())
$oldEnabled = Config::inst()->get('SSViewer', 'theme_enabled'); Config::nest();
Config::inst()->update('SSViewer', 'theme_enabled', true); Config::inst()->update('SSViewer', 'theme_enabled', true);
$response = Director::test(Director::makeRelative($this->Link())); $response = Director::test(Director::makeRelative($this->Link()));
Config::inst()->update('SSViewer', 'theme_enabled', $oldEnabled); Config::unnest();
$errorContent = $response->getBody(); $errorContent = $response->getBody();
// Check we have an assets base directory, creating if it we don't // Store file content in the default store
if(!file_exists(ASSETS_PATH)) { $filename = $this->getErrorFilename();
mkdir(ASSETS_PATH, 02775); $storeFilename = File::join_paths(self::config()->store_filepath, $filename);
self::get_asset_handler()->updateContent($storeFilename, 0, $errorContent);
// Write to physical store
if(self::config()->enable_static_file) {
Filesystem::makeFolder(self::config()->static_filepath);
$staticPath = self::config()->static_filepath . "/" . $filename;
if(!file_put_contents($staticPath, $errorContent)) {
return false;
}
} }
// if the page is published in a language other than default language, // Success
// write a specific language version of the HTML page
$filePath = self::get_filepath_for_errorcode($this->ErrorCode, $this->Locale);
if (!file_put_contents($filePath, $errorContent)) {
$fileErrorText = _t(
'ErrorPage.ERRORFILEPROBLEM',
'Error opening file "{filename}" for writing. Please check file permissions.',
array('filename' => $errorFile)
);
$this->response->addHeader('X-Status', rawurlencode($fileErrorText));
return $this->httpError(405);
}
return true; return true;
} }
@ -274,47 +301,65 @@ class ErrorPage extends Page {
return $labels; return $labels;
} }
/** /**
* Returns an absolute filesystem path to a static error file * Get error content for the given status code. Only returns pre-cached content
* which is generated through {@link publish()}. * stored either in the asset store or in the static path (if enabled).
*
* @param int $statusCode A HTTP Statuscode, mostly 404 or 500
* @param String $locale A locale, e.g. 'de_DE' (Optional)
* *
* @param int $statusCode A HTTP Statuscode, typically 404 or 500
* @return string * @return string
*/ */
public static function get_filepath_for_errorcode($statusCode, $locale = null) { public static function get_content_for_errorcode($statusCode) {
if (singleton('ErrorPage')->hasMethod('alternateFilepathForErrorcode')) { // Attempt to retrieve content from generated file handler
return singleton('ErrorPage')-> alternateFilepathForErrorcode($statusCode, $locale); $filename = self::get_error_filename($statusCode);
$storeFilename = File::join_paths(self::config()->store_filepath, $filename);
$result = self::get_asset_handler()->getGeneratedContent($storeFilename, 0);
if($result) {
return $result;
} }
if(class_exists('Translatable') && singleton('SiteTree')->hasExtension('Translatable') && $locale && $locale != Translatable::default_locale()) { // Fallback to physical store
return self::config()->static_filepath . "/error-{$statusCode}-{$locale}.html"; if(self::config()->enable_static_file) {
} else { $staticPath = self::config()->static_filepath . "/" . $filename;
return self::config()->static_filepath . "/error-{$statusCode}.html"; if(file_exists($staticPath)) {
return file_get_contents($staticPath);
}
} }
} }
/** /**
* Set the path where static error files are saved through {@link publish()}. * Gets the filename identifier for the given error code.
* Defaults to /assets. * Used when handling responses under error conditions.
* *
* @deprecated 4.0 Use "ErrorPage.static_file_path" instead * @param int $statusCode A HTTP Statuscode, typically 404 or 500
* @param string $path * @param ErrorPage $instance Optional instance to use for name generation
*/
static public function set_static_filepath($path) {
Deprecation::notice('4.0', 'Use "ErrorPage.static_file_path" instead');
self::config()->static_filepath = $path;
}
/**
* @deprecated 4.0 Use "ErrorPage.static_file_path" instead
* @return string * @return string
*/ */
static public function get_static_filepath() { protected static function get_error_filename($statusCode, $instance = null) {
Deprecation::notice('4.0', 'Use "ErrorPage.static_file_path" instead'); if(!$instance) {
return self::config()->static_filepath; $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');
} }
} }
@ -336,10 +381,9 @@ class ErrorPage_Controller extends Page_Controller {
* @return SS_HTTPResponse * @return SS_HTTPResponse
*/ */
public function handleRequest(SS_HTTPRequest $request, DataModel $model = NULL) { public function handleRequest(SS_HTTPRequest $request, DataModel $model = NULL) {
$body = parent::handleRequest($request, $model); $response = parent::handleRequest($request, $model);
$this->response->setStatusCode($this->ErrorCode); $response->setStatusCode($this->ErrorCode);
return $response;
return $this->response;
} }
} }

View File

@ -6,32 +6,26 @@
class ErrorPageTest extends FunctionalTest { class ErrorPageTest extends FunctionalTest {
protected static $fixture_file = 'ErrorPageTest.yml'; protected static $fixture_file = 'ErrorPageTest.yml';
protected $orig = array(); /**
* Location of temporary cached files
*
* @var string
*/
protected $tmpAssetsPath = ''; protected $tmpAssetsPath = '';
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
// Set temporary asset backend store
$this->orig['ErrorPage_staticfilepath'] = ErrorPage::config()->static_filepath; AssetStoreTest_SpyStore::activate('ErrorPageTest');
$this->tmpAssetsPath = sprintf('%s/_tmp_assets_%s', TEMP_FOLDER, rand()); Config::inst()->update('ErrorPage', 'static_filepath', AssetStoreTest_SpyStore::base_path());
Filesystem::makeFolder($this->tmpAssetsPath . '/ErrorPageTest'); Config::inst()->update('ErrorPage', 'enable_static_file', true);
ErrorPage::config()->static_filepath = $this->tmpAssetsPath . '/ErrorPageTest';
$this->origEnvType = Config::inst()->get('Director', 'environment_type');
Config::inst()->update('Director', 'environment_type', 'live'); Config::inst()->update('Director', 'environment_type', 'live');
} }
public function tearDown() {
parent::tearDown();
ErrorPage::config()->static_filepath = $this->orig['ErrorPage_staticfilepath'];
Filesystem::removeFolder($this->tmpAssetsPath . '/ErrorPageTest');
Filesystem::removeFolder($this->tmpAssetsPath);
Config::inst()->update('Director', 'environment_type', $this->origEnvType); public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
} }
public function test404ErrorPage() { public function test404ErrorPage() {
@ -82,4 +76,43 @@ class ErrorPageTest extends FunctionalTest {
$this->assertNotNull($response->getBody()); $this->assertNotNull($response->getBody());
$this->assertContains('text/html', $response->getHeader('Content-Type')); $this->assertContains('text/html', $response->getHeader('Content-Type'));
} }
public function testStaticCaching() {
// Test new error code does not have static content
$this->assertEmpty(ErrorPage::get_content_for_errorcode('401'));
$expectedErrorPagePath = AssetStoreTest_SpyStore::base_path() . '/error-401.html';
$this->assertFileNotExists($expectedErrorPagePath, 'Error page is not automatically cached');
// Write new 401 page
$page = new ErrorPage();
$page->ErrorCode = 401;
$page->Title = 'Unauthorised';
$page->write();
$page->publish('Stage', 'Live');
$page->doPublish();
// Static cache should now exist
$this->assertNotEmpty(ErrorPage::get_content_for_errorcode('401'));
$expectedErrorPagePath = AssetStoreTest_SpyStore::base_path() . '/error-401.html';
$this->assertFileExists($expectedErrorPagePath, 'Error page is cached');
}
/**
* Test fallback to file generation API with enable_static_file disabled
*/
public function testGeneratedFile() {
Config::inst()->update('ErrorPage', 'enable_static_file', false);
$this->logInWithPermission('ADMIN');
$page = new ErrorPage();
$page->ErrorCode = 405;
$page->Title = 'Method Not Allowed';
$page->write();
$page->doPublish();
// Error content is available, even though the static file does not exist (only in assetstore)
$this->assertNotEmpty(ErrorPage::get_content_for_errorcode('405'));
$expectedErrorPagePath = AssetStoreTest_SpyStore::base_path() . '/error-405.html';
$this->assertFileNotExists($expectedErrorPagePath, 'Error page is not cached in static location');
}
} }

View File

@ -1,11 +1,11 @@
ErrorPage: ErrorPage:
404: 404:
Title: Page Not Found Title: Page Not Found
URLSegment: page-not-found URLSegment: page-not-found
Content: My error page body Content: My error page body
ErrorCode: 404 ErrorCode: 404
403: 403:
Title: Permission Failure Title: Permission Failure
URLSegment: permission-denied URLSegment: permission-denied
Content: You do not have permission to view this page Content: You do not have permission to view this page
ErrorCode: 403 ErrorCode: 403