Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2894116
PhutilErrorHandler.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
18 KB
Referenced Files
None
Subscribers
None
PhutilErrorHandler.php
View Options
<?php
/**
* Improve PHP error logs and optionally route errors, exceptions and debugging
* information to a central listener.
*
* This class takes over the PHP error and exception handlers when you call
* ##PhutilErrorHandler::initialize()## and forwards all debugging information
* to a listener you install with ##PhutilErrorHandler::setErrorListener()##.
*
* To use PhutilErrorHandler, which will enhance the messages printed to the
* PHP error log, just initialize it:
*
* PhutilErrorHandler::initialize();
*
* To additionally install a custom listener which can print error information
* to some other file or console, register a listener:
*
* PhutilErrorHandler::setErrorListener($some_callback);
*
* For information on writing an error listener, see
* @{function:phutil_error_listener_example}. Providing a listener is optional,
* you will benefit from improved error logs even without one.
*
* Phabricator uses this class to drive the DarkConsole "Error Log" plugin.
*
* @task config Configuring Error Dispatch
* @task exutil Exception Utilities
* @task trap Error Traps
* @task internal Internals
*/
final
class
PhutilErrorHandler
extends
Phobject
{
private
static
$errorListener
=
null
;
private
static
$initialized
=
false
;
private
static
$traps
=
array
(
)
;
const
EXCEPTION
=
'exception'
;
const
ERROR
=
'error'
;
const
PHLOG
=
'phlog'
;
const
DEPRECATED
=
'deprecated'
;
/* -( Configuring Error Dispatch )----------------------------------------- */
/**
* Registers this class as the PHP error and exception handler. This will
* overwrite any previous handlers!
*
* @return void
* @task config
*/
public
static
function
initialize
(
)
{
self
::
$initialized
=
true
;
set_error_handler
(
array
(
__CLASS__
,
'handleError'
)
)
;
set_exception_handler
(
array
(
__CLASS__
,
'handleException'
)
)
;
}
/**
* Provide an optional listener callback which will receive all errors,
* exceptions and debugging messages. It can then print them to a web console,
* for example.
*
* See @{function:phutil_error_listener_example} for details about the
* callback parameters and operation.
*
* @return void
* @task config
*/
public
static
function
setErrorListener
(
$listener
)
{
self
::
$errorListener
=
$listener
;
}
/* -( Exception Utilities )------------------------------------------------ */
/**
* Gets the previous exception of a nested exception. Prior to PHP 5.3 you
* can use @{class:PhutilProxyException} to nest exceptions; after PHP 5.3
* all exceptions are nestable.
*
* @param Exception|Throwable Exception to unnest.
* @return Exception|Throwable|null Previous exception, if one exists.
* @task exutil
*/
public
static
function
getPreviousException
(
$ex
)
{
if
(
method_exists
(
$ex
,
'getPrevious'
)
)
{
return
$ex
->
getPrevious
(
)
;
}
if
(
method_exists
(
$ex
,
'getPreviousException'
)
)
{
return
$ex
->
getPreviousException
(
)
;
}
return
null
;
}
/**
* Find the most deeply nested exception from a possibly-nested exception.
*
* @param Exception|Throwable A possibly-nested exception.
* @return Exception|Throwable Deepest exception in the nest.
* @task exutil
*/
public
static
function
getRootException
(
$ex
)
{
$root
=
$ex
;
while
(
self
::
getPreviousException
(
$root
)
)
{
$root
=
self
::
getPreviousException
(
$root
)
;
}
return
$root
;
}
/* -( Trapping Errors )---------------------------------------------------- */
/**
* Adds an error trap. Normally you should not invoke this directly;
* @{class:PhutilErrorTrap} registers itself on construction.
*
* @param PhutilErrorTrap Trap to add.
* @return void
* @task trap
*/
public
static
function
addErrorTrap
(
PhutilErrorTrap
$trap
)
{
$key
=
$trap
->
getTrapKey
(
)
;
self
::
$traps
[
$key
]
=
$trap
;
}
/**
* Removes an error trap. Normally you should not invoke this directly;
* @{class:PhutilErrorTrap} deregisters itself on destruction.
*
* @param PhutilErrorTrap Trap to remove.
* @return void
* @task trap
*/
public
static
function
removeErrorTrap
(
PhutilErrorTrap
$trap
)
{
$key
=
$trap
->
getTrapKey
(
)
;
unset
(
self
::
$traps
[
$key
]
)
;
}
/* -( Internals )---------------------------------------------------------- */
/**
* Determine if PhutilErrorHandler has been initialized.
*
* @return bool True if initialized.
* @task internal
*/
public
static
function
hasInitialized
(
)
{
return
self
::
$initialized
;
}
/**
* Handles PHP errors and dispatches them forward. This is a callback for
* ##set_error_handler()##. You should not call this function directly; use
* @{function:phlog} to print debugging messages or ##trigger_error()## to
* trigger PHP errors.
*
* This handler converts E_RECOVERABLE_ERROR messages from violated typehints
* into @{class:InvalidArgumentException}s.
*
* This handler converts other E_RECOVERABLE_ERRORs into
* @{class:RuntimeException}s.
*
* This handler converts E_NOTICE messages from uses of undefined variables
* into @{class:RuntimeException}s.
*
* @param int Error code.
* @param string Error message.
* @param string File where the error occurred.
* @param int Line on which the error occurred.
* @param wild Error context information.
* @return void
* @task internal
*/
public
static
function
handleError
(
$num
,
$str
,
$file
,
$line
,
$ctx
=
null
)
{
foreach
(
self
::
$traps
as
$trap
)
{
$trap
->
addError
(
$num
,
$str
,
$file
,
$line
)
;
}
if
(
(
error_reporting
(
)
&
$num
)
==
0
)
{
// Respect the use of "@" to silence warnings: if this error was
// emitted from a context where "@" was in effect, the
// value returned by error_reporting() will be 0. This is the
// recommended way to check for this, see set_error_handler() docs
// on php.net.
return
false
;
}
// See T13499. If this is a user error arising from "trigger_error()" or
// similar, route it through normal error handling: this is probably the
// best match to authorial intent, since the code could choose to throw
// an exception instead if it wanted that behavior. Phabricator does not
// use "trigger_error()" so we never normally expect to reach this
// block in first-party code.
if
(
(
$num
===
E_USER_ERROR
)
||
(
$num
===
E_USER_WARNING
)
||
(
$num
===
E_USER_NOTICE
)
)
{
$trace
=
debug_backtrace
(
)
;
array_shift
(
$trace
)
;
self
::
dispatchErrorMessage
(
self
::
ERROR
,
$str
,
array
(
'file'
=>
$file
,
'line'
=>
$line
,
'error_code'
=>
$num
,
'trace'
=>
$trace
,
)
)
;
return
;
}
// Convert typehint failures into exceptions.
if
(
preg_match
(
'/^Argument (\d+) passed to (\S+) must be/'
,
$str
)
)
{
throw
new
InvalidArgumentException
(
$str
)
;
}
// Convert other E_RECOVERABLE_ERRORs into generic runtime exceptions.
if
(
$num
==
E_RECOVERABLE_ERROR
)
{
throw
new
RuntimeException
(
$str
)
;
}
// Convert uses of undefined variables into exceptions.
if
(
preg_match
(
'/^Undefined variable: /'
,
$str
)
)
{
throw
new
RuntimeException
(
$str
)
;
}
// Convert uses of undefined properties into exceptions.
if
(
preg_match
(
'/^Undefined property: /'
,
$str
)
)
{
throw
new
RuntimeException
(
$str
)
;
}
// Convert undefined constants into exceptions. Usually this means there
// is a missing `$` and the program is horribly broken.
if
(
preg_match
(
'/^Use of undefined constant /'
,
$str
)
)
{
throw
new
RuntimeException
(
$str
)
;
}
// Convert undefined indexes into exceptions.
if
(
preg_match
(
'/^Undefined index: /'
,
$str
)
)
{
throw
new
RuntimeException
(
$str
)
;
}
// Convert undefined offsets into exceptions.
if
(
preg_match
(
'/^Undefined offset: /'
,
$str
)
)
{
throw
new
RuntimeException
(
$str
)
;
}
// See T13499. Convert all other runtime errors not handled in a more
// specific way into runtime exceptions.
throw
new
RuntimeException
(
$str
)
;
}
/**
* Handles PHP exceptions and dispatches them forward. This is a callback for
* ##set_exception_handler()##. You should not call this function directly;
* to print exceptions, pass the exception object to @{function:phlog}.
*
* @param Exception|Throwable Uncaught exception object.
* @return void
* @task internal
*/
public
static
function
handleException
(
$ex
)
{
self
::
dispatchErrorMessage
(
self
::
EXCEPTION
,
$ex
,
array
(
'file'
=>
$ex
->
getFile
(
)
,
'line'
=>
$ex
->
getLine
(
)
,
'trace'
=>
self
::
getExceptionTrace
(
$ex
)
,
'catch_trace'
=>
debug_backtrace
(
)
,
)
)
;
// Normally, PHP exits with code 255 after an uncaught exception is thrown.
// However, if we install an exception handler (as we have here), it exits
// with code 0 instead. Script execution terminates after this function
// exits in either case, so exit explicitly with the correct exit code.
exit
(
255
)
;
}
/**
* Output a stacktrace to the PHP error log.
*
* @param trace A stacktrace, e.g. from debug_backtrace();
* @return void
* @task internal
*/
public
static
function
outputStacktrace
(
$trace
)
{
$lines
=
explode
(
"\n"
,
self
::
formatStacktrace
(
$trace
)
)
;
foreach
(
$lines
as
$line
)
{
error_log
(
$line
)
;
}
}
/**
* Format a stacktrace for output.
*
* @param trace A stacktrace, e.g. from debug_backtrace();
* @return string Human-readable trace.
* @task internal
*/
public
static
function
formatStacktrace
(
$trace
)
{
$result
=
array
(
)
;
$libinfo
=
self
::
getLibraryVersions
(
)
;
if
(
$libinfo
)
{
foreach
(
$libinfo
as
$key
=>
$dict
)
{
$info
=
array
(
)
;
foreach
(
$dict
as
$dkey
=>
$dval
)
{
$info
[
]
=
$dkey
.
'='
.
$dval
;
}
$libinfo
[
$key
]
=
$key
.
'('
.
implode
(
', '
,
$info
)
.
')'
;
}
$result
[
]
=
implode
(
', '
,
$libinfo
)
;
}
foreach
(
$trace
as
$key
=>
$entry
)
{
$line
=
' #'
.
$key
.
' '
;
if
(
!
empty
(
$entry
[
'xid'
]
)
)
{
if
(
$entry
[
'xid'
]
!=
1
)
{
$line
.=
'<#'
.
$entry
[
'xid'
]
.
'> '
;
}
}
if
(
isset
(
$entry
[
'class'
]
)
)
{
$line
.=
$entry
[
'class'
]
.
'::'
;
}
$line
.=
idx
(
$entry
,
'function'
,
''
)
;
if
(
isset
(
$entry
[
'args'
]
)
)
{
$args
=
array
(
)
;
foreach
(
$entry
[
'args'
]
as
$arg
)
{
// NOTE: Print out object types, not values. Values sometimes contain
// sensitive information and are usually not particularly helpful
// for debugging.
$type
=
(
gettype
(
$arg
)
==
'object'
)
?
get_class
(
$arg
)
:
gettype
(
$arg
)
;
$args
[
]
=
$type
;
}
$line
.=
'('
.
implode
(
', '
,
$args
)
.
')'
;
}
if
(
isset
(
$entry
[
'file'
]
)
)
{
$file
=
self
::
adjustFilePath
(
$entry
[
'file'
]
)
;
$line
.=
' called at ['
.
$file
.
':'
.
$entry
[
'line'
]
.
']'
;
}
$result
[
]
=
$line
;
}
return
implode
(
"\n"
,
$result
)
;
}
/**
* All different types of error messages come here before they are
* dispatched to the listener; this method also prints them to the PHP error
* log.
*
* @param const Event type constant.
* @param wild Event value.
* @param dict Event metadata.
* @return void
* @task internal
*/
public
static
function
dispatchErrorMessage
(
$event
,
$value
,
$metadata
)
{
$timestamp
=
date
(
'Y-m-d H:i:s'
)
;
switch
(
$event
)
{
case
self
::
ERROR
:
$default_message
=
sprintf
(
'[%s] ERROR %d: %s at [%s:%d]'
,
$timestamp
,
$metadata
[
'error_code'
]
,
$value
,
$metadata
[
'file'
]
,
$metadata
[
'line'
]
)
;
$metadata
[
'default_message'
]
=
$default_message
;
error_log
(
$default_message
)
;
self
::
outputStacktrace
(
$metadata
[
'trace'
]
)
;
break
;
case
self
::
EXCEPTION
:
$messages
=
array
(
)
;
$current
=
$value
;
do
{
$messages
[
]
=
'('
.
get_class
(
$current
)
.
') '
.
$current
->
getMessage
(
)
;
}
while
(
$current
=
self
::
getPreviousException
(
$current
)
)
;
$messages
=
implode
(
' {>} '
,
$messages
)
;
if
(
strlen
(
$messages
)
>
4096
)
{
$messages
=
substr
(
$messages
,
0
,
4096
)
.
'...'
;
}
$default_message
=
sprintf
(
'[%s] EXCEPTION: %s at [%s:%d]'
,
$timestamp
,
$messages
,
self
::
adjustFilePath
(
self
::
getRootException
(
$value
)
->
getFile
(
)
)
,
self
::
getRootException
(
$value
)
->
getLine
(
)
)
;
$metadata
[
'default_message'
]
=
$default_message
;
error_log
(
$default_message
)
;
self
::
outputStacktrace
(
$metadata
[
'trace'
]
)
;
break
;
case
self
::
PHLOG
:
$default_message
=
sprintf
(
'[%s] PHLOG: %s at [%s:%d]'
,
$timestamp
,
PhutilReadableSerializer
::
printShort
(
$value
)
,
$metadata
[
'file'
]
,
$metadata
[
'line'
]
)
;
$metadata
[
'default_message'
]
=
$default_message
;
error_log
(
$default_message
)
;
break
;
default
:
error_log
(
pht
(
'Unknown event %s'
,
$event
)
)
;
break
;
}
if
(
self
::
$errorListener
)
{
static
$handling_error
;
if
(
$handling_error
)
{
error_log
(
'Error handler was reentered, some errors were not passed to the '
.
'listener.'
)
;
return
;
}
$handling_error
=
true
;
call_user_func
(
self
::
$errorListener
,
$event
,
$value
,
$metadata
)
;
$handling_error
=
false
;
}
}
public
static
function
adjustFilePath
(
$path
)
{
// Compute known library locations so we can emit relative paths if the
// file resides inside a known library. This is a little cleaner to read,
// and limits the number of false positives we get about full path
// disclosure via HackerOne.
$bootloader
=
PhutilBootloader
::
getInstance
(
)
;
$libraries
=
$bootloader
->
getAllLibraries
(
)
;
$roots
=
array
(
)
;
foreach
(
$libraries
as
$library
)
{
$root
=
$bootloader
->
getLibraryRoot
(
$library
)
;
// For these libraries, the effective root is one level up.
switch
(
$library
)
{
case
'arcanist'
:
case
'phorge'
:
case
'phabricator'
:
$root
=
dirname
(
$root
)
;
break
;
}
if
(
!
strncmp
(
$root
,
$path
,
strlen
(
$root
)
)
)
{
return
'<'
.
$library
.
'>'
.
substr
(
$path
,
strlen
(
$root
)
)
;
}
}
return
$path
;
}
public
static
function
getLibraryVersions
(
)
{
$libinfo
=
array
(
)
;
$bootloader
=
PhutilBootloader
::
getInstance
(
)
;
foreach
(
$bootloader
->
getAllLibraries
(
)
as
$library
)
{
$root
=
phutil_get_library_root
(
$library
)
;
$try_paths
=
array
(
$root
,
dirname
(
$root
)
,
)
;
$libinfo
[
$library
]
=
array
(
)
;
$get_refs
=
array
(
'master'
)
;
foreach
(
$try_paths
as
$try_path
)
{
// Try to read what the HEAD of the repository is pointed at. This is
// normally the name of a branch ("ref").
$try_file
=
$try_path
.
'/.git/HEAD'
;
if
(
@
file_exists
(
$try_file
)
)
{
$head
=
@
file_get_contents
(
$try_file
)
;
$matches
=
null
;
if
(
preg_match
(
'(^ref: refs/heads/(.*)$)'
,
trim
(
$head
)
,
$matches
)
)
{
$libinfo
[
$library
]
[
'head'
]
=
trim
(
$matches
[
1
]
)
;
$get_refs
[
]
=
trim
(
$matches
[
1
]
)
;
}
else
{
$libinfo
[
$library
]
[
'head'
]
=
trim
(
$head
)
;
}
break
;
}
}
// Try to read which commit relevant branch heads are at.
foreach
(
array_unique
(
$get_refs
)
as
$ref
)
{
foreach
(
$try_paths
as
$try_path
)
{
$try_file
=
$try_path
.
'/.git/refs/heads/'
.
$ref
;
if
(
@
file_exists
(
$try_file
)
)
{
$hash
=
@
file_get_contents
(
$try_file
)
;
if
(
$hash
)
{
$libinfo
[
$library
]
[
'ref.'
.
$ref
]
=
substr
(
trim
(
$hash
)
,
0
,
12
)
;
break
;
}
}
}
}
// Look for extension files.
$custom
=
@
scandir
(
$root
.
'/extensions/'
)
;
if
(
$custom
)
{
$count
=
0
;
foreach
(
$custom
as
$custom_path
)
{
if
(
preg_match
(
'/\.php$/'
,
$custom_path
)
)
{
$count
++
;
}
}
if
(
$count
)
{
$libinfo
[
$library
]
[
'custom'
]
=
$count
;
}
}
}
ksort
(
$libinfo
)
;
return
$libinfo
;
}
/**
* Get a full trace across all proxied and aggregated exceptions.
*
* This attempts to build a set of stack frames which completely represent
* all of the places an exception came from, even if it came from multiple
* origins and has been aggregated or proxied.
*
* @param Exception|Throwable Exception to retrieve a trace for.
* @return list<wild> List of stack frames.
*/
public
static
function
getExceptionTrace
(
$ex
)
{
$id
=
1
;
// Keep track of discovered exceptions which we need to build traces for.
$stack
=
array
(
array
(
$id
,
$ex
)
,
)
;
$frames
=
array
(
)
;
while
(
$info
=
array_shift
(
$stack
)
)
{
list
(
$xid
,
$ex
)
=
$info
;
// We're going from top-level exception down in bredth-first order, but
// want to build a trace in approximately standard order (deepest part of
// the call stack to most shallow) so we need to reverse each list of
// frames and then reverse everything at the end.
$ex_frames
=
array_reverse
(
$ex
->
getTrace
(
)
)
;
$ex_frames
=
array_values
(
$ex_frames
)
;
$last_key
=
(
count
(
$ex_frames
)
-
1
)
;
foreach
(
$ex_frames
as
$frame_key
=>
$frame
)
{
$frame
[
'xid'
]
=
$xid
;
// If this is a child/previous exception and we're on the deepest frame
// and missing file/line data, fill it in from the exception itself.
if
(
$xid
>
1
&&
(
$frame_key
==
$last_key
)
)
{
if
(
empty
(
$frame
[
'file'
]
)
)
{
$frame
[
'file'
]
=
$ex
->
getFile
(
)
;
$frame
[
'line'
]
=
$ex
->
getLine
(
)
;
}
}
// Since the exceptions are likely to share the most shallow frames,
// try to add those to the trace only once.
if
(
isset
(
$frame
[
'file'
]
)
&&
isset
(
$frame
[
'line'
]
)
)
{
$signature
=
$frame
[
'file'
]
.
':'
.
$frame
[
'line'
]
;
if
(
empty
(
$frames
[
$signature
]
)
)
{
$frames
[
$signature
]
=
$frame
;
}
}
else
{
$frames
[
]
=
$frame
;
}
}
// If this is a proxy exception, add the proxied exception.
$prev
=
self
::
getPreviousException
(
$ex
)
;
if
(
$prev
)
{
$stack
[
]
=
array
(
++
$id
,
$prev
)
;
}
// If this is an aggregate exception, add the child exceptions.
if
(
$ex
instanceof
PhutilAggregateException
)
{
foreach
(
$ex
->
getExceptions
(
)
as
$child
)
{
$stack
[
]
=
array
(
++
$id
,
$child
)
;
}
}
}
return
array_values
(
array_reverse
(
$frames
)
)
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Jan 19, 19:25 (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1127945
Default Alt Text
PhutilErrorHandler.php (18 KB)
Attached To
Mode
rARC Arcanist
Attached
Detach File
Event Timeline
Log In to Comment