diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d86333c701..a58688def9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -202,10 +202,12 @@ class FrameworkExtension extends Extension // Choose storage class based on the DSN $supported = array( - 'sqlite' => 'Symfony\Component\HttpKernel\Profiler\SqliteProfilerStorage', - 'mysql' => 'Symfony\Component\HttpKernel\Profiler\MysqlProfilerStorage', - 'file' => 'Symfony\Component\HttpKernel\Profiler\FileProfilerStorage', - 'mongodb' => 'Symfony\Component\HttpKernel\Profiler\MongoDbProfilerStorage', + 'sqlite' => 'Symfony\Component\HttpKernel\Profiler\SqliteProfilerStorage', + 'mysql' => 'Symfony\Component\HttpKernel\Profiler\MysqlProfilerStorage', + 'file' => 'Symfony\Component\HttpKernel\Profiler\FileProfilerStorage', + 'mongodb' => 'Symfony\Component\HttpKernel\Profiler\MongoDbProfilerStorage', + 'memcache' => 'Symfony\Component\HttpKernel\Profiler\MemcacheProfilerStorage', + 'memcached' => 'Symfony\Component\HttpKernel\Profiler\MemcachedProfilerStorage', ); list($class, ) = explode(':', $config['dsn'], 2); if (!isset($supported[$class])) { diff --git a/src/Symfony/Component/HttpKernel/Profiler/BaseMemcacheProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/BaseMemcacheProfilerStorage.php new file mode 100644 index 0000000000..ba4af6e3df --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/BaseMemcacheProfilerStorage.php @@ -0,0 +1,260 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +/** + * Base Memcache storage for profiling information in a Memcache. + * + * @author Andrej Hudec + */ +abstract class BaseMemcacheProfilerStorage implements ProfilerStorageInterface +{ + const TOKEN_PREFIX = 'sf_profiler_'; + + protected $dsn; + protected $lifetime; + + /** + * Constructor. + * + * @param string $dsn A data source name + * @param string $username + * @param string $password + * @param int $lifetime The lifetime to use for the purge + */ + public function __construct($dsn, $username = '', $password = '', $lifetime = 86400) + { + $this->dsn = $dsn; + $this->lifetime = (int) $lifetime; + } + + /** + * {@inheritdoc} + */ + public function find($ip, $url, $limit, $method) + { + $indexName = $this->getIndexName(); + + $indexContent = $this->getValue($indexName); + if (!$indexContent) { + return array(); + } + + $profileList = explode("\n", $indexContent); + $result = array(); + + foreach ($profileList as $item) { + + if ($limit === 0) { + break; + } + + if ($item=='') { + continue; + } + + list($itemToken, $itemIp, $itemMethod, $itemUrl, $itemTime, $itemParent) = explode("\t", $item, 6); + + if ($ip && false === strpos($itemIp, $ip) || $url && false === strpos($itemUrl, $url) || $method && false === strpos($itemMethod, $method)) { + continue; + } + + $result[$itemToken] = array( + 'token' => $itemToken, + 'ip' => $itemIp, + 'method' => $itemMethod, + 'url' => $itemUrl, + 'time' => $itemTime, + 'parent' => $itemParent, + ); + --$limit; + } + + return array_values($result); + } + + /** + * {@inheritdoc} + */ + public function purge() + { + $this->flush(); + } + + /** + * {@inheritdoc} + */ + public function read($token) + { + if (empty($token)) { + return false; + } + + $profile = $this->getValue($this->getItemName($token)); + + if (false !== $profile) { + $profile = $this->createProfileFromData($token, $profile); + } + + return $profile; + } + + /** + * {@inheritdoc} + */ + public function write(Profile $profile) + { + $data = array( + 'token' => $profile->getToken(), + 'parent' => $profile->getParentToken(), + 'children' => array_map(function ($p) { return $p->getToken(); }, $profile->getChildren()), + 'data' => $profile->getCollectors(), + 'ip' => $profile->getIp(), + 'method' => $profile->getMethod(), + 'url' => $profile->getUrl(), + 'time' => $profile->getTime(), + ); + + if ($this->setValue($this->getItemName($profile->getToken()), $data, $this->lifetime)) { + // Add to index + $indexName = $this->getIndexName(); + + $indexRow = implode("\t", array( + $profile->getToken(), + $profile->getIp(), + $profile->getMethod(), + $profile->getUrl(), + $profile->getTime(), + $profile->getParentToken(), + ))."\n"; + + return $this->appendValue($indexName, $indexRow, $this->lifetime); + } + + return false; + } + + /** + * Retrieve item from the memcache server + * + * @param string $key + * + * @return mixed + */ + abstract protected function getValue($key); + + /** + * Store an item on the memcache server under the specified key + * + * @param string $key + * @param mixed $value + * @param int $expiration + * + * @return boolean + */ + abstract protected function setValue($key, $value, $expiration = 0); + + /** + * Flush all existing items at the memcache server + * + * @return boolean + */ + abstract protected function flush(); + + /** + * Append data to an existing item on the memcache server + * @param string $key + * @param string $value + * @param int $expiration + * + * @return boolean + */ + abstract protected function appendValue($key, $value, $expiration = 0); + + private function createProfileFromData($token, $data, $parent = null) + { + $profile = new Profile($token); + $profile->setIp($data['ip']); + $profile->setMethod($data['method']); + $profile->setUrl($data['url']); + $profile->setTime($data['time']); + $profile->setCollectors($data['data']); + + if (!$parent && $data['parent']) { + $parent = $this->read($data['parent']); + } + + if ($parent) { + $profile->setParent($parent); + } + + foreach ($data['children'] as $token) { + if (!$token) { + continue; + } + + if (!$childProfileData = $this->getValue($this->getItemName($token))) { + continue; + } + + $profile->addChild($this->createProfileFromData($token, $childProfileData, $profile)); + } + + return $profile; + } + + /** + * Get item name + * + * @param string $token + * + * @return string + */ + private function getItemName($token) + { + $name = self::TOKEN_PREFIX . $token; + + if ($this->isItemNameValid($name)) { + return $name; + } + + return false; + } + + /** + * Get name of index + * + * @return string + */ + private function getIndexName() + { + $name = self::TOKEN_PREFIX . 'index'; + + if ($this->isItemNameValid($name)) { + return $name; + } + + return false; + } + + private function isItemNameValid($name) + { + $length = strlen($name); + + if ($length > 250) { + throw new \RuntimeException(sprintf('The memcache item key "%s" is too long (%s bytes). Allowed maximum size is 250 bytes.', $name, $length)); + } + + return true; + } + +} diff --git a/src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php new file mode 100644 index 0000000000..e6e43e27a7 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Memcache; + +/** + * Memcache Profiler Storage + * + * @author Andrej Hudec + */ +class MemcacheProfilerStorage extends BaseMemcacheProfilerStorage +{ + + /** + * @var Memcache + */ + private $memcache; + + /** + * Internal convenience method that returns the instance of the Memcache + * + * @return Memcache + */ + protected function getMemcache() + { + if (null === $this->memcache) { + if (!preg_match('#^memcache://(.*)/(.*)$#', $this->dsn, $matches)) { + throw new \RuntimeException('Please check your configuration. You are trying to use Memcache with an invalid dsn. "' . $this->dsn . '"'); + } + + $host = $matches[1]; + $port = $matches[2]; + + $memcache = new Memcache; + $memcache->addServer($host, $port); + + $this->memcache = $memcache; + } + + return $this->memcache; + } + + /** + * {@inheritdoc} + */ + protected function getValue($key) + { + return $this->getMemcache()->get($key); + } + + /** + * {@inheritdoc} + */ + protected function setValue($key, $value, $expiration = 0) + { + return $this->getMemcache()->set($key, $value, false, $expiration); + } + + /** + * {@inheritdoc} + */ + protected function flush() + { + return $this->getMemcache()->flush(); + } + + /** + * {@inheritdoc} + */ + protected function appendValue($key, $value, $expiration = 0) + { + $memcache = $this->getMemcache(); + + if (method_exists($memcache, 'append')) { + + //Memcache v3.0 + if (!$result = $memcache->append($key, $value, false, $expiration)) { + return $memcache->set($key, $value, false, $expiration); + } + + return $result; + } + + //simulate append in Memcache <3.0 + $content = $memcache->get($key); + + return $memcache->set($key, $content . $value, false, $expiration); + } + +} diff --git a/src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php new file mode 100644 index 0000000000..f9b4e10b03 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Memcached; + +/** + * Memcached Profiler Storage + * + * @author Andrej Hudec + */ +class MemcachedProfilerStorage extends BaseMemcacheProfilerStorage +{ + + /** + * @var Memcached + */ + private $memcached; + + /** + * Internal convenience method that returns the instance of the Memcached + * + * @return Memcached + */ + protected function getMemcached() + { + if (null === $this->memcached) { + if (!preg_match('#^memcached://(.*)/(.*)$#', $this->dsn, $matches)) { + throw new \RuntimeException('Please check your configuration. You are trying to use Memcached with an invalid dsn. "' . $this->dsn . '"'); + } + + $host = $matches[1]; + $port = $matches[2]; + + $memcached = new Memcached; + + //disable compression to allow appending + $memcached->setOption(Memcached::OPT_COMPRESSION, false); + + $memcached->addServer($host, $port); + + $this->memcached = $memcached; + } + + return $this->memcached; + } + + /** + * {@inheritdoc} + */ + protected function getValue($key) + { + return $this->getMemcached()->get($key); + } + + /** + * {@inheritdoc} + */ + protected function setValue($key, $value, $expiration = 0) + { + return $this->getMemcached()->set($key, $value, false, $expiration); + } + + /** + * {@inheritdoc} + */ + protected function flush() + { + return $this->getMemcached()->flush(); + } + + /** + * {@inheritdoc} + */ + protected function appendValue($key, $value, $expiration = 0) + { + $memcached = $this->getMemcached(); + + if (!$result = $memcached->append($key, $value)) { + return $memcached->set($key, $value, $expiration); + } + + return $result; + } + +} diff --git a/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcacheProfilerStorageTest.php b/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcacheProfilerStorageTest.php new file mode 100644 index 0000000000..596a207d4b --- /dev/null +++ b/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcacheProfilerStorageTest.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpKernel\Profiler\MemcacheProfilerStorage; +use Symfony\Component\HttpKernel\Profiler\Profile; + +class DummyMemcacheProfilerStorage extends MemcacheProfilerStorage +{ + public function getMemcache() + { + return parent::getMemcache(); + } +} + +class MemcacheProfilerStorageTest extends \PHPUnit_Framework_TestCase +{ + protected static $storage; + + public static function tearDownAfterClass() + { + if (self::$storage) { + self::$storage->purge(); + } + } + + protected function setUp() + { + if (!extension_loaded('memcache')) { + $this->markTestSkipped('MemcacheProfilerStorageTest requires that the extension memcache is loaded'); + } + + self::$storage = new DummyMemcacheProfilerStorage('memcache://127.0.0.1/11211', '', '', 86400); + try { + self::$storage->getMemcache(); + } catch(\Exception $e) { + $this->markTestSkipped('MemcacheProfilerStorageTest requires that there is a Memcache server present on localhost'); + } + + if (self::$storage) { + self::$storage->purge(); + } + } + + public function testStore() + { + for ($i = 0; $i < 10; $i ++) { + $profile = new Profile('token_'.$i); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar'); + $profile->setMethod('GET'); + self::$storage->write($profile); + } + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar', 20, 'GET')), 10, '->write() stores data in the Memcache'); + } + + + public function testChildren() + { + $parentProfile = new Profile('token_parent'); + $parentProfile->setIp('127.0.0.1'); + $parentProfile->setUrl('http://foo.bar/parent'); + + $childProfile = new Profile('token_child'); + $childProfile->setIp('127.0.0.1'); + $childProfile->setUrl('http://foo.bar/child'); + + $parentProfile->addChild($childProfile); + + self::$storage->write($parentProfile); + self::$storage->write($childProfile); + + // Load them from storage + $parentProfile = self::$storage->read('token_parent'); + $childProfile = self::$storage->read('token_child'); + + // Check child has link to parent + $this->assertNotNull($childProfile->getParent()); + $this->assertEquals($parentProfile->getToken(), $childProfile->getParentToken()); + + // Check parent has child + $children = $parentProfile->getChildren(); + $this->assertEquals(1, count($children)); + $this->assertEquals($childProfile->getToken(), $children[0]->getToken()); + } + + public function testStoreSpecialCharsInUrl() + { + // The SQLite storage accepts special characters in URLs (Even though URLs are not + // supposed to contain them) + $profile = new Profile('simple_quote'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/\''); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('simple_quote'), '->write() accepts single quotes in URL'); + + $profile = new Profile('double_quote'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/"'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('double_quote'), '->write() accepts double quotes in URL'); + + $profile = new Profile('backslash'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/\\'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('backslash'), '->write() accepts backslash in URL'); + + $profile = new Profile('comma'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/,'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('comma'), '->write() accepts comma in URL'); + } + + public function testStoreDuplicateToken() + { + $profile = new Profile('token'); + $profile->setUrl('http://example.com/'); + + $this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is unique'); + + $profile->setUrl('http://example.net/'); + + $this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is already present in the Memcache'); + $this->assertEquals('http://example.net/', self::$storage->read('token')->getUrl(), '->write() overwrites the current profile data'); + + $this->assertCount(1, self::$storage->find('', '', 1000, ''), '->find() does not return the same profile twice'); + } + + public function testRetrieveByIp() + { + $profile = new Profile('token'); + $profile->setIp('127.0.0.1'); + $profile->setMethod('GET'); + + self::$storage->write($profile); + + $this->assertEquals(count(self::$storage->find('127.0.0.1', '', 10, 'GET')), 1, '->find() retrieve a record by IP'); + $this->assertEquals(count(self::$storage->find('127.0.%.1', '', 10, 'GET')), 0, '->find() does not interpret a "%" as a wildcard in the IP'); + $this->assertEquals(count(self::$storage->find('127.0._.1', '', 10, 'GET')), 0, '->find() does not interpret a "_" as a wildcard in the IP'); + } + + public function testRetrieveByUrl() + { + $profile = new Profile('simple_quote'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/\''); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('double_quote'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/"'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('backslash'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo\\bar/'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('percent'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/%'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('underscore'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/_'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('semicolon'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/;'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/\'', 10, 'GET')), 1, '->find() accepts single quotes in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/"', 10, 'GET')), 1, '->find() accepts double quotes in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo\\bar/', 10, 'GET')), 1, '->find() accepts backslash in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/;', 10, 'GET')), 1, '->find() accepts semicolon in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/%', 10, 'GET')), 1, '->find() does not interpret a "%" as a wildcard in the URL'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/_', 10, 'GET')), 1, '->find() does not interpret a "_" as a wildcard in the URL'); + } +} diff --git a/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcachedProfilerStorageTest.php b/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcachedProfilerStorageTest.php new file mode 100644 index 0000000000..527274253c --- /dev/null +++ b/tests/Symfony/Tests/Component/HttpKernel/Profiler/MemcachedProfilerStorageTest.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpKernel\Profiler\MemcachedProfilerStorage; +use Symfony\Component\HttpKernel\Profiler\Profile; + +class DummyMemcachedProfilerStorage extends MemcachedProfilerStorage +{ + public function getMemcached() + { + return parent::getMemcached(); + } +} + +class MemcachedProfilerStorageTest extends \PHPUnit_Framework_TestCase +{ + protected static $storage; + + public static function tearDownAfterClass() + { + if (self::$storage) { + self::$storage->purge(); + } + } + + protected function setUp() + { + if (!extension_loaded('memcached')) { + $this->markTestSkipped('MemcachedProfilerStorageTest requires that the extension memcached is loaded'); + } + + self::$storage = new DummyMemcachedProfilerStorage('memcached://127.0.0.1/11211', '', '', 86400); + try { + self::$storage->getMemcached(); + } catch(\Exception $e) { + $this->markTestSkipped('MemcachedProfilerStorageTest requires that there is a Memcache server present on localhost'); + } + + if (self::$storage) { + self::$storage->purge(); + } + } + + public function testStore() + { + for ($i = 0; $i < 10; $i ++) { + $profile = new Profile('token_'.$i); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar'); + $profile->setMethod('GET'); + self::$storage->write($profile); + } + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar', 20, 'GET')), 10, '->write() stores data in the Memcache'); + } + + + public function testChildren() + { + $parentProfile = new Profile('token_parent'); + $parentProfile->setIp('127.0.0.1'); + $parentProfile->setUrl('http://foo.bar/parent'); + + $childProfile = new Profile('token_child'); + $childProfile->setIp('127.0.0.1'); + $childProfile->setUrl('http://foo.bar/child'); + + $parentProfile->addChild($childProfile); + + self::$storage->write($parentProfile); + self::$storage->write($childProfile); + + // Load them from storage + $parentProfile = self::$storage->read('token_parent'); + $childProfile = self::$storage->read('token_child'); + + // Check child has link to parent + $this->assertNotNull($childProfile->getParent()); + $this->assertEquals($parentProfile->getToken(), $childProfile->getParentToken()); + + // Check parent has child + $children = $parentProfile->getChildren(); + $this->assertEquals(1, count($children)); + $this->assertEquals($childProfile->getToken(), $children[0]->getToken()); + } + + public function testStoreSpecialCharsInUrl() + { + // The SQLite storage accepts special characters in URLs (Even though URLs are not + // supposed to contain them) + $profile = new Profile('simple_quote'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/\''); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('simple_quote'), '->write() accepts single quotes in URL'); + + $profile = new Profile('double_quote'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/"'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('double_quote'), '->write() accepts double quotes in URL'); + + $profile = new Profile('backslash'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/\\'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('backslash'), '->write() accepts backslash in URL'); + + $profile = new Profile('comma'); + $profile->setUrl('127.0.0.1', 'http://foo.bar/,'); + self::$storage->write($profile); + $this->assertTrue(false !== self::$storage->read('comma'), '->write() accepts comma in URL'); + } + + public function testStoreDuplicateToken() + { + $profile = new Profile('token'); + $profile->setUrl('http://example.com/'); + + $this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is unique'); + + $profile->setUrl('http://example.net/'); + + $this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is already present in the Memcache'); + $this->assertEquals('http://example.net/', self::$storage->read('token')->getUrl(), '->write() overwrites the current profile data'); + + $this->assertCount(1, self::$storage->find('', '', 1000, ''), '->find() does not return the same profile twice'); + } + + public function testRetrieveByIp() + { + $profile = new Profile('token'); + $profile->setIp('127.0.0.1'); + $profile->setMethod('GET'); + + self::$storage->write($profile); + + $this->assertEquals(count(self::$storage->find('127.0.0.1', '', 10, 'GET')), 1, '->find() retrieve a record by IP'); + $this->assertEquals(count(self::$storage->find('127.0.%.1', '', 10, 'GET')), 0, '->find() does not interpret a "%" as a wildcard in the IP'); + $this->assertEquals(count(self::$storage->find('127.0._.1', '', 10, 'GET')), 0, '->find() does not interpret a "_" as a wildcard in the IP'); + } + + public function testRetrieveByUrl() + { + $profile = new Profile('simple_quote'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/\''); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('double_quote'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/"'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('backslash'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo\\bar/'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('percent'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/%'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('underscore'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/_'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $profile = new Profile('semicolon'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/;'); + $profile->setMethod('GET'); + self::$storage->write($profile); + + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/\'', 10, 'GET')), 1, '->find() accepts single quotes in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/"', 10, 'GET')), 1, '->find() accepts double quotes in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo\\bar/', 10, 'GET')), 1, '->find() accepts backslash in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/;', 10, 'GET')), 1, '->find() accepts semicolon in URLs'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/%', 10, 'GET')), 1, '->find() does not interpret a "%" as a wildcard in the URL'); + $this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/_', 10, 'GET')), 1, '->find() does not interpret a "_" as a wildcard in the URL'); + } +}