diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a3a28c33608..61b892fca26 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,9 @@ + diff --git a/VERSION.txt b/VERSION.txt index d44862f90b6..e344572a6f8 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -16,4 +16,4 @@ // @license https://opensource.org/licenses/mit-license.php MIT License // +--------------------------------------------------------------------------------------------+ // //////////////////////////////////////////////////////////////////////////////////////////////////// -3.8.8 +3.8.9 diff --git a/src/Auth/BaseAuthenticate.php b/src/Auth/BaseAuthenticate.php index 5b8db393686..923841deb83 100644 --- a/src/Auth/BaseAuthenticate.php +++ b/src/Auth/BaseAuthenticate.php @@ -131,6 +131,16 @@ protected function _findUser($username, $password = null) if ($password !== null) { $hasher = $this->passwordHasher(); $hashedPassword = $result->get($passwordField); + + if ($hashedPassword === null || $hashedPassword === '') { + // Waste time hashing the password, to prevent + // timing side-channels to distinguish whether + // user has password or not. + $hasher->hash($password); + + return false; + } + if (!$hasher->check($password, $hashedPassword)) { return false; } diff --git a/src/Collection/CollectionInterface.php b/src/Collection/CollectionInterface.php index cecf75b4593..39c744295f5 100644 --- a/src/Collection/CollectionInterface.php +++ b/src/Collection/CollectionInterface.php @@ -255,7 +255,7 @@ public function extract($matcher); * @param callable|string $callback the callback or column name to use for sorting * @param int $type the type of comparison to perform, either SORT_STRING * SORT_NUMERIC or SORT_NATURAL - * @see \Cake\Collection\CollectionIterface::sortBy() + * @see \Cake\Collection\CollectionInterface::sortBy() * @return mixed The value of the top element in the collection */ public function max($callback, $type = \SORT_NUMERIC); diff --git a/src/Console/CommandCollection.php b/src/Console/CommandCollection.php index c7e45def465..55dbf2d7e11 100644 --- a/src/Console/CommandCollection.php +++ b/src/Console/CommandCollection.php @@ -62,7 +62,7 @@ public function add($name, $command) if (!is_subclass_of($command, Shell::class) && !is_subclass_of($command, Command::class)) { $class = is_string($command) ? $command : get_class($command); throw new InvalidArgumentException( - "Cannot use '$class' for command '$name' it is not a subclass of Cake\Console\Shell or Cake\Console\Command." + "Cannot use '$class' for command '$name'. It is not a subclass of Cake\Console\Shell or Cake\Console\Command." ); } if (!preg_match('/^[^\s]+(?:(?: [^\s]+){1,2})?$/ui', $name)) { diff --git a/src/Console/ConsoleInputSubcommand.php b/src/Console/ConsoleInputSubcommand.php index d303222e288..078072e80c8 100644 --- a/src/Console/ConsoleInputSubcommand.php +++ b/src/Console/ConsoleInputSubcommand.php @@ -111,7 +111,7 @@ public function help($width = 0) /** * Get the usage value for this option * - * @return \Cake\Console\ConsoleOptionParser|bool Either false or a ConsoleOptionParser + * @return \Cake\Console\ConsoleOptionParser|false Either false or a ConsoleOptionParser */ public function parser() { diff --git a/src/Console/ShellDispatcher.php b/src/Console/ShellDispatcher.php index ed0af5a0ace..851407fd3e8 100644 --- a/src/Console/ShellDispatcher.php +++ b/src/Console/ShellDispatcher.php @@ -353,7 +353,7 @@ protected function _handleAlias($shell) * Check if a shell class exists for the given name. * * @param string $shell The shell name to look for. - * @return string|bool Either the classname or false. + * @return string|false Either the classname or false. */ protected function _shellExists($shell) { diff --git a/src/Controller/Component/AuthComponent.php b/src/Controller/Component/AuthComponent.php index de5b24870e0..4be06be027d 100644 --- a/src/Controller/Component/AuthComponent.php +++ b/src/Controller/Component/AuthComponent.php @@ -811,7 +811,7 @@ public function redirectUrl($url = null) * Triggers `Auth.afterIdentify` event which the authenticate classes can listen * to. * - * @return array|bool User record data, or false, if the user could not be identified. + * @return array|false User record data, or false, if the user could not be identified. */ public function identify() { @@ -975,7 +975,7 @@ public function getAuthenticate($alias) /** * Set a flash message. Uses the Flash component with values from `flash` config. * - * @param string $message The message to set. + * @param string|false $message The message to set. False to skip. * @return void */ public function flash($message) diff --git a/src/Core/ObjectRegistry.php b/src/Core/ObjectRegistry.php index d4e893e1719..a5c88ad5fa6 100644 --- a/src/Core/ObjectRegistry.php +++ b/src/Core/ObjectRegistry.php @@ -155,7 +155,7 @@ protected function _checkDuplicate($name, $config) * Should resolve the classname for a given object type. * * @param string $class The class to resolve. - * @return string|bool The resolved name or false for failure. + * @return string|false The resolved name or false for failure. */ abstract protected function _resolveClassName($class); diff --git a/src/Database/Query.php b/src/Database/Query.php index a4ab1e90e17..a4a5b54f544 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -2002,7 +2002,7 @@ public function getValueBinder() * associate values to those placeholders so that they can be passed correctly * to the statement object. * - * @param \Cake\Database\ValueBinder|bool $binder The binder or false to disable binding. + * @param \Cake\Database\ValueBinder|false $binder The binder or false to disable binding. * @return $this */ public function setValueBinder($binder) diff --git a/src/Error/BaseErrorHandler.php b/src/Error/BaseErrorHandler.php index f96e42a7c77..73002551c99 100644 --- a/src/Error/BaseErrorHandler.php +++ b/src/Error/BaseErrorHandler.php @@ -148,9 +148,19 @@ public function handleError($code, $description, $file = null, $line = null, $co $debug = Configure::read('debug'); if ($debug) { + // By default trim 3 frames off for the public and protected methods + // used by ErrorHandler instances. + $start = 3; + + // Can be used by error handlers that wrap other error handlers + // to coerce the generated stack trace to the correct point. + if (isset($context['_trace_frame_offset'])) { + $start += $context['_trace_frame_offset']; + unset($context['_trace_frame_offset']); + } $data += [ 'context' => $context, - 'start' => 3, + 'start' => $start, 'path' => Debugger::trimPath($file), ]; } diff --git a/src/Filesystem/File.php b/src/Filesystem/File.php index ccd21a4bc24..21c02c02610 100644 --- a/src/Filesystem/File.php +++ b/src/Filesystem/File.php @@ -139,14 +139,14 @@ public function open($mode = 'r', $force = false) /** * Return the contents of this file as a string. * - * @param string|bool $bytes where to start + * @param int|false $lengthInBytes The length to read in bytes or `false` for the full file. Defaults to `false`. * @param string $mode A `fread` compatible mode. * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't - * @return string|false string on success, false on failure + * @return string|false String on success, false on failure */ - public function read($bytes = false, $mode = 'rb', $force = false) + public function read($lengthInBytes = false, $mode = 'rb', $force = false) { - if ($bytes === false && $this->lock === null) { + if ($lengthInBytes === false && $this->lock === null) { return file_get_contents($this->path); } if ($this->open($mode, $force) === false) { @@ -155,8 +155,8 @@ public function read($bytes = false, $mode = 'rb', $force = false) if ($this->lock !== null && flock($this->handle, LOCK_SH) === false) { return false; } - if (is_int($bytes)) { - return fread($this->handle, $bytes); + if (is_int($lengthInBytes)) { + return fread($this->handle, $lengthInBytes); } $data = ''; @@ -167,7 +167,7 @@ public function read($bytes = false, $mode = 'rb', $force = false) if ($this->lock !== null) { flock($this->handle, LOCK_UN); } - if ($bytes === false) { + if ($lengthInBytes === false) { $this->close(); } diff --git a/src/Filesystem/Folder.php b/src/Filesystem/Folder.php index ca9401e7944..665cb520799 100644 --- a/src/Filesystem/Folder.php +++ b/src/Filesystem/Folder.php @@ -166,7 +166,7 @@ public function pwd() * Change directory to $path. * * @param string $path Path to the directory to change to - * @return string|bool The new path. Returns false on failure + * @return string|false The new path. Returns false on failure */ public function cd($path) { diff --git a/src/Log/LogTrait.php b/src/Log/LogTrait.php index b434e4faafc..0aac978f6a5 100644 --- a/src/Log/LogTrait.php +++ b/src/Log/LogTrait.php @@ -25,13 +25,13 @@ trait LogTrait * Convenience method to write a message to Log. See Log::write() * for more information on writing to logs. * - * @param mixed $msg Log message. + * @param mixed $message Log message. * @param int|string $level Error level. * @param string|array $context Additional log data relevant to this message. * @return bool Success of log write. */ - public function log($msg, $level = LogLevel::ERROR, $context = []) + public function log($message, $level = LogLevel::ERROR, $context = []) { - return Log::write($level, $msg, $context); + return Log::write($level, $message, $context); } } diff --git a/src/Mailer/Email.php b/src/Mailer/Email.php index 44a9b8bab8d..e64ba8d2a3f 100644 --- a/src/Mailer/Email.php +++ b/src/Mailer/Email.php @@ -915,7 +915,7 @@ public function getEmailPattern() * EmailPattern setter/getter * * @deprecated 3.4.0 Use setEmailPattern()/getEmailPattern() instead. - * @param string|bool|null $regex The pattern to use for email address validation, + * @param string|false|null $regex The pattern to use for email address validation, * null to unset the pattern and make use of filter_var() instead, false or * nothing to return the current value * @return string|$this diff --git a/src/Mailer/Transport/SmtpTransport.php b/src/Mailer/Transport/SmtpTransport.php index a40e1368b39..39dc819aacd 100644 --- a/src/Mailer/Transport/SmtpTransport.php +++ b/src/Mailer/Transport/SmtpTransport.php @@ -426,7 +426,7 @@ protected function _generateSocket() * Protected method for sending data to SMTP connection * * @param string|null $data Data to be sent to SMTP server - * @param string|bool $checkCode Code to check for in server response, false to skip + * @param string|false $checkCode Code to check for in server response, false to skip * @return string|null The matched code, or null if nothing matched * @throws \Cake\Network\Exception\SocketException */ diff --git a/src/ORM/Association.php b/src/ORM/Association.php index 8d20cbd691a..96d945807e5 100644 --- a/src/ORM/Association.php +++ b/src/ORM/Association.php @@ -1264,7 +1264,7 @@ protected function _formatAssociationResults($query, $surrogate, $options) $property = $options['propertyPath']; $propertyPath = explode('.', $property); - $query->formatResults(function ($results) use ($formatters, $property, $propertyPath) { + $query->formatResults(function ($results) use ($formatters, $property, $propertyPath, $query) { $extracted = []; foreach ($results as $result) { foreach ($propertyPath as $propertyPathItem) { @@ -1282,7 +1282,16 @@ protected function _formatAssociationResults($query, $surrogate, $options) } /** @var \Cake\Collection\CollectionInterface $results */ - return $results->insert($property, $extracted); + $results = $results->insert($property, $extracted); + if ($query->isHydrationEnabled()) { + $results = $results->map(function ($result) { + $result->clean(); + + return $result; + }); + } + + return $results; }, Query::PREPEND); } diff --git a/src/ORM/Association/BelongsToMany.php b/src/ORM/Association/BelongsToMany.php index 4c33a0aa27d..a5c6b7c527f 100644 --- a/src/ORM/Association/BelongsToMany.php +++ b/src/ORM/Association/BelongsToMany.php @@ -744,7 +744,7 @@ public function saveAssociated(EntityInterface $entity, array $options = []) * @param array $options list of options accepted by `Table::save()` * @throws \InvalidArgumentException if the property representing the association * in the parent entity cannot be traversed - * @return \Cake\Datasource\EntityInterface|bool The parent entity after all links have been + * @return \Cake\Datasource\EntityInterface|false The parent entity after all links have been * created if no errors happened, false otherwise */ protected function _saveTarget(EntityInterface $parentEntity, $entities, $options) diff --git a/src/ORM/Association/Loader/SelectWithPivotLoader.php b/src/ORM/Association/Loader/SelectWithPivotLoader.php index c14139916d9..d4af1b38f04 100644 --- a/src/ORM/Association/Loader/SelectWithPivotLoader.php +++ b/src/ORM/Association/Loader/SelectWithPivotLoader.php @@ -127,6 +127,14 @@ protected function _buildQuery($options) return $query; } + /** + * @inheritDoc + */ + protected function _assertFieldsPresent($fetchQuery, $key) + { + // _buildQuery() manually adds in required fields from junction table + } + /** * Generates a string used as a table field that contains the values upon * which the filter should be applied diff --git a/src/ORM/Behavior/TreeBehavior.php b/src/ORM/Behavior/TreeBehavior.php index e31b3f68f8b..bcd2d43045f 100644 --- a/src/ORM/Behavior/TreeBehavior.php +++ b/src/ORM/Behavior/TreeBehavior.php @@ -603,7 +603,7 @@ protected function _removeFromTree($node) * @param \Cake\Datasource\EntityInterface $node The node to move * @param int|bool $number How many places to move the node, or true to move to first position * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found - * @return \Cake\Datasource\EntityInterface|bool $node The node after being moved or false on failure + * @return \Cake\Datasource\EntityInterface|false $node The node after being moved or false on failure */ public function moveUp(EntityInterface $node, $number = 1) { @@ -624,7 +624,7 @@ public function moveUp(EntityInterface $node, $number = 1) * @param \Cake\Datasource\EntityInterface $node The node to move * @param int|bool $number How many places to move the node, or true to move to first position * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found - * @return \Cake\Datasource\EntityInterface|bool $node The node after being moved or false on failure + * @return \Cake\Datasource\EntityInterface|false $node The node after being moved or false on failure */ protected function _moveUp($node, $number) { @@ -693,7 +693,7 @@ protected function _moveUp($node, $number) * @param \Cake\Datasource\EntityInterface $node The node to move * @param int|bool $number How many places to move the node or true to move to last position * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found - * @return \Cake\Datasource\EntityInterface|bool the entity after being moved or false on failure + * @return \Cake\Datasource\EntityInterface|false the entity after being moved or false on failure */ public function moveDown(EntityInterface $node, $number = 1) { @@ -714,7 +714,7 @@ public function moveDown(EntityInterface $node, $number = 1) * @param \Cake\Datasource\EntityInterface $node The node to move * @param int|bool $number How many places to move the node, or true to move to last position * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found - * @return \Cake\Datasource\EntityInterface|bool $node The node after being moved or false on failure + * @return \Cake\Datasource\EntityInterface|false $node The node after being moved or false on failure */ protected function _moveDown($node, $number) { diff --git a/src/ORM/Table.php b/src/ORM/Table.php index 7f834e597fc..e42bb6cfb90 100644 --- a/src/ORM/Table.php +++ b/src/ORM/Table.php @@ -1973,7 +1973,7 @@ public function saveOrFail(EntityInterface $entity, $options = []) * * @param \Cake\Datasource\EntityInterface $entity the entity to be saved * @param \ArrayObject $options the options to use for the save operation - * @return \Cake\Datasource\EntityInterface|bool + * @return \Cake\Datasource\EntityInterface|false * @throws \RuntimeException When an entity is missing some of the primary keys. * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction * is aborted in the afterSave event. @@ -2078,7 +2078,7 @@ protected function _onSaveSuccess($entity, $options) * * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted * @param array $data The actual data that needs to be saved - * @return \Cake\Datasource\EntityInterface|bool + * @return \Cake\Datasource\EntityInterface|false * @throws \RuntimeException if not all the primary keys where supplied or could * be generated when the table has composite primary keys. Or when the table has no primary key. */ @@ -2176,7 +2176,7 @@ protected function _newId($primary) * * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted * @param array $data The actual data that needs to be saved - * @return \Cake\Datasource\EntityInterface|bool + * @return \Cake\Datasource\EntityInterface|false * @throws \InvalidArgumentException When primary key data is missing. */ protected function _update($entity, $data) diff --git a/src/Shell/Task/CommandTask.php b/src/Shell/Task/CommandTask.php index 04cf2f8d9d3..c7b0989cd37 100644 --- a/src/Shell/Task/CommandTask.php +++ b/src/Shell/Task/CommandTask.php @@ -202,7 +202,7 @@ public function subCommands($commandName) * Get Shell instance for the given command * * @param string $commandName The command you want. - * @return \Cake\Console\Shell|bool Shell instance if the command can be found, false otherwise. + * @return \Cake\Console\Shell|false Shell instance if the command can be found, false otherwise. */ public function getShell($commandName) { diff --git a/src/Shell/Task/ExtractTask.php b/src/Shell/Task/ExtractTask.php index 03031f01138..87327806eb3 100644 --- a/src/Shell/Task/ExtractTask.php +++ b/src/Shell/Task/ExtractTask.php @@ -601,12 +601,14 @@ protected function _store($domain, $header, $sentence) */ protected function _writeFiles() { + $this->out(); $overwriteAll = false; if (!empty($this->params['overwrite'])) { $overwriteAll = true; } foreach ($this->_storage as $domain => $sentences) { $output = $this->_writeHeader(); + $headerLength = strlen($output); foreach ($sentences as $sentence => $header) { $output .= $header . $sentence; } @@ -619,6 +621,13 @@ protected function _writeFiles() $filename = str_replace('/', '_', $domain) . '.pot'; $File = new File($this->_output . $filename); + + if ($File->exists() && $this->_checkUnchanged($File, $headerLength, $output) === true) { + $this->out($filename . ' is unchanged. Skipping.'); + $File->close(); + continue; + } + $response = ''; while ($overwriteAll === false && $File->exists() && strtoupper($response) !== 'Y') { $this->out(); @@ -669,6 +678,26 @@ protected function _writeHeader() return $output; } + /** + * 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 File $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(File $oldFile, $headerLength, $newFileContent) + { + $oldFileContent = $oldFile->read(); + + $oldChecksum = sha1(substr($oldFileContent, $headerLength)); + $newChecksum = sha1(substr($newFileContent, $headerLength)); + + return $oldChecksum === $newChecksum; + } + /** * Get the strings from the position forward * diff --git a/src/TestSuite/MiddlewareDispatcher.php b/src/TestSuite/MiddlewareDispatcher.php index d11048f7160..3e2fc66fbb1 100644 --- a/src/TestSuite/MiddlewareDispatcher.php +++ b/src/TestSuite/MiddlewareDispatcher.php @@ -85,7 +85,7 @@ public function __construct($test, $class = null, $constructorArgs = null, $disa $app = $reflect->newInstanceArgs($this->_constructorArgs); $this->app = $app; } catch (ReflectionException $e) { - throw new LogicException(sprintf('Cannot load "%s" for use in integration testing.', $this->_class)); + throw new LogicException(sprintf('Cannot load `%s` for use in integration testing.', $this->_class)); } } diff --git a/src/TestSuite/MockBuilder.php b/src/TestSuite/MockBuilder.php index 141d96cb339..61627c15422 100644 --- a/src/TestSuite/MockBuilder.php +++ b/src/TestSuite/MockBuilder.php @@ -14,6 +14,8 @@ */ namespace Cake\TestSuite; +loadPHPUnitAliases(); + use PHPUnit\Framework\MockObject\MockBuilder as BaseMockBuilder; /** diff --git a/src/Utility/Xml.php b/src/Utility/Xml.php index 201d6a73f1b..3289efa46b2 100644 --- a/src/Utility/Xml.php +++ b/src/Utility/Xml.php @@ -319,7 +319,7 @@ protected static function _fromArray($dom, $node, &$data, $format) } $isNamespace = strpos($key, 'xmlns:'); if ($isNamespace !== false) { - $node->setAttributeNS('http://www.w3.org/2000/xmlns/', $key, $value); + $node->setAttributeNS('http://www.w3.org/2000/xmlns/', $key, (string)$value); continue; } if ($key[0] !== '@' && $format === 'tags') { @@ -328,7 +328,7 @@ protected static function _fromArray($dom, $node, &$data, $format) // https://www.w3.org/TR/REC-xml/#syntax // https://bugs.php.net/bug.php?id=36795 $child = $dom->createElement($key, ''); - $child->appendChild(new DOMText($value)); + $child->appendChild(new DOMText((string)$value)); } else { $child = $dom->createElement($key, $value); } diff --git a/src/Validation/Validation.php b/src/Validation/Validation.php index 73936f01417..d71d8812cbf 100644 --- a/src/Validation/Validation.php +++ b/src/Validation/Validation.php @@ -1254,7 +1254,7 @@ public static function mimeType($check, $mimeTypes = []) * we accept. * * @param string|array|\Psr\Http\Message\UploadedFileInterface $check The data to read a filename out of. - * @return string|bool Either the filename or false on failure. + * @return string|false Either the filename or false on failure. */ protected static function getFilename($check) { diff --git a/src/View/Form/EntityContext.php b/src/View/Form/EntityContext.php index 90857cc59c8..ad52be87db9 100644 --- a/src/View/Form/EntityContext.php +++ b/src/View/Form/EntityContext.php @@ -323,7 +323,7 @@ protected function _extractMultiple($values, $path) * * @param array|null $path Each one of the parts in a path for a field name * or null to get the entity passed in constructor context. - * @return \Cake\Datasource\EntityInterface|\Traversable|array|bool + * @return \Cake\Datasource\EntityInterface|\Traversable|array|false * @throws \RuntimeException When properties cannot be read. */ public function entity($path = null) @@ -603,7 +603,7 @@ protected function _getValidator($parts) * @param array $parts Each one of the parts in a path for a field name * @param bool $fallback Whether or not to fallback to the last found table * when a non-existent field/property is being encountered. - * @return \Cake\ORM\Table|bool Table instance or false + * @return \Cake\ORM\Table|false Table instance or false */ protected function _getTable($parts, $fallback = true) { diff --git a/src/View/Helper/HtmlHelper.php b/src/View/Helper/HtmlHelper.php index b39ed4a451b..c43ac744e51 100644 --- a/src/View/Helper/HtmlHelper.php +++ b/src/View/Helper/HtmlHelper.php @@ -1103,7 +1103,7 @@ public function para($class, $text, array $options = []) if (!empty($options['escape'])) { $text = h($text); } - if ($class && !empty($class)) { + if ($class) { $options['class'] = $class; } $tag = 'para'; diff --git a/src/View/Helper/PaginatorHelper.php b/src/View/Helper/PaginatorHelper.php index 1c3139e0f96..3f7e447b152 100644 --- a/src/View/Helper/PaginatorHelper.php +++ b/src/View/Helper/PaginatorHelper.php @@ -270,7 +270,7 @@ public function sortDir($model = null, array $options = []) /** * Generate an active/inactive link for next/prev methods. * - * @param string|bool $text The enabled text for the link. + * @param string|false $text The enabled text for the link. * @param bool $enabled Whether or not the enabled/disabled version should be created. * @param array $options An array of options from the calling method. * @param array $templates An array of templates with the 'active' and 'disabled' keys. diff --git a/src/View/Helper/UrlHelper.php b/src/View/Helper/UrlHelper.php index 4fc675d4e43..05d2f576667 100644 --- a/src/View/Helper/UrlHelper.php +++ b/src/View/Helper/UrlHelper.php @@ -174,7 +174,7 @@ public function assetUrl($path, array $options = []) if (!array_key_exists('plugin', $options) || $options['plugin'] !== false) { list($plugin, $path) = $this->_View->pluginSplit($path, false); } - if (!empty($options['pathPrefix']) && $path[0] !== '/') { + if (!empty($options['pathPrefix']) && (substr((string)$path, 0, 1) !== '/')) { $path = $options['pathPrefix'] . $path; } if ( diff --git a/src/View/SerializedView.php b/src/View/SerializedView.php index 85fbc577efd..857b33d99f4 100644 --- a/src/View/SerializedView.php +++ b/src/View/SerializedView.php @@ -83,7 +83,7 @@ abstract protected function _serialize($serialize); * names. If true all view variables will be serialized. If unset normal * view template will be rendered. * - * @param string|bool|null $view The view being rendered. + * @param string|false|null $view The view being rendered. * @param string|null $layout The layout being rendered. * @return string|null The rendered view. */ diff --git a/src/View/View.php b/src/View/View.php index 565027113c5..d4b1f0cee0f 100644 --- a/src/View/View.php +++ b/src/View/View.php @@ -1804,15 +1804,16 @@ protected function _elementCache($name, $data, $options) $plugin = null; list($plugin, $name) = $this->pluginSplit($name); - $underscored = null; + $pluginKey = null; if ($plugin) { - $underscored = Inflector::underscore($plugin); + $pluginKey = str_replace('/', '_', Inflector::underscore($plugin)); } + $elementKey = str_replace(['\\', '/'], '_', $name); $cache = $options['cache']; unset($options['cache'], $options['callbacks'], $options['plugin']); $keys = array_merge( - [$underscored, $name], + [$pluginKey, $elementKey], array_keys($options), array_keys($data) ); diff --git a/tests/TestCase/Auth/DigestAuthenticateTest.php b/tests/TestCase/Auth/DigestAuthenticateTest.php index ad46b055323..25532116d3d 100644 --- a/tests/TestCase/Auth/DigestAuthenticateTest.php +++ b/tests/TestCase/Auth/DigestAuthenticateTest.php @@ -25,6 +25,7 @@ use Cake\I18n\Time; use Cake\ORM\Entity; use Cake\TestSuite\TestCase; +use Cake\Utility\Security; /** * Entity for testing with hidden fields. @@ -61,6 +62,8 @@ public function setUp() 'realm' => 'localhost', 'nonce' => 123, 'opaque' => '123abc', + 'secret' => Security::getSalt(), + 'passwordHasher' => 'ShouldNeverTryToUsePasswordHasher', ]); $password = DigestAuthenticate::password('mariano', 'cake', 'localhost'); @@ -110,8 +113,6 @@ public function testAuthenticateNoData() */ public function testAuthenticateWrongUsername() { - $this->expectException(\Cake\Http\Exception\UnauthorizedException::class); - $this->expectExceptionCode(401); $request = new ServerRequest(['url' => 'posts/index']); $data = [ @@ -126,6 +127,10 @@ public function testAuthenticateWrongUsername() $data['response'] = $this->auth->generateResponseHash($data, '09faa9931501bf30f0d4253fa7763022', 'GET'); $request = $request->withEnv('PHP_AUTH_DIGEST', $this->digestHeader($data)); + $this->assertFalse($this->auth->authenticate($request, new Response())); + + $this->expectException(UnauthorizedException::class); + $this->expectExceptionCode(401); $this->auth->unauthenticated($request, $this->response); } @@ -525,7 +530,7 @@ protected function digestHeader($data) 'opaque' => '123abc', ]; $digest = <<assertNotEmpty($result); $this->assertTrue($this->auth->needsPasswordRehash()); } + + /** + * Tests that password hasher function is called exactly once in all cases. + * + * @param string $username + * @param string|null $password + * @return void + * @dataProvider userList + */ + public function testAuthenticateSingleHash($username, $password) + { + $this->auth = new FormAuthenticate($this->Collection, [ + 'userModel' => 'Users', + 'passwordHasher' => CallCounterPasswordHasher::class, + ]); + $this->getTableLocator()->get('Users')->updateAll( + ['password' => $password], + ['username' => $username] + ); + + $request = new ServerRequest([ + 'url' => 'posts/index', + 'post' => [ + 'username' => $username, + 'password' => 'anything', + ], + ]); + $result = $this->auth->authenticate($request, new Response()); + $this->assertFalse($result); + + /** @var \TestApp\Auth\CallCounterPasswordHasher $passwordHasher */ + $passwordHasher = $this->auth->passwordHasher(); + + $this->assertInstanceOf(CallCounterPasswordHasher::class, $passwordHasher); + $this->assertSame(1, $passwordHasher->callCount); + } + + public function userList() + { + return [ + ['notexist', ''], + ['mariano', null], + ['mariano', ''], + ['mariano', 'somehash'], + ]; + } } diff --git a/tests/TestCase/Console/CommandCollectionTest.php b/tests/TestCase/Console/CommandCollectionTest.php index 22ff4600616..89e559e139a 100644 --- a/tests/TestCase/Console/CommandCollectionTest.php +++ b/tests/TestCase/Console/CommandCollectionTest.php @@ -59,7 +59,7 @@ public function testConstructor() public function testConstructorInvalidClass() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot use \'stdClass\' for command \'nope\' it is not a subclass of Cake\Console\Shell'); + $this->expectExceptionMessage('Cannot use \'stdClass\' for command \'nope\'. It is not a subclass of Cake\Console\Shell'); new CommandCollection([ 'i18n' => I18nShell::class, 'nope' => stdClass::class, @@ -131,7 +131,7 @@ public function testAddInstance() public function testAddInvalidInstance() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot use \'stdClass\' for command \'routes\' it is not a subclass of Cake\Console\Shell'); + $this->expectExceptionMessage('Cannot use \'stdClass\' for command \'routes\'. It is not a subclass of Cake\Console\Shell'); $collection = new CommandCollection(); $shell = new stdClass(); $collection->add('routes', $shell); @@ -177,7 +177,7 @@ public function testAddCommandInvalidName($name) public function testInvalidShellClassName() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot use \'stdClass\' for command \'routes\' it is not a subclass of Cake\Console\Shell'); + $this->expectExceptionMessage('Cannot use \'stdClass\' for command \'routes\'. It is not a subclass of Cake\Console\Shell'); $collection = new CommandCollection(); $collection->add('routes', stdClass::class); } diff --git a/tests/TestCase/Error/ErrorHandlerTest.php b/tests/TestCase/Error/ErrorHandlerTest.php index 13a98c5b549..481cc578fa7 100644 --- a/tests/TestCase/Error/ErrorHandlerTest.php +++ b/tests/TestCase/Error/ErrorHandlerTest.php @@ -150,6 +150,38 @@ public function testHandleErrorDebugOn() $this->assertRegExp('/
/', $result);
         $this->assertRegExp('/Notice<\/b>/', $result);
         $this->assertRegExp('/variable:\s+wrong/', $result);
