Browse Source

Merge branch 'master' into 3.next

ADmad 8 years ago
parent
commit
5c037763c8

+ 14 - 4
composer.json

@@ -2,7 +2,17 @@
     "name": "cakephp/cakephp",
     "description": "The CakePHP framework",
     "type": "library",
-    "keywords": ["framework"],
+    "keywords": [
+        "framework",
+        "mvc",
+        "rapid-development",
+        "conventions over configuration",
+        "dry",
+        "orm",
+        "form",
+        "validation",
+        "psr-7"
+    ],
     "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
@@ -21,10 +31,10 @@
         "php": ">=5.6.0",
         "ext-intl": "*",
         "ext-mbstring": "*",
-        "cakephp/chronos": "~1.0",
+        "cakephp/chronos": "^1.0.0",
         "aura/intl": "^3.0.0",
-        "psr/log": "^1.0",
-        "zendframework/zend-diactoros": "^1.4"
+        "psr/log": "^1.0.0",
+        "zendframework/zend-diactoros": "^1.4.0"
     },
     "suggest": {
         "ext-openssl": "To use Security::encrypt() or have secure CSRF token generation.",

+ 20 - 6
src/Cache/composer.json

@@ -1,19 +1,33 @@
 {
     "name": "cakephp/cache",
     "description": "Easy to use Caching library with support for multiple caching backends",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "caching",
+        "cache"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
-        "name": "CakePHP Community",
-        "homepage": "https://cakephp.org"
-    }
+            "name": "CakePHP Community",
+            "homepage": "https://github.com/cakephp/cache/graphs/contributors"
+        }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/cache"
+    },
+    "require": {
+        "php": ">=5.6.0",
+        "cakephp/core": "^3.0.0"
+    },
     "autoload": {
         "psr-4": {
             "Cake\\Cache\\": "."
         }
-    },
-    "require": {
-        "cakephp/core": "~3.0"
     }
 }

+ 23 - 4
src/Collection/composer.json

@@ -1,17 +1,36 @@
 {
     "name": "cakephp/collection",
     "description": "Work easily with arrays and iterators by having a battery of utility traversal methods",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "collections",
+        "iterators",
+        "arrays"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
-        "name": "CakePHP Community",
-        "homepage": "https://cakephp.org"
-    }
+            "name": "CakePHP Community",
+            "homepage": "https://github.com/cakephp/collection/graphs/contributors"
+        }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/collection"
+    },
+    "require": {
+        "php": ">=5.6.0"
+    },
     "autoload": {
         "psr-4": {
             "Cake\\Collection\\": "."
         },
-        "files": ["functions.php"]
+        "files": [
+            "functions.php"
+        ]
     }
 }

+ 21 - 5
src/Core/composer.json

@@ -1,20 +1,36 @@
 {
     "name": "cakephp/core",
     "description": "CakePHP Framework Core classes",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "framework",
+        "core"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
-        "name": "CakePHP Community",
-        "homepage": "https://cakephp.org"
-    }
+            "name": "CakePHP Community",
+            "homepage": "https://github.com/cakephp/core/graphs/contributors"
+        }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/core"
+    },
     "require": {
-        "cakephp/utility": "~3.0"
+        "php": ">=5.6.0",
+        "cakephp/utility": "^3.0.0"
     },
     "autoload": {
         "psr-4": {
             "Cake\\Core\\": "."
         },
-        "files": ["functions.php"]
+        "files": [
+            "functions.php"
+        ]
     }
 }

+ 3 - 5
src/Database/Connection.php

