silverstripe-framework/dev/Debug.php
Ingo Schommer 3469e4d22a Merging in refactored Translatable architecture from trunk, including related/required changesets like enhancements to Object static handling (see details below)
------------------------------------------------------------------------
r68900 | sminnee | 2008-12-15 14:30:41 +1300 (Mon, 15 Dec 2008) | 1 line

Static caching merges from dnc branch
------------------------------------------------------------------------
r68917 | sminnee | 2008-12-15 14:49:06 +1300 (Mon, 15 Dec 2008) | 1 line

Merged Requirements fix from nestedurls branch
------------------------------------------------------------------------
r70033 | aoneil | 2009-01-13 14:03:41 +1300 (Tue, 13 Jan 2009) | 2 lines

Add translation migration task

------------------------------------------------------------------------
r70072 | ischommer | 2009-01-13 17:34:27 +1300 (Tue, 13 Jan 2009) | 5 lines

API CHANGE Removed obsolete internal Translatable methods: hasOwnTranslatableFields(), allFieldsInTable()
ENHANCEMENT Removed $create flag in Translatable::getTranslation() and replaced with explit action createTranslation()
ENHANCEMENT Sorting return array of Translatable::getTranslatedLangs()
ENHANCEMENT Added a note about saving a page before creating a translation
MINOR Added phpdoc to Translatable
------------------------------------------------------------------------
r70073 | ischommer | 2009-01-13 17:34:45 +1300 (Tue, 13 Jan 2009) | 1 line

ENHANCEMENT Added basic unit tests to new Translatable API
------------------------------------------------------------------------
r70080 | aoneil | 2009-01-13 18:04:21 +1300 (Tue, 13 Jan 2009) | 3 lines

BUGFIX: Fix translatable migration regenerating URLSegments when it shouldn't
BUGFIX: Fix translatable migration not writing records to Live properly

------------------------------------------------------------------------
r70118 | ischommer | 2009-01-14 11:28:24 +1300 (Wed, 14 Jan 2009) | 3 lines

API CHANGE Removed obsolete Translatable::table_exists()
ENHANCEMENT Made Translatable constructor arguments optional, as by default all database fields are marked translatable
MINOR More unit tests for Translatable
------------------------------------------------------------------------
r70138 | ischommer | 2009-01-14 17:00:30 +1300 (Wed, 14 Jan 2009) | 1 line

BUGFIX Disabled assumption that SQLQuery->filtersOnID() should only kick in when exactly one WHERE clause is given - this is very fragile and hard to test. It would return TRUE on $where = "SiteTree.ID = 5", but not on $where = array("Lang = 'de'", "SiteTree.ID = 5")
------------------------------------------------------------------------
r70214 | ischommer | 2009-01-15 18:56:25 +1300 (Thu, 15 Jan 2009) | 3 lines

BUGFIX Falling back to Translatable::current_lang() if no $context object is given, in augmentAllChildrenIncludingDeleted() and AllChildrenIncludingDeleted()
MINOR phpdoc for Translatable
MINOR Added more Translatable unit tests
------------------------------------------------------------------------
r70306 | ischommer | 2009-01-16 17:14:34 +1300 (Fri, 16 Jan 2009) | 9 lines

ENHANCEMENT Recursively creating translations for parent pages to ensure that a translated page is still accessible by traversing the tree, e.g. in "cms translation mode" (in Translatable->onBeforeWrite())
ENHANCEMENT Simplified AllChildrenIncludingDeleted() to not require a special augmentAllChildrenIncludingDeleted() implementation: We don't combine untranslated/translated children any longer (which was used in CMS tree view), but rather just show translated records
ENHANCEMENT Ensuring uniqueness of URL segments by appending "-<langcode>" to new translations (in Translatable->onBeforeWrite())
ENHANCEMENT Added Translatable->alternateGetByUrl() as a hook into SiteTree::get_by_url()
ENHANCEMENT Adding link back to original page in CMS editform for translations
BUGFIX Excluding HiddenField instances from Translatable->updateCMSFields()
BUGFIX Don't require a record to be written (through exists()) when checking Translatable->isTranslation() or Translatable->hasTranslation()
MINOR Don't use createMethod() shortcut for Translatable->AllChildrenIncludingDeleted()
MINOR Added Translatable unit tests
------------------------------------------------------------------------
r70318 | ischommer | 2009-01-19 11:46:16 +1300 (Mon, 19 Jan 2009) | 1 line

BUGFIX Reverted special cases for Translatable in Versioned->canBeVersioned() (originally committed in r42119) - was checking for existence of underscores in table names as an indication of the "_lang" suffix, which is no longer needed. It was also a flawed assumption which tripped over classes like TranslatableTest_TestPage
------------------------------------------------------------------------
r70319 | ischommer | 2009-01-19 11:47:02 +1300 (Mon, 19 Jan 2009) | 1 line