+        $this->assertContains(
+            'ErrorHandlerTest.php, line ' . (__LINE__ - 7),
+            $result,
+            'Should contain file and line reference'
+        );
+    }
+
+    /**
+     * test error handling with the _trace_offset context variable
+     *
+     * @return void
+     */
+    public function testHandleErrorTraceOffset()
+    {
+        $this->_restoreError = true;
+
+        set_error_handler(function ($code, $message, $file, $line, $context = null) {
+            $errorHandler = new ErrorHandler();
+            $context['_trace_frame_offset'] = 3;
+            $errorHandler->handleError($code, $message, $file, $line, $context);
+        });
+
+        ob_start();
+        $wrong = $wrong + 1;
+        $result = ob_get_clean();
+
+        $this->assertNotContains(
+            'ErrorHandlerTest.php, line ' . (__LINE__ - 4),
+            $result,
+            'Should not contain file and line reference'
+        );
+        $this->assertNotContains('_trace_frame_offset', $result);
     }
 
     /**
diff --git a/tests/TestCase/ORM/Association/BelongsToManyTest.php b/tests/TestCase/ORM/Association/BelongsToManyTest.php
index a04e35f1ddd..187bf4ef024 100644
--- a/tests/TestCase/ORM/Association/BelongsToManyTest.php
+++ b/tests/TestCase/ORM/Association/BelongsToManyTest.php
@@ -1280,6 +1280,19 @@ public function testEagerLoadingBelongsToManyLimitedFields()
 
         $this->assertNotEmpty($result->tags[0]->id);
         $this->assertEmpty($result->tags[0]->name);
+
+        $result = $table
+            ->find()
+            ->contain([
+                'Tags' => [
+                    'fields' => [
+                        'Tags.name',
+                    ],
+                ],
+            ])
+            ->first();
+        $this->assertNotEmpty($result->tags[0]->name);
+        $this->assertEmpty($result->tags[0]->id);
     }
 
     /**
diff --git a/tests/TestCase/ORM/Association/BelongsToTest.php b/tests/TestCase/ORM/Association/BelongsToTest.php
index 8a7aaa105b7..354614c823d 100644
--- a/tests/TestCase/ORM/Association/BelongsToTest.php
+++ b/tests/TestCase/ORM/Association/BelongsToTest.php
@@ -445,4 +445,27 @@ public function testAttachToNoFieldsSelected()
         $this->assertSame('mariano', $result->author->name);
         $this->assertSame(['author'], array_keys($result->toArray()), 'No other properties included.');
     }
+
+    /**
+     * Test that formatResults in a joined association finder doesn't dirty
+     * the root entity.
+     *
+     * @return void
+     */
+    public function testAttachToFormatResultsNoDirtyResults()
+    {
+        $this->setAppNamespace('TestApp');
+        $articles = $this->getTableLocator()->get('Articles');
+        $articles->belongsTo('Authors')
+            ->setFinder('formatted');
+
+        $query = $articles->find()
+            ->where(['Articles.id' => 1])
+            ->contain('Authors');
+        $result = $query->firstOrFail();
+
+        $this->assertNotEmpty($result->author);
+        $this->assertNotEmpty($result->author->formatted);
+        $this->assertFalse($result->isDirty(), 'Record should be clean as it was pulled from the db.');
+    }
 }
