Browse Source

Replace parse_url() with a regex.

parse_url() fails on passwords that contain # and other special URL
characters. I've re-implemented a DSN parser with a regular expression.
It handles all the DSN flavours in the core tests, and should be
transparent to userland code.

Refs #11019
Mark Story 8 years ago
parent
commit
519b0ce66f
2 changed files with 37 additions and 9 deletions
  1. 14 9
      src/Core/StaticConfigTrait.php
  2. 23 0
      tests/TestCase/Datasource/ConnectionManagerTest.php

+ 14 - 9
src/Core/StaticConfigTrait.php

@@ -249,18 +249,23 @@ trait StaticConfigTrait
             throw new InvalidArgumentException('Only strings can be passed to parseDsn');
         }
 
-        $scheme = '';
-        if (preg_match("/^([\w\\\]+)/", $dsn, $matches)) {
-            $scheme = $matches[1];
-            $dsn = preg_replace("/^([\w\\\]+)/", 'file', $dsn);
-        }
+        $pattern = '/^(?P<scheme>[\w\\\\]+):\/\/((?P<user>.*?)(:(?P<password>.*?))?@)?' .
+            '((?P<host>[.\w\\\\]+)(:(?P<port>\d+))?)?' .
+            '(?P<path>\/[^?]*)?(\?(?P<query>.*))?$/';
+        preg_match($pattern, $dsn, $parsed);
 
-        $parsed = parse_url($dsn);
-        if ($parsed === false) {
-            return $dsn;
+        if (empty($parsed)) {
+            return false;
+        }
+        foreach ($parsed as $k => $v) {
+            if (is_int($k)) {
+                unset($parsed[$k]);
+            }
+            if ($v === '') {
+                unset($parsed[$k]);
+            }
         }
 
-        $parsed['scheme'] = $scheme;
         $query = '';
 
         if (isset($parsed['query'])) {

+ 23 - 0
tests/TestCase/Datasource/ConnectionManagerTest.php

@@ -421,6 +421,29 @@ class ConnectionManagerTest extends TestCase
     }
 
     /**
+     * Test parseDsn with special characters in the password.
+     *
+     * @return void
+     */
+    public function testParseDsnSpecialPassword()
+    {
+        $dsn = 'mysql://user:pas#][{}$%20@!@localhost:3306/database?log=1&quoteIdentifiers=1';
+        $expected = [
+            'className' => 'Cake\Database\Connection',
+            'database' => 'database',
+            'driver' => 'Cake\Database\Driver\Mysql',
+            'host' => 'localhost',
+            'password' => 'pas#][{}$%20@!',
+            'port' => 3306,
+            'scheme' => 'mysql',
+            'username' => 'user',
+            'log' => 1,
+            'quoteIdentifiers' => 1
+        ];
+        $this->assertEquals($expected, ConnectionManager::parseDsn($dsn));
+    }
+
+    /**
      * Tests that directly setting an instance in a config, will not return a different
      * instance later on
      *