diff --git a/composer.json b/composer.json index cd0ad8d..0580651 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "require": { "php": ">=7.4", "ext-ldap": ">=7.4", - "phpmailer/phpmailer": "^6.5.0" + "phpmailer/phpmailer": "^6.5.0", + "symfony/cache": "^v5.4.42" }, "require-dev": { "phpunit/phpunit": ">=8", diff --git a/src/Ltb/Cache.php b/src/Ltb/Cache.php new file mode 100644 index 0000000..5e39477 --- /dev/null +++ b/src/Ltb/Cache.php @@ -0,0 +1,112 @@ +cache = new FilesystemAdapter( + $namespace, + $defaultLifetime, + $directory + ); + + // Clean cache from expired entries + $this->cache->prune(); + + } + + # Generate a cache entry containing a token, + # expiring after $cache_form_expiration seconds + function generate_form_token($cache_form_expiration) + { + $formtoken = hash('sha256', bin2hex(random_bytes(16))); + $cachedToken = $this->cache->getItem($formtoken); + $cachedToken->set($formtoken); + $cachedToken->expiresAfter($cache_form_expiration); + $this->cache->save($cachedToken); + error_log("generated form token: " . + $formtoken . + " valid for $cache_form_expiration s"); + return $formtoken; + } + + # Verify that give token exist in cache + # and if it exists, remove it from cache + function verify_form_token($formtoken) + { + $result = ""; + $cachedToken = $this->cache->getItem($formtoken); + if( $cachedToken->isHit() && $cachedToken->get() == $formtoken ) + { + # Remove token from cache entry + $this->cache->deleteItem($formtoken); + } + else + { + error_log("Invalid form token: sent: $formtoken, stored: " . + $cachedToken->get()); + $result = "invalidformtoken"; + } + return $result; + } + + + # Get a token from the cache + # return the content of the content (can be an array, a string) + function get_token($tokenid) + { + $cached_token = $this->cache->getItem($tokenid); + $cached_token_content = $cached_token->get(); + + if($cached_token->isHit()) + { + return $cached_token_content; + } + else + { + return null; + } + } + + # Save a token to the cache + function save_token($content, $tokenid = null, $cache_token_expiration = null) + { + $msg = ""; + if(is_null($tokenid)) + { + $tokenid = hash('sha256', bin2hex(random_bytes(16))); + $msg .= "Generated cache entry with id: $tokenid"; + } + else + { + $msg .= "Saving existing cache entry with id: $tokenid"; + } + + $cached_token = $this->cache->getItem($tokenid); + $cached_token->set( $content ); + + if(!is_null($cache_token_expiration)) + { + $cached_token->expiresAfter($cache_token_expiration); + $msg .= ", valid for $cache_token_expiration s"; + } + + $this->cache->save($cached_token); + error_log($msg); + + return $tokenid; + } + +} + +?> diff --git a/tests/Ltb/CacheTest.php b/tests/Ltb/CacheTest.php new file mode 100644 index 0000000..b357d2d --- /dev/null +++ b/tests/Ltb/CacheTest.php @@ -0,0 +1,325 @@ +assertTrue($cacheInstance->cache instanceof Symfony\Component\Cache\Adapter\FilesystemAdapter, "Error while initializing cache object"); + } + + + public function test_generate_form_token(): void + { + + $cacheInstance = new \Ltb\Cache( + "testCache", + 0, + null + ); + + $generated_token = ""; + + $cacheInstance->cache = Mockery::mock('FilesystemAdapter'); + $cacheInstance->cache->shouldreceive('getItem') + ->andReturnUsing( + function($token) use (&$generated_token){ + $generated_token = $token; + + $cacheItem = Mockery::mock('CacheItem'); + + $cacheItem->shouldreceive('set') + ->andReturnUsing( + function($formtoken) use (&$token) { + $this->assertEquals($formtoken, + $token, + "Error: received token in set ($formtoken) is different from received token in getItem ($token)"); + } + ); + + $cacheItem->shouldreceive('expiresAfter') + ->with(120); + + return $cacheItem; + } + ); + + $cacheInstance->cache->shouldreceive('save'); + + $receivedToken = $cacheInstance->generate_form_token(120); + $this->assertEquals("$receivedToken", + $generated_token, + "Error: received token in generate_form_token ($receivedToken) is different from received token in getItem ($generated_token)"); + + } + + public function test_verify_form_token_ok(): void + { + + $generated_token = "e712b08e55f8977e2b9ecad35d5180ed24345e76607413411e90df66b9538fa1"; + + $cacheInstance = new \Ltb\Cache( + "testCache", + 0, + null + ); + + $cacheInstance->cache = Mockery::mock('FilesystemAdapter'); + $cacheInstance->cache->shouldreceive('getItem') + ->andReturnUsing( + function($token) use(&$generated_token) { + $cacheItem = Mockery::mock('CacheItem'); + + $cacheItem->shouldreceive('isHit') + ->andReturn(true); + + $cacheItem->shouldreceive('get') + ->andReturn($generated_token); + + return $cacheItem; + } + ); + + $cacheInstance->cache->shouldreceive('deleteItem') + ->with($generated_token); + + $result = $cacheInstance->verify_form_token($generated_token); + $this->assertEquals("", + $result, + "Error: invalid result: '$result' sent by verify_form_token"); + + } + + public function test_verify_form_token_ko(): void + { + + $generated_token = "e712b08e55f8977e2b9ecad35d5180ed24345e76607413411e90df66b9538fa1"; + + $cacheInstance = new \Ltb\Cache( + "testCache", + 0, + null + ); + + $cacheInstance->cache = Mockery::mock('FilesystemAdapter'); + $cacheInstance->cache->shouldreceive('getItem') + ->andReturnUsing( + function($token) use(&$generated_token) { + $cacheItem = Mockery::mock('CacheItem'); + + $cacheItem->shouldreceive('isHit') + ->andReturn(false); + + $cacheItem->shouldreceive('get') + ->andReturn(null); + + return $cacheItem; + } + ); + + $result = $cacheInstance->verify_form_token($generated_token); + $this->assertEquals("invalidformtoken", + $result, + "Error: expected 'invalidformtoken', but received result: '$result' in verify_form_token"); + + } + + public function test_get_token_ok(): void + { + + $tokenid = "e712b08e55f8977e2b9ecad35d5180ed24345e76607413411e90df66b9538fa1"; + $token_content = "test"; + + $cacheInstance = new \Ltb\Cache( + "testCache", + 0, + null + ); + + $cacheInstance->cache = Mockery::mock('FilesystemAdapter'); + $cacheInstance->cache->shouldreceive('getItem') + ->andReturnUsing( + function($token) use(&$token_content) { + $cacheItem = Mockery::mock('CacheItem'); + + $cacheItem->shouldreceive('isHit') + ->andReturn(true); + + $cacheItem->shouldreceive('get') + ->andReturn($token_content); + + return $cacheItem; + } + ); + + $result = $cacheInstance->get_token($tokenid); + $this->assertEquals($token_content, + $result, + "Unexpected token content: '$result' sent by get_token"); + + } + + public function test_get_token_ko(): void + { + + $tokenid = "e712b08e55f8977e2b9ecad35d5180ed24345e76607413411e90df66b9538fa1"; + $token_content = "test"; + + $cacheInstance = new \Ltb\Cache( + "testCache", + 0, + null + ); + + $cacheInstance->cache = Mockery::mock('FilesystemAdapter'); + $cacheInstance->cache->shouldreceive('getItem') + ->andReturnUsing( + function($token) use(&$token_content) { + $cacheItem = Mockery::mock('CacheItem'); + + $cacheItem->shouldreceive('isHit') + ->andReturn(false); + + $cacheItem->shouldreceive('get') + ->andReturn(null); + + return $cacheItem; + } + ); + + $result = $cacheInstance->get_token($tokenid); + $this->assertEquals(null, + $result, + "Unexpected not null token content: '$result' sent by get_token"); + + } + + public function test_save_token_new(): void + { + + $tokenid = ""; + $token_content = [ + 'param1' => 'value1', + 'param2' => 'value2' + ]; + $cache_token_expiration = 3600; + + $cacheInstance = new \Ltb\Cache( + "testCache", + 0, + null + ); + + $cacheInstance->cache = Mockery::mock('FilesystemAdapter'); + $cacheInstance->cache->shouldreceive('getItem') + ->andReturnUsing( + function($token) use(&$tokenid, &$token_content, &$cache_token_expiration) { + $tokenid = $token; + $cacheItem = Mockery::mock('CacheItem'); + + $cacheItem->shouldreceive('set') + ->with($token_content); + + $cacheItem->shouldreceive('expiresAfter') + ->with($cache_token_expiration); + + return $cacheItem; + } + ); + + $cacheInstance->cache->shouldreceive('save'); + + $result = $cacheInstance->save_token($token_content, null, $cache_token_expiration); + $this->assertEquals($tokenid, + $result, + "Bad token id sent by save_token function"); + + } + + public function test_save_token_existing(): void + { + + $tokenid = "e712b08e55f8977e2b9ecad35d5180ed24345e76607413411e90df66b9538fa1"; + $token_content = [ + 'par1' => 'val1', + 'par2' => 'val2' + ]; + $cache_token_expiration = 3600; + + $cacheInstance = new \Ltb\Cache( + "testCache", + 0, + null + ); + + $cacheInstance->cache = Mockery::mock('FilesystemAdapter'); + $cacheInstance->cache->shouldreceive('getItem') + ->with($tokenid) + ->andReturnUsing( + function($token) use(&$tokenid, &$token_content, &$cache_token_expiration) { + $cacheItem = Mockery::mock('CacheItem'); + + $cacheItem->shouldreceive('set') + ->with($token_content); + + $cacheItem->shouldreceive('expiresAfter') + ->with($cache_token_expiration); + + return $cacheItem; + } + ); + + $cacheInstance->cache->shouldreceive('save'); + + $result = $cacheInstance->save_token($token_content, $tokenid, $cache_token_expiration); + $this->assertEquals($tokenid, + $result, + "Bad token id sent by save_token function"); + + } + + public function test_save_token_existing_noexpiration(): void + { + + $tokenid = "e712b08e55f8977e2b9ecad35d5180ed24345e76607413411e90df66b9538fa1"; + $token_content = [ + 'par1' => 'val1', + 'par2' => 'val2' + ]; + + $cacheInstance = new \Ltb\Cache( + "testCache", + 0, + null + ); + + $cacheInstance->cache = Mockery::mock('FilesystemAdapter'); + $cacheInstance->cache->shouldreceive('getItem') + ->with($tokenid) + ->andReturnUsing( + function($token) use(&$tokenid, &$token_content) { + $cacheItem = Mockery::mock('CacheItem'); + + $cacheItem->shouldreceive('set') + ->with($token_content); + + return $cacheItem; + } + ); + + $cacheInstance->cache->shouldreceive('save'); + + $result = $cacheInstance->save_token($token_content, $tokenid); + $this->assertEquals($tokenid, + $result, + "Bad token id sent by save_token function"); + + } +}