Browse Source

Merge pull request #9642 from thinkingmedia/thinkingmedia/debug-dump-filtering

Debugger: adds configurable masking of array/object properties
Mark Story 9 years ago
parent
commit
0f63f82e9f
2 changed files with 118 additions and 5 deletions
  1. 73 5
      src/Error/Debugger.php
  2. 45 0
      tests/TestCase/Error/DebuggerTest.php

+ 73 - 5
src/Error/Debugger.php

@@ -14,6 +14,7 @@
  */
 namespace Cake\Error;
 
+use Cake\Core\InstanceConfigTrait;
 use Cake\Log\Log;
 use Cake\Utility\Hash;
 use Cake\Utility\Security;
@@ -32,6 +33,16 @@ use ReflectionProperty;
  */
 class Debugger
 {
+    use InstanceConfigTrait;
+
+    /**
+     * Default configuration
+     *
+     * @var array
+     */
+    protected $_defaultConfig = [
+        'outputMask' => []
+    ];
 
     /**
      * A list of errors generated by the application.
@@ -152,7 +163,7 @@ class Debugger
      * Returns a reference to the Debugger singleton object instance.
      *
      * @param string|null $class Class name.
-     * @return object
+     * @return object|Debugger
      */
     public static function getInstance($class = null)
     {
@@ -170,6 +181,50 @@ class Debugger
     }
 
     /**
+     * Read or write configuration options for the Debugger instance.
+     *
+     * @param string|array|null $key The key to get/set, or a complete array of configs.
+     * @param mixed|null $value The value to set.
+     * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true.
+     * @return mixed Config value being read, or the object itself on write operations.
+     * @throws \Cake\Core\Exception\Exception When trying to set a key that is invalid.
+     */
+    public static function configInstance($key = null, $value = null, $merge = true)
+    {
+        if (is_array($key) || func_num_args() >= 2) {
+            return static::getInstance()->config($key, $value, $merge);
+        }
+
+        return static::getInstance()->config($key);
+    }
+
+    /**
+     * Reads the current output masking.
+     *
+     * @return array
+     */
+    public static function outputMask()
+    {
+        return static::configInstance('outputMask');
+    }
+
+    /**
+     * Sets configurable masking of debugger output by property name and array key names.
+     *
+     * ### Example
+     *
+     * Debugger::setOutputMask(['password' => '[*************]');
+     *
+     * @param array $value An array where keys are replaced by their values in output.
+     * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true.
+     * @return void
+     */
+    public static function setOutputMask(array $value, $merge = true)
+    {
+        static::configInstance('outputMask', $value, $merge);
+    }
+
+    /**
      * Recursively formats and outputs the contents of the supplied variable.
      *
      * @param mixed $var The variable to dump.
@@ -505,10 +560,13 @@ class Debugger
         $vars = [];
 
         if ($depth >= 0) {
+            $outputMask = (array)static::outputMask();
             foreach ($var as $key => $val) {
                 // Sniff for globals as !== explodes in < 5.4
                 if ($key === 'GLOBALS' && is_array($val) && isset($val['GLOBALS'])) {
                     $val = '[recursion]';
+                } elseif (array_key_exists($key, $outputMask)) {
+                    $val = (string)$outputMask[$key];
                 } elseif ($val !== $var) {
                     $val = static::_export($val, $depth, $indent);
                 }
@@ -526,7 +584,7 @@ class Debugger
     /**
      * Handles object to string conversion.
      *
-     * @param string $var Object to convert.
+     * @param object $var Object to convert.
      * @param int $depth The current depth, used for tracking recursion.
      * @param int $indent The current indentation level.
      * @return string
@@ -555,9 +613,10 @@ class Debugger
         }
 
         if ($depth > 0) {
+            $outputMask = (array)static::outputMask();
             $objectVars = get_object_vars($var);
             foreach ($objectVars as $key => $value) {
-                $value = static::_export($value, $depth - 1, $indent);
+                $value = array_key_exists($key, $outputMask) ? $outputMask[$key] : static::_export($value, $depth - 1, $indent);
                 $props[] = "$key => " . $value;
             }
 
@@ -575,7 +634,12 @@ class Debugger
 
                     $value = static::_export($property, $depth - 1, $indent);
                     $key = $reflectionProperty->name;
-                    $props[] = sprintf('[%s] %s => %s', $visibility, $key, $value);
+                    $props[] = sprintf(
+                        '[%s] %s => %s',
+                        $visibility,
+                        $key,
+                        array_key_exists($key, $outputMask) ? $outputMask[$key] : $value
+                    );
                 }
             }
 
@@ -605,6 +669,8 @@ class Debugger
             throw new InvalidArgumentException('Invalid Debugger output format.');
         }
         $self->_outputFormat = $format;
+
+        return null;
     }
 
     /**
@@ -748,7 +814,9 @@ class Debugger
         $links = implode(' ', $links);
 
         if (isset($tpl['callback']) && is_callable($tpl['callback'])) {
-            return call_user_func($tpl['callback'], $data, compact('links', 'info'));
+            call_user_func($tpl['callback'], $data, compact('links', 'info'));
+
+            return;
         }
         echo Text::insert($tpl['error'], compact('links', 'info') + $data, $insertOpts);
     }

+ 45 - 0
tests/TestCase/Error/DebuggerTest.php

@@ -36,6 +36,11 @@ class DebuggableThing
     }
 }
 
+class SecurityThing
+{
+    public $password = 'pass1234';
+}
+
 /**
  * DebuggerTest class
  *
@@ -561,4 +566,44 @@ object(Cake\Test\TestCase\Error\DebuggableThing) {
 eos;
         $this->assertEquals($expected, $result);
     }
+
+    /**
+     * Tests reading the output mask settings.
+     */
+    public function testSetOutputMask()
+    {
+        Debugger::setOutputMask(['password' => '[**********]']);
+        $this->assertEquals(['password' => '[**********]'], Debugger::outputMask());
+        Debugger::setOutputMask(['serial' => 'XXXXXX']);
+        $this->assertEquals(['password' => '[**********]', 'serial' => 'XXXXXX'], Debugger::outputMask());
+        Debugger::setOutputMask([], false);
+        $this->assertEquals([], Debugger::outputMask());
+    }
+
+    /**
+     * Tests the masking of an array key.
+     *
+     * @return void
+     */
+    public function testMaskArray()
+    {
+        Debugger::setOutputMask(['password' => '[**********]']);
+        $result = Debugger::exportVar(['password' => 'pass1234']);
+        $expected = "['password'=>[**********]]";
+        $this->assertEquals($expected, preg_replace('/\s+/', '', $result));
+    }
+
+    /**
+     * Tests the masking of an array key.
+     *
+     * @return void
+     */
+    public function testMaskObject()
+    {
+        Debugger::setOutputMask(['password' => '[**********]']);
+        $object = new SecurityThing();
+        $result = Debugger::exportVar($object);
+        $expected = "object(Cake\\Test\\TestCase\\Error\\SecurityThing){password=>[**********]}";
+        $this->assertEquals($expected, preg_replace('/\s+/', '', $result));
+    }
 }