Browse Source

Merge branch 'master' into 4.next

Mark Story 6 years ago
parent
commit
c68c2aabc3

+ 9 - 0
phpunit.xml.dist

@@ -29,6 +29,15 @@
     <filter>
         <whitelist>
             <directory suffix=".php">src/</directory>
+            <exclude>
+                <!--
+                This file contains a few functions that cannot be tested
+                as they contain die; or breakpoint functionality.
+                -->
+                <file>src/basics.php</file>
+                <!-- This file is deprecated and unused -->
+                <file>src/Core/ClassLoader.php</file>
+            </exclude>
         </whitelist>
     </filter>
 

+ 33 - 5
src/Command/I18nExtractCommand.php

@@ -617,12 +617,14 @@ class I18nExtractCommand extends Command
      */
     protected function _writeFiles(Arguments $args, ConsoleIo $io): void
     {
+        $io->out();
         $overwriteAll = false;
         if ($args->getOption('overwrite')) {
             $overwriteAll = true;
         }
         foreach ($this->_storage as $domain => $sentences) {
             $output = $this->_writeHeader();
+            $headerLength = strlen($output);
             foreach ($sentences as $sentence => $header) {
                 $output .= $header . $sentence;
             }
@@ -634,12 +636,15 @@ class I18nExtractCommand extends Command
             }
 
             $filename = str_replace('/', '_', $domain) . '.pot';
+            $outputPath = $this->_output . $filename;
+
+            if ($this->checkUnchanged($outputPath, $headerLength, $output) === true) {
+                $io->out($filename . ' is unchanged. Skipping.');
+                continue;
+            }
+
             $response = '';
-            while (
-                $overwriteAll === false
-                && file_exists($this->_output . $filename)
-                && strtoupper($response) !== 'Y'
-            ) {
+            while ($overwriteAll === false && file_exists($outputPath) && strtoupper($response) !== 'Y') {
                 $io->out();
                 $response = $io->askChoice(
                     sprintf('Error: %s already exists in this location. Overwrite? [Y]es, [N]o, [A]ll', $filename),
@@ -688,6 +693,29 @@ class I18nExtractCommand extends Command
     }
 
     /**
+     * Check whether the old and new output are the same, thus unchanged
+     *
+     * Compares the sha1 hashes of the old and new file without header.
+     *
+     * @param string $oldFile The existing file.
+     * @param int $headerLength The length of the file header in bytes.
+     * @param string $newFileContent The content of the new file.
+     * @return bool Whether or not the old and new file are unchanged.
+     */
+    protected function checkUnchanged(string $oldFile, int $headerLength, string $newFileContent): bool
+    {
+        if (!file_exists($oldFile)) {
+            return false;
+        }
+        $oldFileContent = file_get_contents($oldFile);
+
+        $oldChecksum = sha1((string)substr($oldFileContent, $headerLength));
+        $newChecksum = sha1((string)substr($newFileContent, $headerLength));
+
+        return $oldChecksum === $newChecksum;
+    }
+
+    /**
      * Get the strings from the position forward
      *
      * @param int $position Actual position on tokens array

+ 2 - 0
src/Core/ClassLoader.php

@@ -17,6 +17,8 @@ namespace Cake\Core;
 
 /**
  * ClassLoader
+ *
+ * @deprecated 4.0.3 Use composer to generate autoload files instead.
  */
 class ClassLoader
 {

+ 2 - 4
src/Filesystem/File.php

@@ -278,10 +278,8 @@ class File
      */
     public function delete(): bool
     {
-        if (is_resource($this->handle)) {
-            fclose($this->handle);
-            $this->handle = null;
-        }
+        $this->close();
+        $this->handle = null;
         if ($this->exists()) {
             return unlink($this->path);
         }

+ 7 - 0
src/View/Widget/FileWidget.php

@@ -16,6 +16,7 @@ declare(strict_types=1);
  */
 namespace Cake\View\Widget;
 
+use Cake\Core\Configure;
 use Cake\View\Form\ContextInterface;
 
 /**
@@ -74,6 +75,12 @@ class FileWidget extends BasicWidget
      */
     public function secureFields(array $data): array
     {
+        // PSR7 UploadedFileInterface objects are used.
+        if (Configure::read('App.uploadedFilesAsObjects', true)) {
+            return [$data['name']];
+        }
+
+        // Backwards compatibility for array files.
         $fields = [];
         foreach (['name', 'type', 'tmp_name', 'error', 'size'] as $suffix) {
             $fields[] = $data['name'] . '[' . $suffix . ']';

+ 5 - 0
src/View/Widget/YearWidget.php

@@ -18,6 +18,7 @@ namespace Cake\View\Widget;
 
 use Cake\View\Form\ContextInterface;
 use Cake\View\StringTemplate;
+use DateTimeInterface;
 use InvalidArgumentException;
 
 /**
@@ -81,6 +82,10 @@ class YearWidget extends BasicWidget
         $data['min'] = (int)$data['min'];
         $data['max'] = (int)$data['max'];
 
+        if ($data['val'] instanceof DateTimeInterface) {
+            $data['val'] = $data['val']->format('Y');
+        }
+
         if (!empty($data['val'])) {
             $data['min'] = min((int)$data['val'], $data['min']);
             $data['max'] = max((int)$data['val'], $data['max']);

+ 16 - 11
templates/layout/dev_error.php

@@ -45,14 +45,16 @@ use Cake\Error\Debugger;
         padding: 10px;
     }
     .header-title {
+        display: flex;
+        align-items: center;
         font-size: 30px;
         margin: 0;
     }
-    .header-title:hover:after {
-        content: attr(data-content);
+    .header-title a {
         font-size: 18px;
-        vertical-align: middle;
         cursor: pointer;
+        margin-left: 10px;
+        user-select: none;
     }
     .header-type {
         display: block;
@@ -244,8 +246,9 @@ use Cake\Error\Debugger;
 </head>
 <body>
     <header>
-        <h1 class="header-title" data-content="&#128203">
+        <h1 class="header-title">
             <?= Debugger::formatHtmlMessage($this->fetch('title')) ?>
+            <a>&#128203</a>
         </h1>
         <span class="header-type"><?= get_class($error) ?></span>
     </header>
@@ -332,11 +335,13 @@ use Cake\Error\Debugger;
                 event.preventDefault();
             });
 
-            bindEvent('.header-title', 'click', function(event) {
+            bindEvent('.header-title a', 'click', function(event) {
                 event.preventDefault();
                 var text = '';
-                each(this.childNodes, function(el) {
-                    text += el.textContent.trim();
+                each(this.parentNode.childNodes, function(el) {
+                    if (el.nodeName !== 'A') {
+                        text += el.textContent.trim();
+                    }
                 });
 
                 // Use execCommand(copy) as it has the widest support.
@@ -350,16 +355,16 @@ use Cake\Error\Debugger;
                     document.execCommand('copy');
 
                     // Show a success icon and then revert
-                    var original = el.getAttribute('data-content');
-                    el.setAttribute('data-content', '\ud83c\udf70');
+                    var original = el.innerText;
+                    el.innerText = '\ud83c\udf70';
                     setTimeout(function () {
-                        el.setAttribute('data-content', original);
+                        el.innerText =  original;
                     }, 1000);
                 } catch (err) {
                     alert('Unable to update clipboard ' + err);
                 }
                 document.body.removeChild(textArea);
-                this.parentNode.scrollIntoView(true);
+                this.parentNode.parentNode.scrollIntoView(true);
             });
         });
     </script>

+ 15 - 0
tests/TestCase/Command/CacheCommandsTest.php

@@ -76,6 +76,21 @@ class CacheCommandsTest extends ConsoleIntegrationTestCase
     }
 
     /**
+     * Test list output
+     *
+     * @return void
+     */
+    public function testList()
+    {
+        $this->exec('cache list');
+
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+        $this->assertOutputContains('- test');
+        $this->assertOutputContains('- _cake_core_');
+        $this->assertOutputContains('- _cake_model_');
+    }
+
+    /**
      * Test help output
      *
      * @return void

+ 21 - 0
tests/TestCase/Command/I18nExtractCommandTest.php

@@ -124,6 +124,27 @@ class I18nExtractCommandTest extends ConsoleIntegrationTestCase
     }
 
     /**
+     * testExecute with no paths
+     *
+     * @return void
+     */
+    public function testExecuteNoPathOption()
+    {
+        $this->exec(
+            'i18n extract ' .
+            '--merge=no ' .
+            '--extract-core=no ' .
+            '--output=' . $this->path . DS,
+            [
+                TEST_APP . 'templates' . DS,
+                'D',
+            ]
+        );
+        $this->assertExitSuccess();
+        $this->assertFileExists($this->path . DS . 'default.pot');
+    }
+
+    /**
      * testExecute with merging on method
      *
      * @return void

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

@@ -271,6 +271,8 @@ class DebuggerTest extends TestCase
         $result = ob_get_clean();
         $this->assertStringContainsString('Notice: I eated an error', $result);
         $this->assertStringContainsString('DebuggerTest.php', $result);
+
+        Debugger::setOutputFormat('js');
     }
 
     /**

+ 64 - 48
tests/TestCase/Error/Middleware/ErrorHandlerMiddlewareTest.php

@@ -25,8 +25,8 @@ use Cake\Http\ServerRequestFactory;
 use Cake\Log\Log;
 use Cake\TestSuite\TestCase;
 use Error;
+use InvalidArgumentException;
 use LogicException;
-use Psr\Log\LoggerInterface;
 use TestApp\Http\TestRequestHandler;
 
 /**
@@ -34,6 +34,9 @@ use TestApp\Http\TestRequestHandler;
  */
 class ErrorHandlerMiddlewareTest extends TestCase
 {
+    /**
+     * @var \Cake\Log\Engine\ArrayLog
+     */
     protected $logger;
 
     /**
@@ -46,12 +49,12 @@ class ErrorHandlerMiddlewareTest extends TestCase
         parent::setUp();
 
         static::setAppNamespace();
-        $this->logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
 
         Log::reset();
         Log::setConfig('error_test', [
-            'engine' => $this->logger,
+            'className' => 'Array',
         ]);
+        $this->logger = Log::engine('error_test');
     }
 
     /**
@@ -66,19 +69,30 @@ class ErrorHandlerMiddlewareTest extends TestCase
     }
 
     /**
+     * Test constructor error
+     *
+     * @return void
+     */
+    public function testConstructorInvalid()
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('$errorHandler argument must be a config array or ErrorHandler');
+        new ErrorHandlerMiddleware('nope');
+    }
+
+    /**
      * Test returning a response works ok.
      *
      * @return void
      */
     public function testNoErrorResponse()
     {
-        $this->logger->expects($this->never())->method('log');
-
         $request = ServerRequestFactory::fromGlobals();
 
         $middleware = new ErrorHandlerMiddleware();
         $result = $middleware->process($request, new TestRequestHandler());
         $this->assertInstanceOf(Response::class, $result);
+        $this->assertCount(0, $this->logger->read());
     }
 
     /**
@@ -105,7 +119,7 @@ class ErrorHandlerMiddlewareTest extends TestCase
         $middleware = new ErrorHandlerMiddleware(new ErrorHandler([
             'exceptionRenderer' => $factory,
         ]));
-        $handler = new TestRequestHandler(function ($req) {
+        $handler = new TestRequestHandler(function () {
             throw new LogicException('Something bad');
         });
         $middleware->process($request, $handler);
@@ -120,7 +134,7 @@ class ErrorHandlerMiddlewareTest extends TestCase
     {
         $request = ServerRequestFactory::fromGlobals();
         $middleware = new ErrorHandlerMiddleware();
-        $handler = new TestRequestHandler(function ($req) {
+        $handler = new TestRequestHandler(function () {
             throw new \Cake\Http\Exception\NotFoundException('whoops');
         });
         $result = $middleware->process($request, $handler);
@@ -140,7 +154,7 @@ class ErrorHandlerMiddlewareTest extends TestCase
         $request = $request->withHeader('Accept', 'application/json');
 
         $middleware = new ErrorHandlerMiddleware();
-        $handler = new TestRequestHandler(function ($req) {
+        $handler = new TestRequestHandler(function () {
             throw new \Cake\Http\Exception\NotFoundException('whoops');
         });
         $result = $middleware->process($request, $handler);
@@ -172,29 +186,29 @@ class ErrorHandlerMiddlewareTest extends TestCase
      */
     public function testHandleExceptionLogAndTrace()
     {
-        $this->logger->expects($this->at(0))
-            ->method('log')
-            ->with('error', $this->logicalAnd(
-                $this->stringContains('[Cake\Http\Exception\NotFoundException] Kaboom!'),
-                $this->stringContains(str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php')),
-                $this->stringContains('Request URL: /target/url'),
-                $this->stringContains('Referer URL: /other/path'),
-                $this->logicalNot(
-                    $this->stringContains('Previous: ')
-                )
-            ));
-
         $request = ServerRequestFactory::fromGlobals([
             'REQUEST_URI' => '/target/url',
             'HTTP_REFERER' => '/other/path',
         ]);
         $middleware = new ErrorHandlerMiddleware(['log' => true, 'trace' => true]);
-        $handler = new TestRequestHandler(function ($req) {
+        $handler = new TestRequestHandler(function () {
             throw new \Cake\Http\Exception\NotFoundException('Kaboom!');
         });
         $result = $middleware->process($request, $handler);
         $this->assertEquals(404, $result->getStatusCode());
         $this->assertStringContainsString('was not found', '' . $result->getBody());
+
+        $logs = $this->logger->read();
+        $this->assertCount(1, $logs);
+        $this->assertStringContainsString('error', $logs[0]);
+        $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $logs[0]);
+        $this->assertStringContainsString(
+            str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
+            $logs[0]
+        );
+        $this->assertStringContainsString('Request URL: /target/url', $logs[0]);
+        $this->assertStringContainsString('Referer URL: /other/path', $logs[0]);
+        $this->assertStringNotContainsString('Previous:', $logs[0]);
     }
 
     /**
@@ -204,16 +218,6 @@ class ErrorHandlerMiddlewareTest extends TestCase
      */
     public function testHandleExceptionLogAndTraceWithPrevious()
     {
-        $this->logger->expects($this->at(0))
-            ->method('log')
-            ->with('error', $this->logicalAnd(
-                $this->stringContains('[Cake\Http\Exception\NotFoundException] Kaboom!'),
-                $this->stringContains('Caused by: [Cake\Datasource\Exception\RecordNotFoundException] Previous logged'),
-                $this->stringContains(str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php')),
-                $this->stringContains('Request URL: /target/url'),
-                $this->stringContains('Referer URL: /other/path')
-            ));
-
         $request = ServerRequestFactory::fromGlobals([
             'REQUEST_URI' => '/target/url',
             'HTTP_REFERER' => '/other/path',
@@ -226,6 +230,21 @@ class ErrorHandlerMiddlewareTest extends TestCase
         $result = $middleware->process($request, $handler);
         $this->assertEquals(404, $result->getStatusCode());
         $this->assertStringContainsString('was not found', '' . $result->getBody());
+
+        $logs = $this->logger->read();
+        $this->assertCount(1, $logs);
+        $this->assertStringContainsString('error', $logs[0]);
+        $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $logs[0]);
+        $this->assertStringContainsString(
+            'Caused by: [Cake\Datasource\Exception\RecordNotFoundException]',
+            $logs[0]
+        );
+        $this->assertStringContainsString(
+            str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
+            $logs[0]
+        );
+        $this->assertStringContainsString('Request URL: /target/url', $logs[0]);
+        $this->assertStringContainsString('Referer URL: /other/path', $logs[0]);
     }
 
     /**
@@ -235,19 +254,19 @@ class ErrorHandlerMiddlewareTest extends TestCase
      */
     public function testHandleExceptionSkipLog()
     {
-        $this->logger->expects($this->never())->method('log');
-
         $request = ServerRequestFactory::fromGlobals();
         $middleware = new ErrorHandlerMiddleware([
             'log' => true,
             'skipLog' => ['Cake\Http\Exception\NotFoundException'],
         ]);
-        $handler = new TestRequestHandler(function ($req) {
+        $handler = new TestRequestHandler(function () {
             throw new \Cake\Http\Exception\NotFoundException('Kaboom!');
         });
         $result = $middleware->process($request, $handler);
         $this->assertEquals(404, $result->getStatusCode());
         $this->assertStringContainsString('was not found', '' . $result->getBody());
+
+        $this->assertCount(0, $this->logger->read());
     }
 
     /**
@@ -257,25 +276,22 @@ class ErrorHandlerMiddlewareTest extends TestCase
      */
     public function testHandleExceptionLogAttributes()
     {
-        $this->logger->expects($this->at(0))
-            ->method('log')
-            ->with('error', $this->logicalAnd(
-                $this->stringContains(
-                    '[Cake\Http\Exception\MissingControllerException] ' .
-                    'Controller class Articles could not be found.'
-                ),
-                $this->stringContains('Exception Attributes:'),
-                $this->stringContains("'class' => 'Articles'"),
-                $this->stringContains('Request URL:')
-            ));
-
         $request = ServerRequestFactory::fromGlobals();
         $middleware = new ErrorHandlerMiddleware(['log' => true]);
-        $handler = new TestRequestHandler(function ($req) {
+        $handler = new TestRequestHandler(function () {
             throw new MissingControllerException(['class' => 'Articles']);
         });
         $result = $middleware->process($request, $handler);
         $this->assertEquals(404, $result->getStatusCode());
+
+        $logs = $this->logger->read();
+        $this->assertStringContainsString(
+            '[Cake\Http\Exception\MissingControllerException] Controller class Articles could not be found.',
+            $logs[0]
+        );
+        $this->assertStringContainsString('Exception Attributes:', $logs[0]);
+        $this->assertStringContainsString("'class' => 'Articles'", $logs[0]);
+        $this->assertStringContainsString('Request URL:', $logs[0]);
     }
 
     /**
@@ -300,7 +316,7 @@ class ErrorHandlerMiddlewareTest extends TestCase
         $middleware = new ErrorHandlerMiddleware(new ErrorHandler([
             'exceptionRenderer' => $factory,
         ]));
-        $handler = new TestRequestHandler(function ($req) {
+        $handler = new TestRequestHandler(function () {
             throw new \Cake\Http\Exception\ServiceUnavailableException('whoops');
         });
         $response = $middleware->process($request, $handler);

+ 5 - 3
tests/TestCase/Filesystem/FileTest.php

@@ -576,10 +576,12 @@ class FileTest extends TestCase
         if (!file_exists($tmpFile)) {
             touch($tmpFile);
         }
-        $TmpFile = new File($tmpFile);
+        $file = new File($tmpFile);
         $this->assertFileExists($tmpFile);
-        $result = $TmpFile->delete();
-        $this->assertTrue($result);
+
+        $file->read();
+        $this->assertTrue($file->delete());
+        $this->assertFalse($file->exists());
         $this->assertFileNotExists($tmpFile);
 
         $TmpFile = new File('/this/does/not/exist');

+ 22 - 5
tests/TestCase/View/Helper/FormHelperTest.php

@@ -2258,11 +2258,7 @@ class FormHelperTest extends TestCase
         $this->Form->create();
 
         $this->Form->file('Attachment.file');
-        $expected = [
-            'Attachment.file.name', 'Attachment.file.type',
-            'Attachment.file.tmp_name', 'Attachment.file.error',
-            'Attachment.file.size',
-        ];
+        $expected = ['Attachment.file'];
         $result = $this->Form->getFormProtector()->__debugInfo()['fields'];
         $this->assertEquals($expected, $result);
     }
@@ -6296,6 +6292,27 @@ class FormHelperTest extends TestCase
         $this->assertHtml($expected, $result);
 
         $result = $this->Form->year('published', [
+            'empty' => false,
+            'value' => new Date('2008-01-12'),
+            'min' => 2007,
+            'max' => 2009,
+        ]);
+        $expected = [
+            ['select' => ['name' => 'published']],
+            ['option' => ['value' => '2009']],
+            '2009',
+            '/option',
+            ['option' => ['value' => '2008', 'selected' => 'selected']],
+            '2008',
+            '/option',
+            ['option' => ['value' => '2007']],
+            '2007',
+            '/option',
+            '/select',
+        ];
+        $this->assertHtml($expected, $result);
+
+        $result = $this->Form->year('published', [
             'empty' => 'Published on',
         ]);
         $this->assertStringContainsString('Published on', $result);

+ 19 - 0
tests/TestCase/View/Widget/FileWidgetTest.php

@@ -16,6 +16,7 @@ declare(strict_types=1);
  */
 namespace Cake\Test\TestCase\View\Widget;
 
+use Cake\Core\Configure;
 use Cake\Http\ServerRequest;
 use Cake\TestSuite\TestCase;
 use Cake\View\Form\NullContext;
@@ -99,4 +100,22 @@ class FileWidgetTest extends TestCase
         ];
         $this->assertHtml($expected, $result);
     }
+
+    /**
+     * Test secureFields
+     *
+     * @return void
+     */
+    public function testSecureFields()
+    {
+        $input = new FileWidget($this->templates);
+        $data = ['name' => 'image', 'required' => true, 'val' => 'nope'];
+        $this->assertEquals(['image'], $input->secureFields($data));
+
+        Configure::write('App.uploadedFilesAsObjects', false);
+        $this->assertEquals(
+            ['image[name]', 'image[type]', 'image[tmp_name]', 'image[error]', 'image[size]'],
+            $input->secureFields($data)
+        );
+    }
 }