ENHANCEMENT Disabled Translatab-e>augmentWrite() - was only needed for the blacklist fields implementation which is inactive for the moment
------------------------------------------------------------------------
r70326 | ischommer | 2009-01-19 14:25:23 +1300 (Mon, 19 Jan 2009) | 2 lines

ENHANCEMENT Making ErrorPage static HTML files translatable (#2233)
ENHANCEMENT Added ErrorPage::$static_filepath to flexibly set location of static error pages (defaults to /assets)
------------------------------------------------------------------------
r70327 | ischommer | 2009-01-19 15:18:41 +1300 (Mon, 19 Jan 2009) | 1 line

FEATURE Enabled specifying a language through a hidden field in SearchForm which limits the search to pages in this language (incl. unit tests)
------------------------------------------------------------------------
r71258 | sharvey | 2009-02-03 15:49:34 +1300 (Tue, 03 Feb 2009) | 2 lines

BUGFIX: Fix translatable being enabled when it shouldn't be

------------------------------------------------------------------------
r71340 | ischommer | 2009-02-04 14:36:12 +1300 (Wed, 04 Feb 2009) | 1 line

BUGFIX Including Hierarchy->children in flushCache() and renamed to _cache_children. This caused problems in TranslatableTest when re-using the same SiteTree->Children() method with different languages on the same object (even with calling flushCache() inbetween the calls)
------------------------------------------------------------------------
r71567 | gmunn | 2009-02-10 13:49:16 +1300 (Tue, 10 Feb 2009) | 1 line

'URLSegment' on line 484 and 494 now escaped
------------------------------------------------------------------------
r72054 | ischommer | 2009-02-23 10:30:41 +1300 (Mon, 23 Feb 2009) | 3 lines

BUGFIX Fixed finding a translated homepage without an explicit URLSegment (e.g. http://mysite.com/?lang=de) - see #3540
ENHANCEMENT Added Translatable::get_homepage_urlsegment_by_language()
ENHANCEMENT Added RootURLController::get_default_homepage_urlsegment()
------------------------------------------------------------------------
r72367 | ischommer | 2009-03-03 11:13:30 +1300 (Tue, 03 Mar 2009) | 2 lines

ENHANCEMENT Added i18n::get_lang_from_locale() and i18n::convert_rfc1766()
ENHANCEMENT Using IETF/HTTP compatible "long" language code in SiteTree->MetaTags(). This means the default <meta type="content-language..."> value will be "en-US" instead of "en". The locale can be either set through the Translatable content language, or through i18n::set_locale()
------------------------------------------------------------------------
r73036 | sminnee | 2009-03-14 13:16:32 +1300 (Sat, 14 Mar 2009) | 1 line

ENHANCEMENT #3032 ajshort: Use static methods for accessing static data
------------------------------------------------------------------------
r73059 | sminnee | 2009-03-15 14:09:59 +1300 (Sun, 15 Mar 2009) | 2 lines

ENHANCEMENT: Added Object::clearCache() to clear a cache
BUGFIX: Make object cache testing more robust
------------------------------------------------------------------------
r73338 | ischommer | 2009-03-19 05:13:40 +1300 (Thu, 19 Mar 2009) | 9 lines

API CHANGE Added concept of "translation groups" to Translatable- every page can belong to a group of related translations, rather than having an explicit "original", meaning you can have pages in "non-default" languages which have no representation in other language trees. This group is recorded in a new table "<classname>_translationgroups". Translatable->createTranslation() and Translatable->onBeforeWrite() will automatically associate records in this groups. Added Translatable->addTranslationGroup(), Translatable->removeTranslationGroup(), Translatable->getTranslationGroup()
API CHANGE Removed Translatable->isTranslation() - after the new "translation group" model, every page is potentially a translation
API CHANGE Translatable->findOriginalIDs(), Translatable->setOriginalPage(), Translatable->getOriginalPage()
ENHANCEMENT Translatable->getCMSFields() will now always show the "create translation" option, not only on default languages - meaning you can create translations based on other translations
ENHANCEMENT Translatable language dropdown in CMS will always show all available languages, rather than filtering by already existing translations
ENHANCEMENT Added check for an existing record in Translatable->createTranslation()
BUGFIX Removed Translatable->getLang() which overloaded the $db property - it was causing side effects during creation of SiteTree default records.
BUGFIX Added check in Translatable->augmentSQL() to avoid reapplying "Lang = ..." filter twice
BUGFIX Removed bypass in Translatable->AllChildrenIncludingDeleted()
------------------------------------------------------------------------
r73339 | ischommer | 2009-03-19 05:15:46 +1300 (Thu, 19 Mar 2009) | 1 line

BUGFIX Disabled "untranslated" CSS class for SiteTree elements - doesn't apply any longer with the new "translation groups" concept
------------------------------------------------------------------------
r73341 | ischommer | 2009-03-19 06:01:51 +1300 (Thu, 19 Mar 2009) | 1 line

BUGFIX Disabled auto-excluding of default language from the "available languages" array in LanguageDropdownField - due to the new "translation groups" its possible to have a translation from another language into the default language
------------------------------------------------------------------------
r73342 | ischommer | 2009-03-19 06:13:23 +1300 (Thu, 19 Mar 2009) | 4 lines

BUGFIX Setting ParentID of translated record if recursively creating parents in Translatable::onBeforeWrite()
BUGFIX Fixing inline form action for "create translation"
BUGFIX Removed link to "original page" for a translation - no longer valid
MINOR documentation for Translatable
------------------------------------------------------------------------
r73464 | ischommer | 2009-03-20 20:51:00 +1300 (Fri, 20 Mar 2009) | 1 line

MINOR documentation
------------------------------------------------------------------------
r73465 | ischommer | 2009-03-20 20:58:52 +1300 (Fri, 20 Mar 2009) | 1 line

BUGFIX Fixed Hierarchy->Children() testing in TranslatableTest - with the new datamodel you can't call Children() in a different language regardless of Translatable::set_reading_lang(), the Children() call has to be made from a parent in the same language
------------------------------------------------------------------------
r73466 | ischommer | 2009-03-20 21:36:40 +1300 (Fri, 20 Mar 2009) | 2 lines

ENHANCEMENT Added Translatable::get_locale_from_lang(), Translatable::get_common_locales(), $common_locales and $likely_subtags in preparation to switch Translatable from using short "lang" codes to proper long locales
API CHANGE Deprecated Translatable::set_default_lang(), Translatable::default_lang()
------------------------------------------------------------------------
r73467 | ischommer | 2009-03-20 21:38:57 +1300 (Fri, 20 Mar 2009) | 1 line

ENHANCEMENT Supporting "Locale-English" and "Locale-Native" as listing arguments in LanguageDropdownField
------------------------------------------------------------------------
r73468 | ischommer | 2009-03-20 21:47:06 +1300 (Fri, 20 Mar 2009) | 7 lines

ENHANCEMENT Adjusted SearchForm, Debug, ErrorPage, SiteTree to using locales instead of lang codes
API CHANGE Changed Translatable datamodel to use locales ("en_US") instead of lang values ("en).
API CHANGE Changed Translatable::$default_lang to $default_locale, Translatable::$reading_lang to $reading_locale
API CHANGE Using "locale" instead of "lang" in Translatable::choose_site_lang() to auto-detect language from cookies or GET parameters
API CHANGE Deprecated Translatable::is_default_lang(), set_default_lang(), get_default_lang(), current_lang(), set_reading_lang(), get_reading_lang(), get_by_lang(), get_one_by_lang()
API CHANGE Removed Translatable::get_original() - with the new "translation groups" concept there no longer is an original for a translation
BUGFIX Updated MigrateTranslatableTask to new Locale based datamodel
------------------------------------------------------------------------
r73470 | ischommer | 2009-03-20 21:56:57 +1300 (Fri, 20 Mar 2009) | 1 line

MINOR fixed typo
------------------------------------------------------------------------
r73472 | sminnee | 2009-03-21 17:30:04 +1300 (Sat, 21 Mar 2009) | 1 line

BUGFIX: Fixed translatable test execution by making protected methods public
------------------------------------------------------------------------
r73473 | sminnee | 2009-03-21 18:10:05 +1300 (Sat, 21 Mar 2009) | 1 line

ENHANCEMENT: Added Object::combined_static(), which gets all values of a static property from each class in the hierarchy
------------------------------------------------------------------------
r73883 | ischommer | 2009-04-01 08:32:19 +1300 (Wed, 01 Apr 2009) | 1 line

BUGFIX Making $_SINGLETONS a global instead of a static in Core.php so it can be re-used in other places
------------------------------------------------------------------------
r73951 | ischommer | 2009-04-02 05:35:32 +1300 (Thu, 02 Apr 2009) | 3 lines

API CHANGE Deprecated Translatable::enable() and i18n::enable()- use Object::add_extension('SiteTree','Translatable'), Deprecated Translatable::disable() and i18n::disable() - use Object::remove_extension('SiteTree','Translatable'), Deprecated Translatable::enabled() - use $myPage->hasExtension('Translatable')
API CHANGE Removed Translatable::creating_from() - doesn't apply any longer
ENHANCEMENT Translatable extension is no longer hooked up to SiteTree by default, which should improve performance and memory usage for sites not using Translatable. Please use Object::add_extension('SiteTree','Translatable') in your _config.php instead. Adjusted several classes (Image, ErrorPage, RootURLController) to the new behaviour.
------------------------------------------------------------------------
r73882 | ischommer | 2009-04-01 08:31:21 +1300 (Wed, 01 Apr 2009) | 1 line

ENHANCEMENT Added DataObjectDecorator->setOwner()
------------------------------------------------------------------------
r73884 | ischommer | 2009-04-01 08:32:51 +1300 (Wed, 01 Apr 2009) | 1 line

ENHANCEMENT Added Extension::get_classname_without_arguments()
------------------------------------------------------------------------
r73900 | ischommer | 2009-04-01 11:27:53 +1300 (Wed, 01 Apr 2009) | 7 lines

API CHANGE Deprecated Object->extInstance(), use getExtensionInstance() instead
ENHANCEMENT Added Object->getExtensionInstances()
ENHANCEMENT Added Object::get_extensions()
ENHANCEMENT Unsetting class caches when using Object::add_extension() to avoid problems with defineMethods etc.
BUGFIX Fixed extension comparison with case sensitivity and stripping arguments in Object::has_extension()
BUGFIX Unsetting all cached singletons in Object::remove_extension() to avoid outdated extension_instances
MINOR Documentation in Object
------------------------------------------------------------------------
r74017 | ischommer | 2009-04-03 10:49:40 +1300 (Fri, 03 Apr 2009) | 1 line

ENHANCEMENT Improved deprecated fallbacks in Translatable by auto-converting short language codes to long locales and vice versa through i18n::get_lang_from_locale()/i18n::get_locale_from_lang()
------------------------------------------------------------------------
r74030 | ischommer | 2009-04-03 11:41:26 +1300 (Fri, 03 Apr 2009) | 1 line

MINOR Re-added Translatable::default_lang() for more graceful fallback to Translatable::default_locale()
------------------------------------------------------------------------
r74065 | ischommer | 2009-04-04 05:38:51 +1300 (Sat, 04 Apr 2009) | 1 line

BUGFIX Re-added Translatable->isTranslation() for more friendly deprecation (originally removed in r73338)
------------------------------------------------------------------------
r74069 | ischommer | 2009-04-04 09:43:01 +1300 (Sat, 04 Apr 2009) | 1 line

BUGFIX Fixed legacy handling of Translatable::enable(),Translatable::disable() and Translatable::is_enabled() - applying extension to SiteTree instead of Page to avoid datamodel clashes
------------------------------------------------------------------------
r74070 | ischommer | 2009-04-04 10:23:51 +1300 (Sat, 04 Apr 2009) | 1 line

API CHANGE Deprecated Translatable::choose_site_lang(), use choose_site_locale()
------------------------------------------------------------------------
r74941 | ischommer | 2009-04-22 15:22:09 +1200 (Wed, 22 Apr 2009) | 2 lines

ENHANCEMENT Adding SapphireTest::set_up_once() and SapphireTest::tear_down_once() for better test performance with state that just needs to be initialized once per test case (not per test method). Added new SapphireTestSuite to support this through PHPUnit.
ENHANCEMENT Using set_up_once() in TranslatableTest and TranslatableSearchFormTest for better test run performance
------------------------------------------------------------------------
r74942 | ischommer | 2009-04-22 15:24:50 +1200 (Wed, 22 Apr 2009) | 1 line

BUGFIX Fixed TranslatableSearchFormTest->setUp() method
------------------------------------------------------------------------
r73509 | ischommer | 2009-03-23 11:59:14 +1300 (Mon, 23 Mar 2009) | 1 line

MINOR phpdoc documentation
------------------------------------------------------------------------


git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/branches/2.3@74986 467b73ca-7a2a-4603-9d3b-597d59a354a9
2011-02-02 14:27:16 +13:00

671 lines
21 KiB
PHP

<?php
/**
* Supports debugging and core error handling.
*
* Attaches custom methods to the default
* error handling hooks in PHP. Currently, three levels
* of error are supported:
*
* - Notice
* - Warning
* - Error
*
* Notice level errors are currently unsupported, and will be passed
* directly to the normal PHP error output.
*
* Uncaught exceptions are currently passed to the debug
* reporter as standard PHP errors.
*
* There are four different types of error handler supported by the
* Debug class:
*
* - Friendly
* - Fatal
* - Logger
* - Emailer
*
* Currently, only Friendly, Fatal, and Emailer handlers are implemented.
*
* @todo port header/footer wrapping code to external reporter class
* @todo add support for user defined config: Debug::die_on_notice(true | false)
* @todo add appropriate handling for E_NOTICE and E_USER_NOTICE levels
* @todo better way of figuring out the error context to display in highlighted source
* @todo implement error logger handler
*
* @package sapphire
* @subpackage core
*/
class Debug {
/**
* @var $custom_smtp_server string Custom mailserver for sending mails.
*/
protected static $custom_smtp_server = '';
/**
* @var $send_errors_to string Email address to send error notifications
*/
protected static $send_errors_to;
/**
* @var $send_warnings_to string Email address to send warning notifications
*/
protected static $send_warnings_to;
/**
* String indicating the file where errors are logged.
* Filename is relative to the site root.
* The named file will have a terse log sent to it, and the full log (an
* encoded file containing backtraces and things) will go to a file of a similar
* name, but with the suffix ".full" added.
*/
protected static $log_errors_to = null;
/**
* Show the contents of val in a debug-friendly way.
* Debug::show() is intended to be equivalent to dprintr()
*/
static function show($val, $showHeader = true) {
if(!Director::isLive()) {
if($showHeader) {
$caller = Debug::caller();
if(Director::is_ajax() || Director::is_cli())
echo "Debug ($caller[class]$caller[type]$caller[function]() in line $caller[line] of " . basename($caller['file']) . ")\n";
else
echo "<div style=\"background-color: white; text-align: left;\">\n<hr>\n<h3>Debug <span style=\"font-size: 65%\">($caller[class]$caller[type]$caller[function]() \n<span style=\"font-weight:normal\">in line</span> $caller[line] \n<span style=\"font-weight:normal\">of</span> " . basename($caller['file']) . ")</span>\n</h3>\n";
}
echo Debug::text($val);
if(!Director::is_ajax() && !Director::is_cli()) echo "</div>";
else echo "\n\n";
}
}
/**
* Close out the show dumper
*
* @param mixed $val
*/
static function endshow($val) {
if(!Director::isLive()) {
$caller = Debug::caller();
echo "<hr>\n<h3>Debug \n<span style=\"font-size: 65%\">($caller[class]$caller[type]$caller[function]() \n<span style=\"font-weight:normal\">in line</span> $caller[line] \n<span style=\"font-weight:normal\">of</span> " . basename($caller['file']) . ")</span>\n</h3>\n";
echo Debug::text($val);
die();
}
}
/**
* Quick dump of a variable.
*
* @param mixed $val
*/
static function dump($val) {
echo '<pre style="background-color:#ccc;padding:5px;font-size:14px;line-height:18px;">';
$caller = Debug::caller();
echo "<span style=\"font-size: 12px;color:#666;\">Line $caller[line] of " . basename($caller['file']) . ":</span>\n";
if (is_string($val)) print_r(wordwrap($val, 100));
else print_r($val);
echo '</pre>';
}
/**
* ??
*
* @param unknown_type $val
* @return unknown
*/
static function text($val) {
if(is_object($val)) {
if(method_exists($val, 'hasMethod')) {
$hasDebugMethod = $val->hasMethod('debug');
} else {
$hasDebugMethod = method_exists($val, 'debug');
}
if($hasDebugMethod) {
return $val->debug();
}
}
if(is_array($val)) {
$result = "<ul>\n";
foreach($val as $k => $v) {
$result .= "<li>$k = " . Debug::text($v) . "</li>\n";
}
$val = $result . "</ul>\n";
} else if (is_object($val)) {
$val = var_export($val, true);
} else {
if(!Director::is_cli() && !Director::is_ajax()) {
$val = "<pre style=\"font-family: Courier new\">" . htmlentities($val) . "</pre>\n";
}
}
return $val;
}
/**
* Show a debugging message
*/
static function message($message, $showHeader = true) {
if(!Director::isLive()) {
$caller = Debug::caller();
$file = basename($caller['file']);
if(Director::is_cli()) {
if($showHeader) echo "Debug (line $caller[line] of $file):\n ";
echo trim($message) . "\n";
} else {
echo "<p style=\"background-color: white; color: black; width: 95%; margin: 0.5em; padding: 0.3em; border: 1px #CCC solid\">\n";
if($showHeader) echo "<b>Debug (line $caller[line] of $file):</b>\n ";
echo Convert::raw2xml(trim($message)) . "</p>\n";
}
}
}
/**
* Log to a standard text file output.
*
* @param $message string to output
*/
static function log($message) {
$file = dirname(__FILE__).'/../../debug.log';
$now = date('r');
$oldcontent = file_get_contents($file);
$content = $oldcontent . "\n\n== $now ==\n$message\n";
file_put_contents($file, $content);
}
/**
* Load error handlers into environment.
* Caution: The error levels default to E_ALL is the site is in dev-mode (set in main.php).
*/
static function loadErrorHandlers() {
set_error_handler('errorHandler', error_reporting());
set_exception_handler('exceptionHandler');
}
/**
* Handle a non-fatal warning error thrown by PHP interpreter.
*
* @param unknown_type $errno
* @param unknown_type $errstr
* @param unknown_type $errfile
* @param unknown_type $errline
* @param unknown_type $errcontext
*/
static function warningHandler($errno, $errstr, $errfile, $errline, $errcontext) {
if(error_reporting() == 0) return;
if(self::$send_warnings_to) self::emailError(self::$send_warnings_to, $errno, $errstr, $errfile, $errline, $errcontext, "Warning");
self::log_error_if_necessary( $errno, $errstr, $errfile, $errline, $errcontext, "Warning");
if(Director::isDev()) {
self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Warning");
}
}
/**
* Handle a fatal error, depending on the mode of the site (ie: Dev, Test, or Live).
*
* Runtime execution dies immediately once the error is generated.
*
* @param unknown_type $errno
* @param unknown_type $errstr
* @param unknown_type $errfile
* @param unknown_type $errline
* @param unknown_type $errcontext
*/
static function fatalHandler($errno, $errstr, $errfile, $errline, $errcontext) {
if(self::$send_errors_to) self::emailError(self::$send_errors_to, $errno, $errstr, $errfile, $errline, $errcontext, "Error");
self::log_error_if_necessary( $errno, $errstr, $errfile, $errline, $errcontext, "Error");
if(Director::isDev() || Director::is_cli()) {
Debug::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error");
} else {
Debug::friendlyError();
}
exit(1);
}
/**
* Render a user-facing error page, using the default HTML error template
* rendered by {@link ErrorPage} if it exists. Doesn't use the standard {@link HTTPResponse} class
* the keep dependencies minimal.
*
* @uses ErrorPage
*
* @param int $statusCode HTTP Status Code (Default: 500)
* @param string $friendlyErrorMessage User-focused error message. Should not contain code pointers or "tech-speak".
* Used in the HTTP Header and ajax responses.
* @param string $friendlyErrorDetail Detailed user-focused message. Is just used if no {@link ErrorPage} is found
* for this specific status code.
* @return string HTML error message for non-ajax requests, plaintext for ajax-request.
*/
static function friendlyError($statusCode = 500, $friendlyErrorMessage = null, $friendlyErrorDetail = null) {
if(!$friendlyErrorMessage) $friendlyErrorMessage = 'There has been an error';
if(!$friendlyErrorDetail) $friendlyErrorDetail = 'The website server has not been able to respond to your request.';
if(!headers_sent()) header($_SERVER['SERVER_PROTOCOL'] . " $statusCode $friendlyErrorMessage");
if(Director::is_ajax()) {
echo $friendlyErrorMessage;
} else {
$errorFilePath = ErrorPage::get_filepath_for_errorcode($statusCode, Translatable::current_lang());
if(file_exists($errorfilePath)) {
echo file_get_contents(ASSETS_PATH . "/error-$statusCode.html");
} else {
$renderer = new DebugView();
$renderer->writeHeader();
$renderer->writeInfo("Website Error", $friendlyErrorMessage, $friendlyErrorDetail);
if(Email::getAdminEmail()) {
$mailto = Email::obfuscate(Email::getAdminEmail());
$renderer->writeParagraph('Contact an administrator: ' . $mailto . '');
}
$renderer->writeFooter();
}
}
}
/**
* Create an instance of an appropriate DebugView object.
*/
static function create_debug_view() {
if(Director::is_cli() || Director::is_ajax()) return new CliDebugView();
else return new DebugView();
}
/**
* Render a developer facing error page, showing the stack trace and details
* of the code where the error occured.
*
* @param unknown_type $errno
* @param unknown_type $errstr
* @param unknown_type $errfile
* @param unknown_type $errline
* @param unknown_type $errcontext
*/
static function showError($errno, $errstr, $errfile, $errline, $errcontext, $errtype) {
if(!headers_sent()) {
$errText = "$errtype: \"$errstr\" at line $errline of $errfile";
$errText = str_replace(array("\n","\r")," ",$errText);
if(!headers_sent()) header($_SERVER['SERVER_PROTOCOL'] . " 500 $errText");
// if error is displayed through ajax with CliDebugView, use plaintext output
if(Director::is_ajax()) header('Content-Type: text/plain');
}
// Legacy error handling for customized prototype.js Ajax.Base.responseIsSuccess()
// if(Director::is_ajax()) echo "ERROR:\n";
$reporter = self::create_debug_view();
// Coupling alert: This relies on knowledge of how the director gets its URL, it could be improved.
$httpRequest = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $_REQUEST['url'];
if(isset($_SERVER['REQUEST_METHOD'])) $httpRequest = $_SERVER['REQUEST_METHOD'] . ' ' . $httpRequest;
$reporter->writeHeader($httpRequest);
$reporter->writeError($httpRequest, $errno, $errstr, $errfile, $errline, $errcontext);
$lines = file($errfile);
// Make the array 1-based
array_unshift($lines,"");
unset($lines[0]);
$offset = $errline-10;
$lines = array_slice($lines, $offset, 16, true);
$reporter->writeSourceFragment($lines, $errline);
$reporter->writeTrace($lines);
$reporter->writeFooter();
exit(1);
}
/**
* Utility method to render a snippet of PHP source code, from selected file
* and highlighting the given line number.
*
* @param string $errfile
* @param int $errline
*/
static function showLines($errfile, $errline) {
$lines = file($errfile);
$offset = $errline-10;
$lines = array_slice($lines, $offset, 16);
echo '<pre>';
$offset++;
foreach($lines as $line) {
$line = htmlentities($line);
if ($offset == $errline) {
echo "<span>$offset</span> <span class=\"error\">$line</span>";
} else {
echo "<span>$offset</span> $line";
}
$offset++;
}
echo '</pre>';
}
/**
* Dispatch an email notification message when an error is triggered.
* Uses the native PHP mail() function.
*
* @param string $emailAddress
* @param string $errno
* @param string $errstr
* @param string $errfile
* @param int $errline
* @param string $errcontext
* @param string $errorType "warning" or "error"
* @return boolean
*/
static function emailError($emailAddress, $errno, $errstr, $errfile, $errline, $errcontext, $errorType = "Error") {
if(strtolower($errorType) == 'warning') {
$colour = "orange";
} else {
$colour = "red";
}
$data = "<div style=\"border: 5px $colour solid\">\n";
$data .= "<p style=\"color: white; background-color: $colour; margin: 0\">$errorType: $errstr<br /> At line $errline in $errfile\n<br />\n<br />\n</p>\n";
$data .= Debug::backtrace(true);
$data .= "</div>\n";
// override smtp-server if needed
if(self::$custom_smtp_server) ini_set("SMTP", self::$custom_smtp_server);
$relfile = Director::makeRelative($errfile);
if($relfile[0] == '/') $relfile = substr($relfile,1);
return mail($emailAddress, "$errorType at $relfile line $errline (http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI])", $data, "Content-type: text/html\nFrom: errors@silverstripe.com");
}
/**
* Log the given error, if self::$log_errors is set.
* Uses the native error_log() funtion in PHP.
*
* Format: [d-M-Y h:i:s] <type> at <file> line <line>: <errormessage> <url>
*
* @todo Detect script path for CLI errors
* @todo Log detailed errors to full file
*/
protected static function log_error_if_necessary($errno, $errstr, $errfile, $errline, $errcontext, $errtype) {
if(self::$log_errors_to) {
$shortFile = "../" . self::$log_errors_to;
$fullFile = $shortFile . '.full';
$relfile = Director::makeRelative($errfile);
if($relfile[0] == '/') $relfile = substr($relfile,1);
$urlSuffix = "";
if(isset($_SERVER['HTTP_HOST']) && $_SERVER['HTTP_HOST'] && isset($_SERVER['REQUEST_URI'])) {
$urlSuffix = " (http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI])";
}
error_log('[' . date('d-M-Y h:i:s') . "] $errtype at $relfile line $errline: $errstr$urlSuffix\n", 3, $shortFile);
}
}
/**
* @param string $server IP-Address or domain
*/
static function set_custom_smtp_server($server) {
self::$custom_smtp_server = $server;
}
/**
* @return string
*/
static function get_custom_smtp_server() {
return self::$custom_smtp_server;
}
/**
* Send errors to the given email address.
* Can be used like so:
* if(Director::isLive()) Debug::send_errors_to("sam@silverstripe.com");
*
* @param string $emailAddress The email address to send errors to
* @param string $sendWarnings Set to true to send warnings as well as errors (Default: false)
*/
static function send_errors_to($emailAddress, $sendWarnings = false) {
self::$send_errors_to = $emailAddress;
self::$send_warnings_to = $sendWarnings ? $emailAddress : null;
}
/**
* @return string
*/
static function get_send_errors_to() {
return self::$send_errors_to;
}
/**
* @param string $emailAddress
*/
static function send_warnings_to($emailAddress) {
self::$send_warnings_to = $emailAddress;
}
/**
* @return string
*/
static function get_send_warnings_to() {
return self::$send_warnings_to;
}
/**
* Call this to enable logging of errors.
*/
static function log_errors_to($logFile = ".sserrors") {
self::$log_errors_to = $logFile;
}
/**
* Deprecated. Send live errors and warnings to the given address.
* @deprecated 2.3 Use send_errors_to() instead.
*/
static function sendLiveErrorsTo($emailAddress) {
user_error('Debug::sendLiveErrorsTo() is deprecated. Use Debug::send_errors_to() instead.', E_USER_NOTICE);
if(!Director::isDev()) self::send_errors_to($emailAddress, true);
}
static function caller() {
$bt = debug_backtrace();
$caller = $bt[2];
$caller['line'] = $bt[1]['line'];
$caller['file'] = $bt[1]['file'];
if(!isset($caller['class'])) $caller['class'] = '';
if(!isset($caller['type'])) $caller['type'] = '';
return $caller;
}
/**
* Render or return a backtrace from the given scope.
*
* @param unknown_type $returnVal
* @param unknown_type $ignoreAjax
* @return unknown
*/
static function backtrace($returnVal = false, $ignoreAjax = false) {
$bt = debug_backtrace();
$result = self::get_rendered_backtrace($bt, Director::is_cli() || (Director::is_ajax() && !$ignoreAjax));
if ($returnVal) {
return $result;
} else {
echo $result;
}
}
/**
* Render a backtrace array into an appropriate plain-text or HTML string.
* @param $bt The trace array, as returned by debug_backtrace() or Exception::getTrace().
* @param $plainText Set to false for HTML output, or true for plain-text output
*/
static function get_rendered_backtrace($bt, $plainText = false) {
// Ingore functions that are plumbing of the error handler
$ignoredFunctions = array('DebugView->writeTrace', 'CliDebugView->writeTrace',
'Debug::emailError','Debug::warningHandler','Debug::fatalHandler','errorHandler','Debug::showError',
'Debug::backtrace', 'exceptionHandler');
while( $bt && in_array(self::full_func_name($bt[0]), $ignoredFunctions) ) {
array_shift($bt);
}
$result = "<ul>";
foreach($bt as $item) {
if($plainText) {
$result .= self::full_func_name($item,true) . "\n";
if(isset($item['line']) && isset($item['file'])) $result .= "line $item[line] of " . basename($item['file']) . "\n";
$result .= "\n";
} else {
if ($item['function'] == 'user_error') {
$name = $item['args'][0];
} else {
$name = self::full_func_name($item,true);
}
$result .= "<li><b>" . htmlentities($name) . "</b>\n<br />\n";
$result .= isset($item['line']) ? "Line $item[line] of " : '';
$result .= isset($item['file']) ? htmlentities(basename($item['file'])) : '';
$result .= "</li>\n";
}
}
$result .= "</ul>";
return $result;
}
/**
* Return the full function name. If showArgs is set to true, a string representation of the arguments will be shown
*/
static function full_func_name($item, $showArgs = false) {
$funcName = '';
if(isset($item['class'])) $funcName .= $item['class'];
if(isset($item['type'])) $funcName .= $item['type'];
if(isset($item['function'])) $funcName .= $item['function'];
if($showArgs && isset($item['args'])) {
$args = array();
foreach($item['args'] as $arg) {
if(!is_object($arg) || method_exists($arg, '__toString')) {
$args[] = (string) $arg;
} else {
$args[] = get_class($arg);
}
}
$funcName .= "(" . implode(",", $args) .")";
}
return $funcName;
}
/**
* Check if the user has permissions to run URL debug tools,
* else redirect them to log in.
*/
static function require_developer_login() {
if(Director::isDev()) {
return;
}
if(isset($_SESSION['loggedInAs'])) {
// We have to do some raw SQL here, because this method is called in Object::defineMethods().
// This means we have to be careful about what objects we create, as we don't want Object::defineMethods()
// being called again.
// This basically calls Permission::checkMember($_SESSION['loggedInAs'], 'ADMIN');
$memberID = $_SESSION['loggedInAs'];
$groups = DB::query("SELECT GroupID from Group_Members WHERE MemberID=" . $memberID);
$groupCSV = implode($groups->column(), ',');
$permission = DB::query("
SELECT ID
FROM Permission
WHERE (
Code = 'ADMIN'
AND Type = " . Permission::GRANT_PERMISSION . "
AND GroupID IN ($groupCSV)
)
")->value();
if($permission) {
return;
}
}
// This basically does the same as
// Security::permissionFailure(null, "You need to login with developer access to make use of debugging tools.");
// We have to do this because of how early this method is called in execution.
$_SESSION['Security']['Message']['message'] = "You need to login with developer access to make use of debugging tools.";
$_SESSION['Security']['Message']['type'] = 'warning';
$_SESSION['BackURL'] = $_SERVER['REQUEST_URI'];
header($_SERVER['SERVER_PROTOCOL'] . " 302 Found");
header("Location: " . Director::baseURL() . "Security/login");
die();
}
}
/**
* Generic callback, to catch uncaught exceptions when they bubble up to the top of the call chain.
*
* @ignore
* @param Exception $exception
*/
function exceptionHandler($exception) {
$errno = E_USER_ERROR;
$type = get_class($exception);
$message = "Uncaught " . $type . ": " . $exception->getMessage();
$file = $exception->getFile();
$line = $exception->getLine();
$context = $exception->getTrace();
Debug::fatalHandler($errno, $message, $file, $line, $context);
}
/**
* Generic callback to catch standard PHP runtime errors thrown by the interpreter
* or manually triggered with the user_error function.
* Caution: The error levels default to E_ALL is the site is in dev-mode (set in main.php).
*
* @ignore
* @param int $errno
* @param string $errstr
* @param string $errfile
* @param int $errline
* @param string $errcontext
*/
function errorHandler($errno, $errstr, $errfile, $errline, $errcontext) {
switch($errno) {
case E_ERROR:
case E_CORE_ERROR:
case E_USER_ERROR:
Debug::fatalHandler($errno, $errstr, $errfile, $errline, $errcontext);
break;
case E_NOTICE:
case E_WARNING:
case E_CORE_WARNING:
case E_USER_WARNING:
Debug::warningHandler($errno, $errstr, $errfile, $errline, $errcontext);
break;
}
}