@@ -213,15 +213,13 @@ class Connection implements ConnectionInterface
     /**
      * Connects to the configured database.
      *
-     * @throws \Cake\Database\Exception\MissingConnectionException if credentials are invalid
-     * @return bool true on success or false if already connected.
+     * @throws \Cake\Database\Exception\MissingConnectionException if credentials are invalid.
+     * @return bool true, if the connection was already established or the attempt was successful.
      */
     public function connect()
     {
         try {
-            $this->_driver->connect();
-
-            return true;
+            return $this->_driver->connect();
         } catch (\Exception $e) {
             throw new MissingConnectionException(['reason' => $e->getMessage()]);
         }

+ 19 - 3
src/Database/composer.json

@@ -1,16 +1,32 @@
 {
     "name": "cakephp/database",
     "description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "database",
+        "abstraction",
+        "database abstraction",
+        "pdo"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
             "name": "CakePHP Community",
-            "homepage": "https://cakephp.org"
+            "homepage": "https://github.com/cakephp/database/graphs/contributors"
         }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/database"
+    },
     "require": {
-        "cakephp/core": "~3.0",
-        "cakephp/datasource": "~3.0"
+        "php": ">=5.6.0",
+        "cakephp/core": "^3.0.0",
+        "cakephp/datasource": "^3.0.0"
     },
     "suggest": {
         "cakephp/log": "Require this if you want to use the built-in query logger",

+ 18 - 2
src/Datasource/composer.json

@@ -1,15 +1,31 @@
 {
     "name": "cakephp/datasource",
     "description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "datasource",
+        "connection management",
+        "entity",
+        "query"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
             "name": "CakePHP Community",
-            "homepage": "https://cakephp.org"
+            "homepage": "https://github.com/cakephp/datasource/graphs/contributors"
         }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/datasource"
+    },
     "require": {
-        "cakephp/core": "~3.0"
+        "php": ">=5.6.0",
+        "cakephp/core": "^3.0.0"
     },
     "suggest": {
         "cakephp/utility": "Require this if you decide to use EntityTrait",

+ 20 - 3
src/Event/composer.json

@@ -1,13 +1,30 @@
 {
     "name": "cakephp/event",
     "description": "CakePHP event dispatcher library that helps implementing the observer pattern",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "event",
+        "dispatcher",
+        "observer pattern"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
-        "name": "CakePHP Community",
-        "homepage": "https://cakephp.org"
-    }
+            "name": "CakePHP Community",
+            "homepage": "https://github.com/cakephp/event/graphs/contributors"
+        }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/event"
+    },
+    "require": {
+        "php": ">=5.6.0"
+    },
     "autoload": {
         "psr-4": {
             "Cake\\Event\\": "."

+ 20 - 3
src/Filesystem/composer.json

@@ -1,13 +1,30 @@
 {
     "name": "cakephp/filesystem",
     "description": "CakePHP filesystem convenience classes to help you work with files and folders.",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "filesystem",
+        "files",
+        "folders"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
-        "name": "CakePHP Community",
-        "homepage": "https://cakephp.org"
-    }
+            "name": "CakePHP Community",
+            "homepage": "https://github.com/cakephp/filesystem/graphs/contributors"
+        }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/filesystem"
+    },
+    "require": {
+        "php": ">=5.6.0"
+    },
     "autoload": {
         "psr-4": {
             "Cake\\Filesystem\\": "."

+ 17 - 4
src/Form/composer.json

@@ -1,19 +1,32 @@
 {
     "name": "cakephp/form",
     "description": "CakePHP Form library",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "form"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
             "name": "CakePHP Community",
-            "homepage": "https://cakephp.org"
+            "homepage": "https://github.com/cakephp/form/graphs/contributors"
         }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/form"
+    },
+    "require": {
+        "php": ">=5.6.0",
+        "cakephp/validation": "^3.0.0"
+    },
     "autoload": {
         "psr-4": {
             "Cake\\Form\\": "."
         }
-    },
-    "require": {
-        "cakephp/validation": "~3.0"
     }
 }

+ 32 - 10
src/I18n/composer.json

@@ -1,26 +1,48 @@
 {
     "name": "cakephp/i18n",
     "description": "CakePHP Internationalization library with support for messages translation and dates and numbers localization",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "i18n",
+        "internationalisation",
+        "internationalization",
+        "localisation",
+        "localization",
+        "translation",
+        "date",
+        "number"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
-        "name": "CakePHP Community",
-        "homepage": "https://cakephp.org"
-    }
+            "name": "CakePHP Community",
+            "homepage": "https://github.com/cakephp/i18n/graphs/contributors"
+        }
     ],
