Browse Source

Merge branch 'master' into 4.next

Mark Story 5 years ago
parent
commit
201ea18990

+ 1 - 1
.travis.yml

@@ -75,7 +75,7 @@ script:
 after_success:
   - |
       if [[ $TRAVIS_PHP_VERSION == '7.4' ]]; then
-        wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.1.0/php-coveralls.phar
+        wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.2.0/php-coveralls.phar
         chmod +x php-coveralls.phar
         ./php-coveralls.phar
       fi

+ 1 - 1
composer.json

@@ -111,7 +111,7 @@
             "@phpstan",
             "@psalm"
         ],
-        "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:0.12.36 psalm/phar:~3.13.1 && mv composer.backup composer.json"
+        "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:0.12.38 psalm/phar:~3.13.1 && mv composer.backup composer.json"
     },
     "config": {
         "sort-packages": true,

+ 2 - 0
src/Controller/Controller.php

@@ -82,6 +82,8 @@ use UnexpectedValueException;
  * @property \Cake\Controller\Component\FormProtectionComponent $FormProtection
  * @property \Cake\Controller\Component\PaginatorComponent $Paginator
  * @property \Cake\Controller\Component\RequestHandlerComponent $RequestHandler
+ * @property \Cake\Controller\Component\SecurityComponent $Security
+ * @property \Cake\Controller\Component\AuthComponent $Auth
  * @link https://book.cakephp.org/4/en/controllers.html
  */
 class Controller implements EventListenerInterface, EventDispatcherInterface

+ 23 - 12
src/Error/Debug/HtmlFormatter.php

@@ -105,7 +105,7 @@ class HtmlFormatter implements FormatterInterface
      */
     public function dump(NodeInterface $node): string
     {
-        $html = $this->export($node);
+        $html = $this->export($node, 0);
         $head = '';
         if (!static::$outputHeader) {
             static::$outputHeader = true;
@@ -119,9 +119,10 @@ class HtmlFormatter implements FormatterInterface
      * Convert a tree of NodeInterface objects into HTML
      *
      * @param \Cake\Error\Debug\NodeInterface $var The node tree to dump.
+     * @param int $indent The current indentation level.
      * @return string
      */
-    protected function export(NodeInterface $var): string
+    protected function export(NodeInterface $var, int $indent): string
     {
         if ($var instanceof ScalarNode) {
             switch ($var->getType()) {
@@ -140,10 +141,10 @@ class HtmlFormatter implements FormatterInterface
             }
         }
         if ($var instanceof ArrayNode) {
-            return $this->exportArray($var);
+            return $this->exportArray($var, $indent + 1);
         }
         if ($var instanceof ClassNode || $var instanceof ReferenceNode) {
-            return $this->exportObject($var);
+            return $this->exportObject($var, $indent + 1);
         }
         if ($var instanceof SpecialNode) {
             return $this->style('special', (string)$var->getValue());
@@ -155,25 +156,29 @@ class HtmlFormatter implements FormatterInterface
      * Export an array type object
      *
      * @param \Cake\Error\Debug\ArrayNode $var The array to export.
+     * @param int $indent The current indentation level.
      * @return string Exported array.
      */
-    protected function exportArray(ArrayNode $var): string
+    protected function exportArray(ArrayNode $var, int $indent): string
     {
         $open = '<span class="cake-dbg-array">' .
             $this->style('punct', '[') .
             '<samp class="cake-dbg-array-items">';
         $vars = [];
+        $break = "\n" . str_repeat('  ', $indent);
+        $endBreak = "\n" . str_repeat('  ', $indent - 1);
 
         $arrow = $this->style('punct', ' => ');
         foreach ($var->getChildren() as $item) {
             $val = $item->getValue();
-            $vars[] = '<span class="cake-dbg-array-item">' .
-                $this->export($item->getKey()) . $arrow . $this->export($val) .
+            $vars[] = $break . '<span class="cake-dbg-array-item">' .
+                $this->export($item->getKey(), $indent) . $arrow . $this->export($val, $indent) .
                 $this->style('punct', ',') .
                 '</span>';
         }
 
         $close = '</samp>' .
+            $endBreak .
             $this->style('punct', ']') .
             '</span>';
 
@@ -184,16 +189,19 @@ class HtmlFormatter implements FormatterInterface
      * Handles object to string conversion.
      *
      * @param \Cake\Error\Debug\ClassNode|\Cake\Error\Debug\ReferenceNode $var Object to convert.
+     * @param int $indent The current indentation level.
      * @return string
      * @see \Cake\Error\Debugger::exportVar()
      */
-    protected function exportObject($var): string
+    protected function exportObject($var, int $indent): string
     {
         $objectId = "cake-db-object-{$this->id}-{$var->getId()}";
         $out = sprintf(
             '<span class="cake-dbg-object" id="%s">',
             $objectId
         );
+        $break = "\n" . str_repeat('  ', $indent);
+        $endBreak = "\n" . str_repeat('  ', $indent - 1);
 
         if ($var instanceof ReferenceNode) {
             $link = sprintf(
@@ -224,23 +232,26 @@ class HtmlFormatter implements FormatterInterface
             $visibility = $property->getVisibility();
             $name = $property->getName();
             if ($visibility && $visibility !== 'public') {
-                $props[] = '<span class="cake-dbg-prop">' .
+                $props[] = $break .
+                    '<span class="cake-dbg-prop">' .
                     $this->style('visibility', $visibility) .
                     ' ' .
                     $this->style('property', $name) .
                     $arrow .
-                    $this->export($property->getValue()) .
+                    $this->export($property->getValue(), $indent) .
                 '</span>';
             } else {
-                $props[] = '<span class="cake-dbg-prop">' .
+                $props[] = $break .
+                    '<span class="cake-dbg-prop">' .
                     $this->style('property', $name) .
                     $arrow .
-                    $this->export($property->getValue()) .
+                    $this->export($property->getValue(), $indent) .
                     '</span>';
             }
         }
 
         $end = '</samp>' .
+            $endBreak .
             $this->style('punct', '}') .
             '</span>';
 

+ 52 - 11
src/Error/Debug/dumpHeader.html

@@ -2,6 +2,7 @@
 .cake-debug {
   --color-bg: #ECECE9;
   --color-highlight-bg: #fcf8e3;
+  --color-control-bg: hsla(0, 0%, 50%, 0.2);
 
   --color-orange: #c44f24;
   --color-green: #0b6125;
@@ -21,6 +22,7 @@
   line-height: 16px;
   font-size: 14px;
   margin-bottom: 10px;
+  position: relative;
 }
 .cake-debug:last-child {
   margin-bottom: 0;
@@ -58,21 +60,33 @@ nesting works.
 [data-hidden=true] {
   display: none;
 }
-.cake-dbg-collapse:before {
-  content: "\25b8";
+
+.cake-dbg-collapse {
   display: inline-block;
-  width: 10px;
-  height: 10px;
-  font-size: 13px;
-  line-height: 10px;
-  background: hsla(0, 0%, 50%, 0.2);
-  padding: 4px 2px 4px 6px;
+  width: 14px;
+  height: 14px;
+  vertical-align: middle;
   border-radius: 3px;
   color: var(--color-blue);
+
+  background: var(--color-control-bg);
+  /* Image is an rawurlencoded SVG */
+  background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2212%22%20width%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Cpolygon%20points%3D%223%2C1%203%2C11%208%2C6%22 style%3D%22fill%3A%234070a0%3B%22%2F%3E%3C%2Fsvg%3E");
+  background-position: 2px 1px;
+  background-repeat: no-repeat;
 }
-.cake-dbg-collapse[data-open=true]:before {
-  content: "\25be";
-  padding: 4px 3px 4px 5px;
+.cake-dbg-collapse[data-open=true] {
+  transform: rotate(90deg);
+}
+/* Copy button */
+.cake-dbg-copy {
+  position: absolute;
+  top: 0px;
+  right: 0px;
+  padding: 6px;
+  background: var(--color-control-bg);
+  color: var(--color-blue);
+  border-radius: 0 0 0 3px;
 }
 
 /* Textual elements */
@@ -116,6 +130,7 @@ function initialize() {
   createCollapsibles(doc.querySelectorAll('.cake-dbg-object-props'));
   attachRefEvents(doc.querySelectorAll('.cake-dbg'));
   openBlocks(doc.querySelectorAll('.cake-debug[data-open-all="true"]'));
+  attachCopyButton(doc.querySelectorAll('.cake-dbg'));
 }
 // Add a name on window so DebugKit can add controls to dump blocks
 win.__cakeDebugBlockInit = initialize;
@@ -211,6 +226,32 @@ function openPath(container, target) {
   }
 }
 
+function attachCopyButton(nodes) {
+  nodes.forEach(function (container) {
+    var copy = doc.createElement('a');
+    copy.classList.add('cake-dbg-copy');
+    copy.setAttribute('href', '#');
+    copy.setAttribute('title', 'Copy contents of debug output');
+    copy.appendChild(doc.createTextNode('Copy'));
+
+    // Add copy behavior
+    copy.addEventListener('click', function (event) {
+      event.preventDefault();
+      event.stopPropagation();
+      var lineNo = '';
+      if (container.parentNode && container.parentNode.classList.contains('cake-debug')) {
+        var line = container.parentNode.querySelector('span');
+        lineNo = line.textContent + "\n";
+      }
+
+      // Chop off last 4 to exclude copy button text.
+      navigator.clipboard.writeText(lineNo + container.textContent.substring(0, container.textContent.length - 4));
+    });
+
+    container.appendChild(copy);
+  });
+}
+
 doc.addEventListener('DOMContentLoaded', initialize);
 }(window, document))
 </script>

+ 36 - 8
src/Routing/Route/Route.php

@@ -118,10 +118,13 @@ class Route
      * - `_host` - Define the host name pattern if you want this route to only match
      *   specific host names. You can use `.*` and to create wildcard subdomains/hosts
      *   e.g. `*.example.com` matches all subdomains on `example.com`.
+     * - `_method` - Defines the HTTP method(s) the route applies to. It can be
+     *   a string or array of valid HTTP method name.
      *
      * @param string $template Template string with parameter placeholders
      * @param array $defaults Defaults for the route.
      * @param array $options Array of additional options for the Route
+     * @throws \InvalidArgumentException When `$options['_method']` are not in `VALID_METHODS` list.
      */
     public function __construct(string $template, array $defaults = [], array $options = [])
     {
@@ -131,6 +134,10 @@ class Route
         $this->setExtensions((array)$this->options['_ext']);
         $this->setMiddleware((array)$this->options['_middleware']);
         unset($this->options['_middleware']);
+
+        if (isset($this->defaults['_method'])) {
+            $this->defaults['_method'] = $this->normalizeAndValidateMethods($this->defaults['_method']);
+        }
     }
 
     /**
@@ -164,20 +171,36 @@ class Route
      *
      * @param string[] $methods The HTTP methods to accept.
      * @return $this
-     * @throws \InvalidArgumentException
+     * @throws \InvalidArgumentException When methods are not in `VALID_METHODS` list.
      */
     public function setMethods(array $methods)
     {
-        $methods = array_map('strtoupper', $methods);
-        $diff = array_diff($methods, static::VALID_METHODS);
+        $this->defaults['_method'] = $this->normalizeAndValidateMethods($methods);
+
+        return $this;
+    }
+
+    /**
+     * Normalize method names to upper case and validate that they are valid HTTP methods.
+     *
+     * @param string|string[] $methods Methods.
+     * @return string|string[]
+     * @throws \InvalidArgumentException When methods are not in `VALID_METHODS` list.
+     */
+    protected function normalizeAndValidateMethods($methods)
+    {
+        $methods = is_array($methods)
+            ? array_map('strtoupper', $methods)
+            : strtoupper($methods);
+
+        $diff = array_diff((array)$methods, static::VALID_METHODS);
         if ($diff !== []) {
             throw new InvalidArgumentException(
-                sprintf('Invalid HTTP method received. %s is invalid.', implode(', ', $diff))
+                sprintf('Invalid HTTP method received. `%s` is invalid.', implode(', ', $diff))
             );
         }
-        $this->defaults['_method'] = $methods;
 
-        return $this;
+        return $methods;
     }
 
     /**
@@ -417,9 +440,13 @@ class Route
      * @param string $url The URL to attempt to parse.
      * @param string $method The HTTP method of the request being parsed.
      * @return array|null An array of request parameters, or null on failure.
+     * @throws \InvalidArgumentException When method is not an empty string or in `VALID_METHODS` list.
      */
     public function parse(string $url, string $method): ?array
     {
+        if ($method !== '') {
+            $method = $this->normalizeAndValidateMethods($method);
+        }
         $compiledRoute = $this->compile();
         [$url, $ext] = $this->_parseExtension($url);
 
@@ -740,9 +767,10 @@ class Route
         if (empty($url['_method'])) {
             $url['_method'] = 'GET';
         }
-        $methods = array_map('strtoupper', (array)$url['_method']);
+        $defaults = (array)$this->defaults['_method'];
+        $methods = (array)$this->normalizeAndValidateMethods($url['_method']);
         foreach ($methods as $value) {
-            if (in_array($value, (array)$this->defaults['_method'], true)) {
+            if (in_array($value, $defaults, true)) {
                 return true;
             }
         }

+ 1 - 2
src/Routing/Router.php

@@ -146,7 +146,6 @@ class Router
      * parameters to the route collection.
      *
      * @var callable[]
-     * @psalm-var array<int, (\Closure|callable-string)>
      */
     protected static $_urlFilters = [];
 
@@ -332,7 +331,6 @@ class Router
      *
      * @param callable $function The function to add
      * @return void
-     * @psalm-param \Closure|callable-string $function
      */
     public static function addUrlFilter(callable $function): void
     {
@@ -357,6 +355,7 @@ class Router
                 if (is_array($filter)) {
                     $ref = new ReflectionMethod($filter[0], $filter[1]);
                 } else {
+                    /** @psalm-var \Closure|callable-string $filter */
                     $ref = new ReflectionFunction($filter);
                 }
                 $message = sprintf(

+ 12 - 7
tests/TestCase/Error/Debug/HtmlFormatterTest.php

@@ -72,13 +72,18 @@ class HtmlFormatterTest extends TestCase
         $this->assertGreaterThan(0, count($dom->childNodes));
 
         $expected = <<<TEXT
-object(MyObject) id:1 {stringProp =&gt; &#039;value&#039;
-protected intProp =&gt; (int) 1
-protected floatProp =&gt; (float) 1.1
-protected boolProp =&gt; true
-private nullProp =&gt; null
-arrayProp =&gt; [&#039;&#039; =&gt; too much,(int) 1 =&gt; object(MyObject) id: 1 {},]}
+object(MyObject) id:1 {
+  stringProp =&gt; &#039;value&#039;
+  protected intProp =&gt; (int) 1
+  protected floatProp =&gt; (float) 1.1
+  protected boolProp =&gt; true
+  private nullProp =&gt; null
+  arrayProp =&gt; [
+    &#039;&#039; =&gt; too much,
+    (int) 1 =&gt; object(MyObject) id: 1 {},
+  ]
+}
 TEXT;
-        $this->assertStringContainsString(str_replace("\n", '', $expected), strip_tags($result));
+        $this->assertStringContainsString($expected, strip_tags($result));
     }
 }

+ 11 - 3
tests/TestCase/Routing/Route/RouteTest.php

@@ -54,6 +54,13 @@ class RouteTest extends TestCase
         $this->assertFalse($route->compiled());
     }
 
+    public function testConstructionWithInvalidMethod()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Invalid HTTP method received. `NOPE` is invalid');
+        $route = new Route('/books/reviews', ['controller' => 'Reviews', 'action' => 'index', '_method' => 'nope']);
+    }
+
     /**
      * Test set middleware in the constructor
      *
@@ -1284,11 +1291,12 @@ class RouteTest extends TestCase
             '_method' => 'POST',
             '_matchedRoute' => '/sample',
         ];
-        $this->assertEquals($expected, $route->parse('/sample', 'POST'));
+        $this->assertEquals($expected, $route->parse('/sample', 'post'));
     }
 
     /**
      * Test that http header conditions can cause route failures.
+     * And that http method names are normalized.
      *
      * @return void
      */
@@ -1297,7 +1305,7 @@ class RouteTest extends TestCase
         $route = new Route('/sample', [
             'controller' => 'posts',
             'action' => 'index',
-            '_method' => ['PUT', 'POST'],
+            '_method' => ['put', 'post'],
         ]);
         $this->assertNull($route->parse('/sample', 'GET'));
 
@@ -1766,7 +1774,7 @@ class RouteTest extends TestCase
     public function testSetMethodsInvalid()
     {
         $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionMessage('Invalid HTTP method received. NOPE is invalid');
+        $this->expectExceptionMessage('Invalid HTTP method received. `NOPE` is invalid');
         $route = new Route('/books/reviews', ['controller' => 'Reviews', 'action' => 'index']);
         $route->setMethods(['nope']);
     }

+ 25 - 0
tests/TestCase/Routing/RouteBuilderTest.php

@@ -721,6 +721,31 @@ class RouteBuilderTest extends TestCase
     }
 
     /**
+     * Test connecting resources with restricted mappings.
+     *
+     * @return void
+     */
+    public function testResourcesWithMapOnly()
+    {
+        $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'api']);
+        $routes->resources('Articles', [
+            'map' => [
+                'conditions' => ['action' => 'conditions', 'method' => 'DeLeTe'],
+            ],
+            'only' => ['conditions'],
+        ]);
+
+        $all = $this->collection->routes();
+        $this->assertCount(1, $all);
+        $this->assertSame('DELETE', $all[0]->defaults['_method'], 'method should be normalized.');
+        $this->assertSame('Articles', $all[0]->defaults['controller']);
+        $this->assertSame('conditions', $all[0]->defaults['action']);
+
+        $result = $this->collection->parse('/api/articles/conditions', 'DELETE');
+        $this->assertNotNull($result);
+    }
+
+    /**
      * Test connecting resources.
      *
      * @return void