diff --git a/tests/TestCase/ORM/Behavior/TranslateBehaviorTest.php b/tests/TestCase/ORM/Behavior/TranslateBehaviorTest.php
index b4d511967dd..17c8c0350cb 100644
--- a/tests/TestCase/ORM/Behavior/TranslateBehaviorTest.php
+++ b/tests/TestCase/ORM/Behavior/TranslateBehaviorTest.php
@@ -827,6 +827,58 @@ public function testFindSingleLocaleBelongsToMany()
         $this->assertEquals('Translated Info', $result->tags[0]->special_tags[0]->extra_info);
     }
 
+    /**
+     * Tests that parent entity isn't dirty when containing a translated association
+     *
+     * @return void
+     */
+    public function testGetAssociationNotDirtyBelongsTo()
+    {
+        $table = $this->getTableLocator()->get('Articles');
+        $authors = $table->belongsTo('Authors')->getTarget();
+        $authors->addBehavior('Translate', ['fields' => ['name']]);
+
+        $authors->setLocale('eng');
+
+        $entity = $table->get(1);
+        $this->assertNotEmpty($entity);
+        $entity = $table->loadInto($entity, ['Authors']);
+        $this->assertFalse($entity->isDirty());
+        $this->assertNotEmpty($entity->author);
+        $this->assertFalse($entity->author->isDirty());
+
+        $entity = $table->get(1, ['contain' => ['Authors']]);
+        $this->assertNotEmpty($entity);
+        $this->assertFalse($entity->isDirty());
+        $this->assertNotEmpty($entity->author);
+        $this->assertFalse($entity->author->isDirty());
+    }
+
+    /**
+     * Tests that parent entity isn't dirty when containing a translated association
+     *
+     * @return void
+     */
+    public function testGetAssociationNotDirtyHasOne()
+    {
+        $table = $this->getTableLocator()->get('Authors');
+        $table->hasOne('Articles');
+        $table->Articles->addBehavior('Translate', ['fields' => ['title']]);
+
+        $entity = $table->get(1);
+        $this->assertNotEmpty($entity);
+        $entity = $table->loadInto($entity, ['Articles']);
+        $this->assertFalse($entity->isDirty());
+        $this->assertNotEmpty($entity->article);
+        $this->assertFalse($entity->article->isDirty());
+
+        $entity = $table->get(1, ['contain' => 'Articles']);
+        $this->assertNotEmpty($entity);
+        $this->assertFalse($entity->isDirty());
+        $this->assertNotEmpty($entity->article);
+        $this->assertFalse($entity->article->isDirty());
+    }
+
     /**
      * Tests that updating an existing record translations work
      *
diff --git a/tests/TestCase/ORM/QueryRegressionTest.php b/tests/TestCase/ORM/QueryRegressionTest.php
index fcbafe2f9eb..9c59f385b89 100644
--- a/tests/TestCase/ORM/QueryRegressionTest.php
+++ b/tests/TestCase/ORM/QueryRegressionTest.php
@@ -154,12 +154,14 @@ public function testEagerLoadingMismatchingAliasInHasOne()
      */
     public function testEagerLoadingBelongsToManyList()
     {
-        $this->expectException(\InvalidArgumentException::class);
         $this->loadFixtures('Articles', 'Tags', 'ArticlesTags');
         $table = $this->getTableLocator()->get('Articles');
         $table->belongsToMany('Tags', [
             'finder' => 'list',
         ]);
+
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionMessage('"_joinData" is missing from the belongsToMany results');
         $table->find()->contain('Tags')->toArray();
     }
 
