2020-04-09 14:32:40 +00:00
< ? php
2020-04-16 13:03:15 +00:00
/**
* CodeHacker class file .
*
* @ package WooCommerce / Testing
*/
//phpcs:disable WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors.Discouraged
2020-04-09 14:32:40 +00:00
2020-04-13 07:32:19 +00:00
namespace Automattic\WooCommerce\Testing\CodeHacking ;
2020-04-16 13:03:15 +00:00
use \ReflectionObject ;
use \ReflectionFunction ;
use \ReflectionException ;
2020-04-09 14:32:40 +00:00
/**
* CodeHacker - allows to hack ( alter on the fly ) the content of PHP code files .
*
* Based on BypassFinals : https :// github . com / dg / bypass - finals
*
* How to use :
*
* 1. Register hacks using CodeHacker :: add_hack ( hack ) . A hack is either :
* - A function with 'hack($code, $path)' signature , or
* - An object having a public 'hack($code, $path)' method .
*
* Where $code is a string containing the code to hack , and $path is the full path of the file
* containing the code . The function / method must return a string with the code already hacked .
*
* 2. Run CodeHacker :: enable ()
*
* For using with PHPUnit , see CodeHackerTestHook .
*/
2020-05-18 08:07:20 +00:00
final class CodeHacker {
2020-04-09 14:32:40 +00:00
2020-05-18 08:07:20 +00:00
const PROTOCOL = 'file' ;
const HACK_CALLBACK_ARGUMENT_COUNT = 2 ;
2020-04-09 14:32:40 +00:00
2020-04-23 14:21:46 +00:00
/**
* Value of " context " parameter to be passed to the native PHP filesystem related functions .
*
* @ var mixed
*/
2020-04-09 14:32:40 +00:00
public $context ;
2020-04-23 14:21:46 +00:00
/**
* File handle of the file that is open .
*
* @ var mixed
*/
2020-04-09 14:32:40 +00:00
private $handle ;
2020-04-23 14:21:46 +00:00
/**
* Optional white list of files to hack , if empty all the files will be hacked .
*
* @ var array
*/
2020-04-16 13:03:15 +00:00
private static $path_white_list = array ();
2020-04-09 14:32:40 +00:00
2020-04-23 14:21:46 +00:00
/**
* Registered hacks .
*
* @ var array
*/
2020-04-09 14:32:40 +00:00
private static $hacks = array ();
2020-05-06 12:40:17 +00:00
/**
* Registered persistent hacks .
*
* @ var array
*/
private static $persistent_hacks = array ();
2020-04-23 14:21:46 +00:00
/**
* Is the code hacker enabled ? .
*
* @ var bool
*/
2020-04-12 11:09:57 +00:00
private static $enabled = false ;
2020-04-16 13:03:15 +00:00
/**
* Enable the code hacker .
*/
2020-04-09 14:32:40 +00:00
public static function enable () {
2020-04-12 11:09:57 +00:00
if ( ! self :: $enabled ) {
stream_wrapper_unregister ( self :: PROTOCOL );
stream_wrapper_register ( self :: PROTOCOL , __CLASS__ );
self :: $enabled = true ;
}
}
2020-04-16 13:03:15 +00:00
/**
* Disable the code hacker .
*/
2020-05-06 12:40:17 +00:00
public static function disable () {
2020-04-12 11:09:57 +00:00
if ( self :: $enabled ) {
stream_wrapper_restore ( self :: PROTOCOL );
self :: $enabled = false ;
}
}
2020-04-16 13:03:15 +00:00
/**
2020-05-06 12:40:17 +00:00
* Unregister all the non - persistent registered hacks .
2020-04-16 13:03:15 +00:00
*/
2020-04-12 11:09:57 +00:00
public static function clear_hacks () {
2020-05-06 12:40:17 +00:00
self :: $hacks = self :: $persistent_hacks ;
2020-04-12 11:09:57 +00:00
}
2020-04-16 13:03:15 +00:00
/**
* Check if the code hacker is enabled .
*
* @ return bool True if the code hacker is enabled .
*/
2020-04-12 11:09:57 +00:00
public static function is_enabled () {
return self :: $enabled ;
2020-04-09 14:32:40 +00:00
}
2020-05-06 12:40:17 +00:00
/**
* Check if persistent hacks have been registered .
*
* @ return bool True if persistent hacks have been registered .
*/
public static function has_persistent_hacks () {
return count ( self :: $persistent_hacks ) > 0 ;
}
2020-04-16 13:03:15 +00:00
/**
* Register a new hack .
*
* @ param mixed $hack A function with signature " hack( $code , $path ) " or an object containing a method with that signature .
2020-05-06 12:40:17 +00:00
* @ param bool $persistent If true , the hack will be registered as persistent ( so that clear_hacks will not clear it ) .
2020-04-16 13:03:15 +00:00
* @ throws \Exception Invalid input .
*/
2020-05-06 12:40:17 +00:00
public static function add_hack ( $hack , $persistent = false ) {
2020-04-09 14:32:40 +00:00
if ( ! is_callable ( $hack ) && ! is_object ( $hack ) ) {
2020-05-18 08:07:20 +00:00
throw new \Exception ( " CodeHacker::addhack: Hacks must be either functions, or objects having a 'process( \$ text, \$ path)' method. " );
2020-04-09 14:32:40 +00:00
}
2020-04-16 13:03:15 +00:00
if ( ! self :: is_valid_hack_callback ( $hack ) && ! self :: is_valid_hack_object ( $hack ) ) {
2020-05-18 08:07:20 +00:00
throw new \Exception ( " CodeHacker::addhack: Hacks must be either a function with a 'hack( \$ code, \$ path)' signature, or an object containing a public method 'hack' with that signature. " );
2020-04-16 13:03:15 +00:00
}
2020-04-09 14:32:40 +00:00
2020-05-06 12:40:17 +00:00
if ( $persistent ) {
self :: $persistent_hacks [] = $hack ;
}
2020-04-09 14:32:40 +00:00
self :: $hacks [] = $hack ;
}
2020-04-23 14:21:46 +00:00
/**
* Check if the supplied argument is a valid hack callback ( has two mandatory arguments ) .
*
* @ param mixed $callback Argument to check .
*
* @ return bool true if the argument is a valid hack callback , false otherwise .
* @ throws ReflectionException Error when instantiating ReflectionFunction .
*/
2020-04-16 13:03:15 +00:00
private static function is_valid_hack_callback ( $callback ) {
2020-05-18 08:07:20 +00:00
return is_callable ( $callback ) && HACK_CALLBACK_ARGUMENT_COUNT === ( new ReflectionFunction ( $callback ) ) -> getNumberOfRequiredParameters ();
2020-04-16 13:03:15 +00:00
}
2020-04-23 14:21:46 +00:00
/**
* Check if the supplied argument is a valid hack object ( has a public " hack " method with two mandatory arguments ) .
*
* @ param mixed $callback Argument to check .
*
2020-05-18 08:07:20 +00:00
* @ return bool rue if the argument is a valid hack object , false otherwise .
2020-04-23 14:21:46 +00:00
*/
2020-04-16 13:03:15 +00:00
private static function is_valid_hack_object ( $callback ) {
if ( ! is_object ( $callback ) ) {
return false ;
}
$ro = new ReflectionObject ( ( $callback ) );
try {
$rm = $ro -> getMethod ( 'hack' );
return $rm -> isPublic () && ! $rm -> isStatic () && 2 === $rm -> getNumberOfRequiredParameters ();
} catch ( ReflectionException $exception ) {
return false ;
}
}
/**
* Set the white list of files to hack . If note set , all the PHP files will be hacked .
*
* @ param array $path_white_list Paths of the files to hack , can be relative paths .
*/
public static function set_white_list ( array $path_white_list ) {
self :: $path_white_list = $path_white_list ;
2020-04-09 14:32:40 +00:00
}
2020-04-23 14:21:46 +00:00
/**
* Close directory handle .
*/
2020-04-09 14:32:40 +00:00
public function dir_closedir () {
closedir ( $this -> handle );
}
2020-04-23 14:21:46 +00:00
/**
* Open directory handle .
*
* @ param string $path Specifies the URL that was passed to opendir () .
* @ param int $options Whether or not to enforce safe_mode ( 0x04 ) .
*
* @ return bool TRUE on success or FALSE on failure .
*/
2020-04-09 14:32:40 +00:00
public function dir_opendir ( $path , $options ) {
$this -> handle = $this -> context
? $this -> native ( 'opendir' , $path , $this -> context )
: $this -> native ( 'opendir' , $path );
return ( bool ) $this -> handle ;
}
2020-04-23 14:21:46 +00:00
/**
* Read entry from directory handle .
*
* @ return false | string string representing the next filename , or FALSE if there is no next file .
*/
2020-04-09 14:32:40 +00:00
public function dir_readdir () {
2020-04-16 13:03:15 +00:00
return readdir ( $this -> handle );
2020-04-09 14:32:40 +00:00
}
2020-04-23 14:21:46 +00:00
/**
* Rewind directory handle .
*
* @ return TRUE on success or FALSE on failure .
*/
2020-04-09 14:32:40 +00:00
public function dir_rewinddir () {
return rewinddir ( $this -> handle );
}
2020-04-23 14:21:46 +00:00
/**
* Create a directory .
*
* @ param string $path Directory which should be created .
* @ param int $mode The value passed to mkdir () .
* @ param int $options A bitwise mask of values , such as STREAM_MKDIR_RECURSIVE .
*
* @ return bool TRUE on success or FALSE on failure .
*/
2020-04-09 14:32:40 +00:00
public function mkdir ( $path , $mode , $options ) {
$recursive = ( bool ) ( $options & STREAM_MKDIR_RECURSIVE );
return $this -> native ( 'mkdir' , $path , $mode , $recursive , $this -> context );
}
2020-04-23 14:21:46 +00:00
/**
* Renames a file or directory .
*
* @ param string $path_from The URL to the current file .
* @ param string $path_to The URL which the path_from should be renamed to .
*
* @ return bool TRUE on success or FALSE on failure .
*/
2020-04-16 13:03:15 +00:00
public function rename ( $path_from , $path_to ) {
return $this -> native ( 'rename' , $path_from , $path_to , $this -> context );
2020-04-09 14:32:40 +00:00
}
2020-04-23 14:21:46 +00:00
/**
* Removes a directory .
*
* @ param string $path The directory URL which should be removed .
* @ param int $options A bitwise mask of values , such as STREAM_MKDIR_RECURSIVE .
*
* @ return bool TRUE on success or FALSE on failure .
*/
2020-04-09 14:32:40 +00:00
public function rmdir ( $path , $options ) {
return $this -> native ( 'rmdir' , $path , $this -> context );
}
2020-04-23 14:21:46 +00:00
/**
* Retrieve the underlying resource .
*
* @ param mixed $cast_as Can be STREAM_CAST_FOR_SELECT when stream_select () is calling stream_cast () or STREAM_CAST_AS_STREAM when stream_cast () is called for other uses .
*
* @ return mixed The underlying stream resource used by the wrapper , or FALSE .
*/
2020-04-16 13:03:15 +00:00
public function stream_cast ( $cast_as ) {
2020-04-09 14:32:40 +00:00
return $this -> handle ;
}
2020-04-23 14:21:46 +00:00
/**
* Close a resource .
*/
2020-04-09 14:32:40 +00:00
public function stream_close () {
fclose ( $this -> handle );
}
2020-04-23 14:21:46 +00:00
/**
* Tests for end - of - file on a file pointer .
*
* @ return bool TRUE if the read / write position is at the end of the stream and if no more data is available to be read , or FALSE otherwise .
*/
2020-04-09 14:32:40 +00:00
public function stream_eof () {
return feof ( $this -> handle );
}
2020-04-23 14:21:46 +00:00
/**
* Flushes the output .
*
* @ return bool TRUE if the cached data was successfully stored ( or if there was no data to store ), or FALSE if the data could not be stored .
*/
2020-04-09 14:32:40 +00:00
public function stream_flush () {
return fflush ( $this -> handle );
}
2020-04-23 14:21:46 +00:00
/**
* Advisory file locking .
*
* @ param int $operation LOCK_SH , LOCK_EX , LOCK_UN , or LOCK_NB .
*
* @ return bool TRUE on success or FALSE on failure .
*/
2020-04-09 14:32:40 +00:00
public function stream_lock ( $operation ) {
return $operation
? flock ( $this -> handle , $operation )
: true ;
}
2020-04-23 14:21:46 +00:00
/**
* Change stream metadata .
*
* @ param string $path The file path or URL to set metadata . Note that in the case of a URL , it must be a :// delimited URL . Other URL forms are not supported .
* @ param int $option STREAM_META_TOUCH , STREAM_META_OWNER_NAME , STREAM_META_OWNER , STREAM_META_GROUP_NAME , STREAM_META_GROUP , or STREAM_META_ACCESS .
* @ param mixed $value Depends on $option .
*
* @ return bool TRUE on success or FALSE on failure . If option is not implemented , FALSE should be returned .
*/
2020-04-09 14:32:40 +00:00
public function stream_metadata ( $path , $option , $value ) {
switch ( $option ) {
case STREAM_META_TOUCH :
$value += array ( null , null );
return $this -> native ( 'touch' , $path , $value [ 0 ], $value [ 1 ] );
case STREAM_META_OWNER_NAME :
case STREAM_META_OWNER :
return $this -> native ( 'chown' , $path , $value );
case STREAM_META_GROUP_NAME :
case STREAM_META_GROUP :
return $this -> native ( 'chgrp' , $path , $value );
case STREAM_META_ACCESS :
return $this -> native ( 'chmod' , $path , $value );
}
}
2020-04-23 14:21:46 +00:00
/**
* Opens file or URL . Note that this is where the hacking actually happens .
*
* @ param string $path Specifies the URL that was passed to the original function .
* @ param string $mode The mode used to open the file , as detailed for fopen () .
* @ param int $options Holds additional flags set by the streams API : STREAM_USE_PATH , STREAM_REPORT_ERRORS .
* @ param string $opened_path If the path is opened successfully , and STREAM_USE_PATH is set in options , opened_path should be set to the full path of the file / resource that was actually opened .
*
* @ return bool TRUE on success or FALSE on failure .
*/
2020-04-16 13:03:15 +00:00
public function stream_open ( $path , $mode , $options , & $opened_path ) {
$use_path = ( bool ) ( $options & STREAM_USE_PATH );
if ( 'rb' === $mode && self :: path_in_white_list ( $path ) && 'php' === pathinfo ( $path , PATHINFO_EXTENSION ) ) {
$content = $this -> native ( 'file_get_contents' , $path , $use_path , $this -> context );
if ( false === $content ) {
2020-04-09 14:32:40 +00:00
return false ;
}
$modified = self :: hack ( $content , $path );
if ( $modified !== $content ) {
$this -> handle = tmpfile ();
$this -> native ( 'fwrite' , $this -> handle , $modified );
$this -> native ( 'fseek' , $this -> handle , 0 );
return true ;
}
}
$this -> handle = $this -> context
2020-04-16 13:03:15 +00:00
? $this -> native ( 'fopen' , $path , $mode , $use_path , $this -> context )
: $this -> native ( 'fopen' , $path , $mode , $use_path );
2020-04-09 14:32:40 +00:00
return ( bool ) $this -> handle ;
}
2020-04-23 14:21:46 +00:00
/**
* Read from stream .
*
* @ param int $count How many bytes of data from the current position should be returned .
*
* @ return false | string If there are less than count bytes available , return as many as are available . If no more data is available , return either FALSE or an empty string .
*/
2020-04-09 14:32:40 +00:00
public function stream_read ( $count ) {
return fread ( $this -> handle , $count );
}
2020-04-23 14:21:46 +00:00
/**
* Seeks to specific location in a stream .
*
* @ param int $offset The stream offset to seek to .
* @ param int $whence SEEK_SET , SEEK_CUR , or SEEK_END .
*
* @ return bool TRUE if the position was updated , FALSE otherwise .
*/
2020-04-09 14:32:40 +00:00
public function stream_seek ( $offset , $whence = SEEK_SET ) {
return fseek ( $this -> handle , $offset , $whence ) === 0 ;
}
2020-04-23 14:21:46 +00:00
/**
* Change stream options .
*
* @ param int $option STREAM_OPTION_BLOCKING , STREAM_OPTION_READ_TIMEOUT , or STREAM_OPTION_WRITE_BUFFER .
* @ param int $arg1 Depends on $option .
* @ param int $arg2 Depends on $option .
*/
2020-04-09 14:32:40 +00:00
public function stream_set_option ( $option , $arg1 , $arg2 ) {
}
2020-04-23 14:21:46 +00:00
/**
* Retrieve information about a file resource .
*
* @ return array See stat () .
*/
2020-04-09 14:32:40 +00:00
public function stream_stat () {
2020-04-16 13:03:15 +00:00
return fstat ( $this -> handle );
2020-04-09 14:32:40 +00:00
}
2020-04-23 14:21:46 +00:00
/**
* Retrieve the current position of a stream .
*
* @ return false | int The current position of the stream .
*/
2020-04-09 14:32:40 +00:00
public function stream_tell () {
2020-04-16 13:03:15 +00:00
return ftell ( $this -> handle );
2020-04-09 14:32:40 +00:00
}
2020-04-23 14:21:46 +00:00
/**
* Truncate stream .
*
* @ param int $new_size The new size .
*
* @ return bool TRUE on success or FALSE on failure .
*/
2020-04-16 13:03:15 +00:00
public function stream_truncate ( $new_size ) {
return ftruncate ( $this -> handle , $new_size );
2020-04-09 14:32:40 +00:00
}
2020-04-23 14:21:46 +00:00
/**
* Write to stream .
*
* @ param string $data Should be stored into the underlying stream .
*
* @ return false | int The number of bytes that were successfully stored , or 0 if none could be stored .
*/
2020-04-09 14:32:40 +00:00
public function stream_write ( $data ) {
return fwrite ( $this -> handle , $data );
}
2020-04-23 14:21:46 +00:00
/**
* Delete a file .
*
* @ param string $path The file URL which should be deleted .
*
* @ return bool TRUE on success or FALSE on failure .
*/
2020-04-09 14:32:40 +00:00
public function unlink ( $path ) {
return $this -> native ( 'unlink' , $path );
}
2020-04-23 14:21:46 +00:00
/**
* Retrieve information about a file .
*
* @ param string $path The file path or URL to stat . Note that in the case of a URL , it must be a :// delimited URL . Other URL forms are not supported .
* @ param int $flags Holds additional flags set by the streams API . It can hold one or more of the following values OR ' d together .
*
* @ return mixed Should return as many elements as stat () does . Unknown or unavailable values should be set to a rational value ( usually 0 ) . Pay special attention to mode as documented under stat () .
*/
2020-04-09 14:32:40 +00:00
public function url_stat ( $path , $flags ) {
$func = $flags & STREAM_URL_STAT_LINK ? 'lstat' : 'stat' ;
return $flags & STREAM_URL_STAT_QUIET
? @ $this -> native ( $func , $path )
: $this -> native ( $func , $path );
}
2020-04-23 14:21:46 +00:00
/**
* Executes a native PHP function .
*
* @ param string $func Name of the function to execute . Pass the arguments for the PHP function after this one .
*
* @ return mixed Return value from the native PHP function .
*/
2020-04-09 14:32:40 +00:00
private function native ( $func ) {
stream_wrapper_restore ( self :: PROTOCOL );
$res = call_user_func_array ( $func , array_slice ( func_get_args (), 1 ) );
stream_wrapper_unregister ( self :: PROTOCOL );
stream_wrapper_register ( self :: PROTOCOL , __CLASS__ );
return $res ;
}
2020-04-23 14:21:46 +00:00
/**
* Apply the reigstered hacks to the contents of a file .
*
* @ param string $code Code content to hack .
* @ param string $path Path of the file being hacked .
*
* @ return string The code after applying all the registered hacks .
*/
2020-04-09 14:32:40 +00:00
private static function hack ( $code , $path ) {
foreach ( self :: $hacks as $hack ) {
if ( is_callable ( $hack ) ) {
$code = call_user_func ( $hack , $code , $path );
} else {
$code = $hack -> hack ( $code , $path );
}
}
return $code ;
}
2020-04-23 14:21:46 +00:00
/**
* Check if a file path is in the white list .
*
* @ param string $path File path to check .
*
* @ return bool TRUE if there ' s an entry in the white list that ends with $path , FALSE otherwise .
*/
2020-04-16 13:03:15 +00:00
private static function path_in_white_list ( $path ) {
if ( empty ( self :: $path_white_list ) ) {
2020-04-09 14:32:40 +00:00
return true ;
}
2020-04-16 13:03:15 +00:00
foreach ( self :: $path_white_list as $white_list_item ) {
if ( substr ( $path , - strlen ( $white_list_item ) ) === $white_list_item ) {
2020-04-09 14:32:40 +00:00
return true ;
}
}
return false ;
}
}
2020-04-16 13:03:15 +00:00
//phpcs:enable WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors.Discouraged