-    "autoload": {
-        "psr-4": {
-            "Cake\\I18n\\": "."
-        },
-        "files": ["functions.php"]
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/i18n"
     },
     "require": {
-        "cakephp/core": "~3.0",
+        "php": ">=5.6.0",
         "ext-intl": "*",
-        "cakephp/chronos": "*",
+        "cakephp/core": "^3.0.0",
+        "cakephp/chronos": "^1.0.0",
         "aura/intl": "^3.0.0"
     },
     "suggest": {
         "cakephp/cache": "Require this if you want automatic caching of translators"
+    },
+    "autoload": {
+        "psr-4": {
+            "Cake\\I18n\\": "."
+        },
+        "files": [
+            "functions.php"
+        ]
     }
 }

+ 7 - 7
src/Log/Engine/FileLog.php

@@ -182,23 +182,23 @@ class FileLog extends BaseLog
      */
     protected function _rotateFile($filename)
     {
-        $filepath = $this->_path . $filename;
-        clearstatcache(true, $filepath);
+        $filePath = $this->_path . $filename;
+        clearstatcache(true, $filePath);
 
-        if (!file_exists($filepath) ||
-            filesize($filepath) < $this->_size
+        if (!file_exists($filePath) ||
+            filesize($filePath) < $this->_size
         ) {
             return null;
         }
 
         $rotate = $this->_config['rotate'];
         if ($rotate === 0) {
-            $result = unlink($filepath);
+            $result = unlink($filePath);
         } else {
-            $result = rename($filepath, $filepath . '.' . time());
+            $result = rename($filePath, $filePath . '.' . time());
         }
 
-        $files = glob($filepath . '.*');
+        $files = glob($filePath . '.*');
         if ($files) {
             $filesToDelete = count($files) - $rotate;
             while ($filesToDelete > 0) {

+ 22 - 7
src/Log/composer.json

@@ -1,20 +1,35 @@
 {
     "name": "cakephp/log",
     "description": "CakePHP logging library with support for multiple different streams",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "log",
+        "logging",
+        "streams"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
-        "name": "CakePHP Community",
-        "homepage": "https://cakephp.org"
-    }
+            "name": "CakePHP Community",
+            "homepage": "https://github.com/cakephp/log/graphs/contributors"
+        }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/log"
+    },
+    "require": {
+        "php": ">=5.6.0",
+        "cakephp/core": "^3.0.0",
+        "psr/log": "^1.0.0"
+    },
     "autoload": {
         "psr-4": {
             "Cake\\Log\\": "."
         }
-    },
-    "require": {
-        "cakephp/core": "~3.0",
-        "psr/log": "^1.0"
     }
 }

+ 14 - 9
src/ORM/Association/BelongsToMany.php

@@ -821,14 +821,19 @@ class BelongsToMany extends Association
             $sourceKeys = array_combine($foreignKey, $sourceEntity->extract($bindingKey));
             $targetKeys = array_combine($assocForeignKey, $e->extract($targetPrimaryKey));
 
-            if ($sourceKeys !== $joint->extract($foreignKey)) {
-                $joint->set($sourceKeys, ['guard' => false]);
+            $changedKeys = (
+                $sourceKeys !== $joint->extract($foreignKey) ||
+                $targetKeys !== $joint->extract($assocForeignKey)
+            );
+            // Keys were changed, the junction table record _could_ be
+            // new. By clearing the primary key values, and marking the entity
+            // as new, we let save() sort out whether or not we have a new link
+            // or if we are updating an existing link.
+            if ($changedKeys) {
+                $joint->isNew(true);
+                $joint->unsetProperty($junction->getPrimaryKey())
+                    ->set(array_merge($sourceKeys, $targetKeys), ['guard' => false]);
             }
-
-            if ($targetKeys !== $joint->extract($assocForeignKey)) {
-                $joint->set($targetKeys, ['guard' => false]);
-            }
-
             $saved = $junction->save($joint, $options);
 
             if (!$saved && !empty($options['atomic'])) {
@@ -1306,13 +1311,13 @@ class BelongsToMany extends Association
     protected function _checkPersistenceStatus($sourceEntity, array $targetEntities)
     {
         if ($sourceEntity->isNew()) {
-            $error = 'Source entity needs to be persisted before proceeding';
+            $error = 'Source entity needs to be persisted before links can be created or removed.';
             throw new InvalidArgumentException($error);
         }
 
         foreach ($targetEntities as $entity) {
             if ($entity->isNew()) {
-                $error = 'Cannot link not persisted entities';
+                $error = 'Cannot link entities that have not been persisted yet.';
                 throw new InvalidArgumentException($error);
             }
         }

+ 25 - 10
src/ORM/composer.json

@@ -1,28 +1,43 @@
 {
     "name": "cakephp/orm",
     "description": "CakePHP ORM - Provides a flexible and powerful ORM implementing a data-mapper pattern.",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "orm",
+        "data-mapper",
+        "data-mapper pattern"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
             "name": "CakePHP Community",
-            "homepage": "https://cakephp.org"
+            "homepage": "https://github.com/cakephp/orm/graphs/contributors"
         }
     ],
-    "autoload": {
-        "psr-4": {
-            "Cake\\ORM\\": "."
-        }
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/orm"
     },
     "require": {
-        "cakephp/collection": "~3.0",
-        "cakephp/core": "~3.0",
+        "php": ">=5.6.0",
+        "cakephp/collection": "^3.0.0",
+        "cakephp/core": "^3.0.0",
         "cakephp/datasource": "^3.1.2",
         "cakephp/database": "^3.1.4",
-        "cakephp/event": "~3.0",
-        "cakephp/utility": "~3.0",
-        "cakephp/validation": "~3.0"
+        "cakephp/event": "^3.0.0",
+        "cakephp/utility": "^3.0.0",
+        "cakephp/validation": "^3.0.0"
     },
     "suggest": {
         "cakephp/i18n": "If you are using Translate / Timestamp Behavior."
+    },
+    "autoload": {
+        "psr-4": {
+            "Cake\\ORM\\": "."
+        }
     }
 }

+ 16 - 1
src/Routing/RouteBuilder.php

@@ -718,7 +718,7 @@ class RouteBuilder
      * This method creates a scoped route collection that includes
      * relevant prefix information.
      *
-     * The path parameter is used to generate the routing parameter name.
+     * The $name parameter is used to generate the routing parameter name.
      * For example a path of `admin` would result in `'prefix' => 'admin'` being
      * applied to all connected routes.
      *
@@ -726,6 +726,17 @@ class RouteBuilder
      * Nested prefixes will result in prefix values like `admin/api` which translates
      * to the `Controller\Admin\Api\` namespace.
      *
+     * If you need to have prefix with dots, eg: '/api/v1.0', use 'path' key
+     * for $params argument:
+     *
+     * ```
+     * $route->prefix('api', function($route) {
+     *     $route->prefix('v10', ['path' => '/v1.0'], function($route) {
+     *         // Translates to `Controller\Api\V10\` namespace
+     *     });
+     * });
+     * ```
+     *
      * @param string $name The prefix name to use.
      * @param array|callable $params An array of routing defaults to add to each connected route.
      *   If you have no parameters, this argument can be a callable.
@@ -744,6 +755,10 @@ class RouteBuilder
         }
         $name = Inflector::underscore($name);
         $path = '/' . $name;
+        if (isset($params['path'])) {
+            $path = $params['path'];
+            unset($params['path']);
+        }
         if (isset($this->_params['prefix'])) {
             $name = $this->_params['prefix'] . '/' . $name;
         }

+ 1 - 1
src/TestSuite/IntegrationTestCase.php

@@ -452,7 +452,7 @@ abstract class IntegrationTestCase extends TestCase
             $request = $this->_buildRequest($url, $method, $data);
             $response = $dispatcher->execute($request);
             $this->_requestSession = $request['session'];
-            if ($this->_retainFlashMessages) {
+            if ($this->_retainFlashMessages && $this->_flashMessages) {
                 $this->_requestSession->write('Flash', $this->_flashMessages);
             }
             $this->_response = $response;

+ 25 - 4
src/Utility/composer.json

@@ -1,21 +1,42 @@
 {
     "name": "cakephp/utility",
     "description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "utility",
+        "inflector",
+        "string",
+        "hash",
+        "security"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
             "name": "CakePHP Community",
-            "homepage": "https://cakephp.org"
+            "homepage": "https://github.com/cakephp/utility/graphs/contributors"
         }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/utility"
+    },
+    "require": {
+        "php": ">=5.6.0"
+    },
     "suggest": {
-      "ext-intl": "To use Text::transliterate() or Text::slug()",
-      "lib-ICU": "To use Text::transliterate() or Text::slug()"
+        "ext-intl": "To use Text::transliterate() or Text::slug()",
+        "lib-ICU": "To use Text::transliterate() or Text::slug()"
     },
     "autoload": {
         "psr-4": {
             "Cake\\Utility\\": "."
         },
-        "files": ["bootstrap.php"]
+        "files": [
+            "bootstrap.php"
+        ]
     }
 }

+ 1 - 1
src/Validation/ValidationRule.php

@@ -134,7 +134,7 @@ class ValidationRule
         }
 
         if ($this->_pass) {
-            $args = array_merge([$value], $this->_pass, [$context]);
+            $args = array_values(array_merge([$value], $this->_pass, [$context]));
             $result = $callable(...$args);
         } else {
             $result = $callable($value, $context);

+ 22 - 8
src/Validation/composer.json

@@ -1,21 +1,35 @@
 {
     "name": "cakephp/validation",
     "description": "CakePHP Validation library",
+    "type": "library",
+    "keywords": [
+        "cakephp",
+        "validation",
+        "data validation"
+    ],
+    "homepage": "https://cakephp.org",
     "license": "MIT",
     "authors": [
         {
-        "name": "CakePHP Community",
-        "homepage": "https://cakephp.org"
-    }
+            "name": "CakePHP Community",
+            "homepage": "https://github.com/cakephp/validation/graphs/contributors"
+        }
     ],
+    "support": {
+        "issues": "https://github.com/cakephp/cakephp/issues",
+        "forum": "https://stackoverflow.com/tags/cakephp",
+        "irc": "irc://irc.freenode.org/cakephp",
+        "source": "https://github.com/cakephp/validation"
+    },
+    "require": {
+        "php": ">=5.6.0",
+        "cakephp/utility": "^3.0.0",
+        "cakephp/i18n": "^3.0.0",
+        "psr/http-message": "^1.0.0"
+    },
     "autoload": {
         "psr-4": {
             "Cake\\Validation\\": "."
         }
-    },
-    "require": {
-        "cakephp/utility": "~3.0",
-        "cakephp/i18n": "~3.0",
-        "psr/http-message": "~1.0"
     }
 }

+ 2 - 2
src/View/Form/FormContext.php

@@ -135,9 +135,9 @@ class FormContext implements ContextInterface
     public function attributes($field)
     {
         $column = (array)$this->_form->schema()->field($field);
-        $whitelist = ['length' => null, 'precision' => null];
+        $whiteList = ['length' => null, 'precision' => null];
 
-        return array_intersect_key($column, $whitelist);
+        return array_intersect_key($column, $whiteList);
     }
 
     /**

+ 2 - 1
src/View/Helper/HtmlHelper.php

@@ -334,7 +334,8 @@ class HtmlHelper extends Helper
      *   over value of `escape`)
      * - `confirm` JavaScript confirmation message.
      *
-     * @param string $title The content to be wrapped by `<a>` tags.
+     * @param string|array $title The content to be wrapped by `<a>` tags.
+     *   Can be an array if $url is null. If $url is null, $title will be used as both the URL and title.
      * @param string|array|null $url Cake-relative URL or array of URL parameters, or
      *   external URL (starts with http://)
      * @param array $options Array of options and HTML attributes.

+ 1 - 1
src/View/Helper/TextHelper.php

@@ -123,7 +123,7 @@ class TextHelper extends Helper
                     (?<left>[\[<(]) # left paren,brace
                     (?>
                         # Lax match URL
-                        (?<url>(?:https?|ftp|nntp):\/\/[\p{L}0-9.\-_:]+(?:[\/?][\p{L}0-9.\-_:\/?=&>\[\]()#@\+~%]+)?)
+                        (?<url>(?:https?|ftp|nntp):\/\/[\p{L}0-9.\-_:]+(?:[\/?][\p{L}0-9.\-_:\/?=&>\[\]\(\)\#\@\+~!;,%]+[^-_:?>\[\(\@\+~!;<,.%\s])?)
                         (?<right>[\])>]) # right paren,brace
                     )
                 )

+ 1 - 1
tests/TestCase/Log/LogTest.php

@@ -126,7 +126,7 @@ class LogTest extends TestCase
     }
 
     /**
-     * test config() with valid key name
+     * test invalid level
      *
      * @expectedException \InvalidArgumentException
      * @return void

+ 84 - 19
tests/TestCase/ORM/Association/BelongsToManyTest.php

@@ -67,17 +67,6 @@ class BelongsToManyTest extends TestCase
     }
 
     /**
-     * Tear down
-     *
-     * @return void
-     */
-    public function tearDown()
-    {
-        parent::tearDown();
-        TableRegistry::clear();
-    }
-
-    /**
      * Tests that foreignKey() returns the correct configured value
      *
      * @return void
@@ -390,7 +379,7 @@ class BelongsToManyTest extends TestCase
      * Test linking entities having a non persisted source entity
      *
      * @expectedException \InvalidArgumentException
-     * @expectedExceptionMessage Source entity needs to be persisted before proceeding
+     * @expectedExceptionMessage Source entity needs to be persisted before links can be created or removed
      * @return void
      */
     public function testLinkWithNotPersistedSource()
@@ -410,7 +399,7 @@ class BelongsToManyTest extends TestCase
      * Test liking entities having a non persisted target entity
      *
      * @expectedException \InvalidArgumentException
-     * @expectedExceptionMessage Cannot link not persisted entities
+     * @expectedExceptionMessage Cannot link entities that have not been persisted yet
      * @return void
      */
     public function testLinkWithNotPersistedTarget()
@@ -427,15 +416,88 @@ class BelongsToManyTest extends TestCase
     }
 
     /**
+     * Tests that linking entities will persist correctly with append strategy
+     *
+     * @return void
+     */
+    public function testLinkSuccessSaveAppend()
+    {
+        $articles = TableRegistry::get('Articles');
+        $tags = TableRegistry::get('Tags');
+
+        $config = [
+            'sourceTable' => $articles,
+            'targetTable' => $tags,
+            'joinTable' => 'articles_tags',
+            'saveStrategy' => BelongsToMany::SAVE_APPEND,
+        ];
+        $assoc = $articles->belongsToMany('Tags', $config);
+
+        // Load without tags as that is a main use case for append strategies
+        $article = $articles->get(1);
+        $opts = ['markNew' => false];
+        $tags = [
+            new Entity(['id' => 2, 'name' => 'add'], $opts),
+            new Entity(['id' => 3, 'name' => 'adder'], $opts)
+        ];
+
+        $this->assertTrue($assoc->link($article, $tags));
+        $this->assertCount(2, $article->tags, 'In-memory tags are incorrect');
+        $this->assertSame([2, 3], collection($article->tags)->extract('id')->toList());
+
+        $article = $articles->get(1, ['contain' => ['Tags']]);
+        $this->assertCount(3, $article->tags, 'Persisted tags are wrong');
+        $this->assertSame([1, 2, 3], collection($article->tags)->extract('id')->toList());
+    }
+
+    /**
+     * Tests that linking the same tag to multiple articles works
+     *
+     * @return void
+     */
+    public function testLinkSaveAppendSharedTarget()
+    {
+        $articles = TableRegistry::get('Articles');
+        $tags = TableRegistry::get('Tags');
+        $articlesTags = TableRegistry::get('ArticlesTags');
+        $articlesTags->deleteAll('1=1');
+
+        $config = [
+            'sourceTable' => $articles,
+            'targetTable' => $tags,
+            'joinTable' => 'articles_tags',
+            'saveStrategy' => BelongsToMany::SAVE_APPEND,
+        ];
+        $assoc = $articles->belongsToMany('Tags', $config);
+
+        $articleOne = $articles->get(1);
+        $articleTwo = $articles->get(2);
+
+        $tagTwo = $tags->get(2);
+        $tagThree = $tags->get(3);
+
+        $this->assertTrue($assoc->link($articleOne, [$tagThree, $tagTwo]));
+        $this->assertTrue($assoc->link($articleTwo, [$tagThree]));
+
+        $this->assertCount(2, $articleOne->tags, 'In-memory tags are incorrect');
+        $this->assertSame([3, 2], collection($articleOne->tags)->extract('id')->toList());
+
+        $this->assertCount(1, $articleTwo->tags, 'In-memory tags are incorrect');
+        $this->assertSame([3], collection($articleTwo->tags)->extract('id')->toList());
+        $rows = $articlesTags->find()->all();
+        $this->assertCount(3, $rows, '3 link rows should be created.');
+    }
+
+    /**
      * Tests that liking entities will validate data and pass on to _saveLinks
      *
      * @return void
      */
-    public function testLinkSuccess()
+    public function testLinkSuccessWithMocks()
     {
         $connection = ConnectionManager::get('test');
         $joint = $this->getMockBuilder('\Cake\ORM\Table')
-            ->setMethods(['save'])
+            ->setMethods(['save', 'getPrimaryKey'])
             ->setConstructorArgs([['alias' => 'ArticlesTags', 'connection' => $connection]])
             ->getMock();
 
@@ -452,7 +514,10 @@ class BelongsToManyTest extends TestCase
         $tags = [new Entity(['id' => 2], $opts), new Entity(['id' => 3], $opts)];
         $saveOptions = ['foo' => 'bar'];
 
-        $joint->expects($this->at(0))
+        $joint->method('getPrimaryKey')
+            ->will($this->returnValue(['article_id', 'tag_id']));
+
+        $joint->expects($this->at(1))
             ->method('save')
             ->will($this->returnCallback(function ($e, $opts) use ($entity) {
                 $expected = ['article_id' => 1, 'tag_id' => 2];
@@ -463,7 +528,7 @@ class BelongsToManyTest extends TestCase
                 return $entity;
             }));
 
-        $joint->expects($this->at(1))
+        $joint->expects($this->at(2))
             ->method('save')
             ->will($this->returnCallback(function ($e, $opts) use ($entity) {
                 $expected = ['article_id' => 1, 'tag_id' => 3];
@@ -482,7 +547,7 @@ class BelongsToManyTest extends TestCase
      * Test liking entities having a non persisted source entity
      *
      * @expectedException \InvalidArgumentException
-     * @expectedExceptionMessage Source entity needs to be persisted before proceeding
+     * @expectedExceptionMessage Source entity needs to be persisted before links can be created or removed
      * @return void
      */
     public function testUnlinkWithNotPersistedSource()
@@ -502,7 +567,7 @@ class BelongsToManyTest extends TestCase
      * Test liking entities having a non persisted target entity
      *
      * @expectedException \InvalidArgumentException
-     * @expectedExceptionMessage Cannot link not persisted entities
+     * @expectedExceptionMessage Cannot link entities that have not been persisted
      * @return void
      */
     public function testUnlinkWithNotPersistedTarget()

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

@@ -352,6 +352,27 @@ class RouteBuilderTest extends TestCase
     }
 
     /**
+     * Test creating sub-scopes with prefix()
+     *
+     * @return void
+     */
+    public function testPathWithDotInPrefix()
+    {
+        $routes = new RouteBuilder($this->collection, '/admin', ['prefix' => 'admin']);
+        $res = $routes->prefix('api', function ($r) {
+            $r->prefix('v10', ['path' => '/v1.0'], function ($r2) {
+                $this->assertEquals('/admin/api/v1.0', $r2->path());
+                $this->assertEquals(['prefix' => 'admin/api/v10'], $r2->params());
+                $r2->prefix('b1', ['path' => '/beta.1'], function ($r3) {
+                    $this->assertEquals('/admin/api/v1.0/beta.1', $r3->path());
+                    $this->assertEquals(['prefix' => 'admin/api/v10/b1'], $r3->params());
+                });
+            });
+        });
+        $this->assertNull($res);
+    }
+
+    /**
      * Test creating sub-scopes with plugin()
      *
      * @return void

+ 15 - 0
tests/TestCase/TestSuite/IntegrationTestCaseTest.php

@@ -420,6 +420,21 @@ class IntegrationTestCaseTest extends IntegrationTestCase
     }
 
     /**
+     * Test flash assertions stored with enableRememberFlashMessages() even if
+     * no view is rendered
+     *
+     * @return void
+     */
+    public function testFlashAssertionsWithNoRender()
+    {
+        $this->enableRetainFlashMessages();
+        $this->get('/posts/flashNoRender');
+        $this->assertRedirect();
+
+        $this->assertSession('An error message', 'Flash.flash.0.message');
+    }
+
+    /**
      * Tests the failure message for assertCookieNotSet
      *
      * @expectedException \PHPUnit\Framework\AssertionFailedError

+ 1 - 1
tests/TestCase/Validation/ValidationRuleTest.php

@@ -67,7 +67,7 @@ class ValidationRuleTest extends TestCase
         $Rule = new ValidationRule(['rule' => 'willFail']);
         $this->assertFalse($Rule->process($data, $providers, $context));
 
-        $Rule = new ValidationRule(['rule' => 'willPass']);
+        $Rule = new ValidationRule(['rule' => 'willPass', 'pass' => ['key' => 'value']]);
         $this->assertTrue($Rule->process($data, $providers, $context));
 
         $Rule = new ValidationRule(['rule' => 'willFail3']);

+ 6 - 0
tests/TestCase/View/Form/FormContextTest.php

@@ -25,6 +25,12 @@ use Cake\View\Form\FormContext;
  */
 class FormContextTest extends TestCase
 {
+    /**
+     * The request object.
+     *
+     * @var \Cake\Http\ServerRequest
+     */
+    protected $request;
 
     /**
      * setup method.

+ 16 - 0
tests/TestCase/View/Helper/TextHelperTest.php

@@ -368,6 +368,22 @@ class TextHelperTest extends TestCase
             [
                 'https://sevvlor.com/page%20not%20found',
                 '<a href="https://sevvlor.com/page%20not%20found">https://sevvlor.com/page%20not%20found</a>'
+            ],
+            [
+                'https://fakedomain.ext/path/#!topic/test',
+                '<a href="https://fakedomain.ext/path/#!topic/test">https://fakedomain.ext/path/#!topic/test</a>'
+            ],
+            [
+                'https://fakedomain.ext/path/#!topic/test;other;tag',
+                '<a href="https://fakedomain.ext/path/#!topic/test;other;tag">https://fakedomain.ext/path/#!topic/test;other;tag</a>'
+            ],
+            [
+                'This is text,https://fakedomain.ext/path/#!topic/test,tag, with a comma',
+                'This is text,<a href="https://fakedomain.ext/path/#!topic/test,tag">https://fakedomain.ext/path/#!topic/test,tag</a>, with a comma'
+            ],
+            [
+                'This is text https://fakedomain.ext/path/#!topic/path!',
+                'This is text <a href="https://fakedomain.ext/path/#!topic/path">https://fakedomain.ext/path/#!topic/path</a>!'
             ]
         ];
     }

+ 12 - 0
tests/test_app/TestApp/Controller/PostsController.php

@@ -63,6 +63,18 @@ class PostsController extends AppController
     }
 
     /**
+     * Sets a flash message and redirects (no rendering)
+     *
+     * @return \Cake\Network\Response
+     */
+    public function flashNoRender()
+    {
+        $this->Flash->error('An error message');
+
+        return $this->redirect(['action' => 'index']);
+    }
+
+    /**
      * Stub get method
      *
      * @return void