diff --git a/tests/TestCase/TestSuite/IntegrationTestTraitTest.php b/tests/TestCase/TestSuite/IntegrationTestTraitTest.php
index 60f8cc6e5ae..ca6572ff174 100644
--- a/tests/TestCase/TestSuite/IntegrationTestTraitTest.php
+++ b/tests/TestCase/TestSuite/IntegrationTestTraitTest.php
@@ -409,7 +409,7 @@ public function testGetSpecificRouteHttpServer()
     public function testConfigApplication()
     {
         $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Cannot load "TestApp\MissingApp" for use in integration');
+        $this->expectExceptionMessage('Cannot load `TestApp\MissingApp` for use in integration');
         $this->configApplication('TestApp\MissingApp', []);
         $this->get('/request_action/test_request_action');
     }
diff --git a/tests/TestCase/View/ViewTest.php b/tests/TestCase/View/ViewTest.php
index e16164fcff1..e25393e8bfe 100644
--- a/tests/TestCase/View/ViewTest.php
+++ b/tests/TestCase/View/ViewTest.php
@@ -1072,6 +1072,33 @@ public function testElementCache()
         Cache::drop('test_view');
     }
 
+    /**
+     * Test elementCache method with namespaces and subfolder
+     *
+     * @return void
+     */
+    public function testElementCacheSubfolder()
+    {
+        Cache::drop('test_view');
+        Cache::setConfig('test_view', [
+            'engine' => 'File',
+            'duration' => '+1 day',
+            'path' => CACHE . 'views/',
+            'prefix' => '',
+        ]);
+        Cache::clear(true, 'test_view');
+
+        $View = $this->PostsController->createView();
+        $View->setElementCache('test_view');
+
+        $result = $View->element('subfolder/test_element', [], ['cache' => true]);
+        $expected = 'this is the test element in subfolder';
+        $this->assertEquals($expected, trim($result));
+
+        $result = Cache::read('element__subfolder_test_element', 'test_view');
+        $this->assertEquals($expected, trim($result));
+    }
+
     /**
      * Test element events
      *
diff --git a/tests/test_app/TestApp/Auth/CallCounterPasswordHasher.php b/tests/test_app/TestApp/Auth/CallCounterPasswordHasher.php
new file mode 100644
index 00000000000..329be4741f5
--- /dev/null
+++ b/tests/test_app/TestApp/Auth/CallCounterPasswordHasher.php
@@ -0,0 +1,36 @@
+callCount++;
+
+        return 'hash123';
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function check($password, $hashedPassword)
+    {
+        if ($hashedPassword == null || $hashedPassword === '') {
+            throw new InvalidArgumentException('Empty hash not expected');
+        }
+
+        $this->callCount++;
+
+        return false;
+    }
+}
diff --git a/tests/test_app/TestApp/Model/Table/ArticlesTable.php b/tests/test_app/TestApp/Model/Table/ArticlesTable.php
index a62ed7b0abc..c2acf9b0538 100644
--- a/tests/test_app/TestApp/Model/Table/ArticlesTable.php
+++ b/tests/test_app/TestApp/Model/Table/ArticlesTable.php
@@ -31,6 +31,7 @@ public function initialize(array $config)
      * Find published
      *
      * @param \Cake\ORM\Query $query The query
+     * @param array $options The options
      * @return \Cake\ORM\Query
      */
     public function findPublished($query, array $options = [])
diff --git a/tests/test_app/TestApp/Model/Table/AuthorsTable.php b/tests/test_app/TestApp/Model/Table/AuthorsTable.php
index 08dc8e3c872..d97f085fd54 100644
--- a/tests/test_app/TestApp/Model/Table/AuthorsTable.php
+++ b/tests/test_app/TestApp/Model/Table/AuthorsTable.php
@@ -34,4 +34,22 @@ public function findByAuthor(Query $query, array $options = [])
 
         return $query;
     }
+
+    /**
+     * Finder that applies a formatter to test dirty associations
+     *
+     * @param \Cake\ORM\Query $query The query
+     * @param array $options The options
+     * @return \Cake\ORM\Query
+     */
+    public function findFormatted(Query $query, array $options = [])
+    {
+        return $query->formatResults(function ($results) {
+            return $results->map(function ($author) {
+                $author->formatted = $author->name . '!!';
+
+                return $author;
+            });
+        });
+    }
 }
diff --git a/tests/test_app/TestApp/Template/Element/subfolder/test_element.ctp b/tests/test_app/TestApp/Template/Element/subfolder/test_element.ctp
new file mode 100644
index 00000000000..d7bcd9c230a
--- /dev/null
+++ b/tests/test_app/TestApp/Template/Element/subfolder/test_element.ctp
@@ -0,0 +1 @@
+this is the test element in subfolder