diff --git a/composer.json b/composer.json index 49d85f8f5ff1950cfbad208a5567d169b5da4cc8..a837f10218ea42537d30ca92852e9ab6042e7e62 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,6 @@ "jaimeperez/twig-configurable-i18n": "^1.2" }, "require-dev": { - "ext-pdo_sqlite": "*", "phpunit/phpunit": "~4.8.35", "mikey179/vfsStream": "~1.6", "friendsofphp/php-cs-fixer": "^2.2" diff --git a/modules/core/lib/Storage/SQLPermanentStorage.php b/modules/core/lib/Storage/SQLPermanentStorage.php index 54bb5642bf1b59201b850164f1ac343489d4b64a..8788fd7ccb15f828b683e00caf5d6b2b7b4316d8 100644 --- a/modules/core/lib/Storage/SQLPermanentStorage.php +++ b/modules/core/lib/Storage/SQLPermanentStorage.php @@ -9,198 +9,209 @@ * @author Andreas Ă…kre Solberg <andreas@uninett.no>, UNINETT AS. * @package SimpleSAMLphp */ -class sspmod_core_Storage_SQLPermanentStorage { - - private $db; - - function __construct($name, $config = NULL) { - if (is_null($config)) - $config = SimpleSAML_Configuration::getInstance(); - - $datadir = $config->getPathValue('datadir', 'data/'); - - if (!is_dir($datadir)) - throw new Exception('Data directory [' . $datadir. '] does not exist'); - if (!is_writable($datadir)) - throw new Exception('Data directory [' . $datadir. '] is not writable'); - - $sqllitedir = $datadir . 'sqllite/'; - if (!is_dir($sqllitedir)) { - mkdir($sqllitedir); - } - - $dbfile = $sqllitedir . $name . '.sqllite'; - - if ($this->db = new SQLiteDatabase($dbfile)) { - $q = @$this->db->query('SELECT key1 FROM data LIMIT 1'); - if ($q === false) { - $this->db->queryExec(' - CREATE TABLE data ( - key1 text, - key2 text, - type text, - value text, - created timestamp, - updated timestamp, - expire timestamp, - PRIMARY KEY (key1,key2,type) - ); - '); - } - } else { - throw new Exception('Error creating SQL lite database [' . $dbfile . '].'); - } - } - - public function set($type, $key1, $key2, $value, $duration = NULL) { - if ($this->exists($type, $key1, $key2)) { - $this->update($type, $key1, $key2, $value, $duration); - } else { - $this->insert($type, $key1, $key2, $value, $duration); - } - } - - private function insert($type, $key1, $key2, $value, $duration = NULL) { - - $setDuration = ''; - if (is_null($duration)) { - $setDuration = 'NULL'; - } else { - $setDuration = "'" . sqlite_escape_string(time() + $duration) . "'"; - } - - $query = "INSERT INTO data (key1,key2,type,created,updated,expire,value) VALUES (" . - "'" . sqlite_escape_string($key1) . "'," . - "'" . sqlite_escape_string($key2) . "'," . - "'" . sqlite_escape_string($type) . "'," . - "'" . sqlite_escape_string(time()) . "'," . - "'" . sqlite_escape_string(time()) . "'," . - $setDuration . "," . - "'" . sqlite_escape_string(serialize($value)) . "')"; - $results = $this->db->queryExec($query); - return $results; - } - - private function update($type, $key1, $key2, $value, $duration = NULL) { - - $setDuration = ''; - if (is_null($duration)) { - $setDuration = ", expire = NULL "; - } else { - $setDuration = ", expire = '" . sqlite_escape_string(time() + $duration) . "' "; - } - - $query = "UPDATE data SET " . - "updated = '" . sqlite_escape_string(time()) . "'," . - "value = '" . sqlite_escape_string(serialize($value)) . "'" . - $setDuration . - "WHERE " . - "key1 = '" . sqlite_escape_string($key1) . "' AND " . - "key2 = '" . sqlite_escape_string($key2) . "' AND " . - "type = '" . sqlite_escape_string($type) . "'"; - $results = $this->db->queryExec($query); - return $results; - } - - public function get($type = NULL, $key1 = NULL, $key2 = NULL) { - - $condition = self::getCondition($type, $key1, $key2); - $query = "SELECT * FROM data WHERE " . $condition; - $results = $this->db->arrayQuery($query, SQLITE_ASSOC); - - if (count($results) !== 1) return NULL; - - $res = $results[0]; - $res['value'] = unserialize($res['value']); - return $res; - } - - /* - * Return the value directly (not in a container) - */ - public function getValue($type = NULL, $key1 = NULL, $key2 = NULL) { - $res = $this->get($type, $key1, $key2); - if ($res === NULL) return NULL; - return $res['value']; - } - - public function exists($type, $key1, $key2) { - $query = "SELECT * FROM data WHERE " . - "key1 = '" . sqlite_escape_string($key1) . "' AND " . - "key2 = '" . sqlite_escape_string($key2) . "' AND " . - "type = '" . sqlite_escape_string($type) . "' LIMIT 1"; - $results = $this->db->arrayQuery($query, SQLITE_ASSOC); - return (count($results) == 1); - } - - public function getList($type = NULL, $key1 = NULL, $key2 = NULL) { - - $condition = self::getCondition($type, $key1, $key2); - $query = "SELECT * FROM data WHERE " . $condition; - $results = $this->db->arrayQuery($query, SQLITE_ASSOC); - if (count($results) == 0) return NULL; - - foreach($results AS $key => $value) { - $results[$key]['value'] = unserialize($results[$key]['value']); - } - return $results; - } - - public function getKeys($type = NULL, $key1 = NULL, $key2 = NULL, $whichKey = 'type') { - - if (!in_array($whichKey, array('key1', 'key2', 'type'), true)) - throw new Exception('Invalid key type'); - - $condition = self::getCondition($type, $key1, $key2); - - $query = "SELECT DISTINCT " . $whichKey . " FROM data WHERE " . $condition; - $results = $this->db->arrayQuery($query, SQLITE_ASSOC); - - if (count($results) == 0) return NULL; - - $resarray = array(); - foreach($results AS $key => $value) { - $resarray[] = $value[$whichKey]; - } - - return $resarray; - } - - - public function remove($type, $key1, $key2) { - $query = "DELETE FROM data WHERE " . - "key1 = '" . sqlite_escape_string($key1) . "' AND " . - "key2 = '" . sqlite_escape_string($key2) . "' AND " . - "type = '" . sqlite_escape_string($type) . "'"; - $results = $this->db->arrayQuery($query, SQLITE_ASSOC); - return (count($results) == 1); - } - - public function removeExpired() { - $query = "DELETE FROM data WHERE expire NOT NULL AND expire < " . time(); - $this->db->arrayQuery($query, SQLITE_ASSOC); - $changes = $this->db->changes(); - return $changes; - } - - - /** - * Create a SQL condition statement based on parameters - */ - private static function getCondition($type = NULL, $key1 = NULL, $key2 = NULL) { - $conditions = array(); - - if (!is_null($type)) $conditions[] = "type = '" . sqlite_escape_string($type) . "'"; - if (!is_null($key1)) $conditions[] = "key1 = '" . sqlite_escape_string($key1) . "'"; - if (!is_null($key2)) $conditions[] = "key2 = '" . sqlite_escape_string($key2) . "'"; - - if (count($conditions) === 0) return '1'; - - $condition = join(' AND ', $conditions); - - return $condition; - } - - +class sspmod_core_Storage_SQLPermanentStorage +{ + private $db; + + public function __construct($name, $config = null) + { + if (is_null($config)) { + $config = SimpleSAML_Configuration::getInstance(); + } + + $datadir = $config->getPathValue('datadir', 'data/'); + + if (!is_dir($datadir)) { + throw new Exception('Data directory ['.$datadir.'] does not exist'); + } else if (!is_writable($datadir)) { + throw new Exception('Data directory ['.$datadir.'] is not writable'); + } + + $sqllitedir = $datadir.'sqllite/'; + if (!is_dir($sqllitedir)) { + mkdir($sqllitedir); + } + + $dbfile = 'sqlite:'.$sqllitedir.$name.'.sqlite'; + if ($this->db = new \PDO($dbfile)) { + $q = @$this->db->query('SELECT key1 FROM data LIMIT 1'); + if ($q === false) { + $this->db->exec(' + CREATE TABLE data ( + key1 text, + key2 text, + type text, + value text, + created timestamp, + updated timestamp, + expire timestamp, + PRIMARY KEY (key1,key2,type) + ); + '); + } + } else { + throw new Exception('Error creating SQL lite database ['.$dbfile.'].'); + } + } + + public function set($type, $key1, $key2, $value, $duration = null) + { + if ($this->exists($type, $key1, $key2)) { + $this->update($type, $key1, $key2, $value, $duration); + } else { + $this->insert($type, $key1, $key2, $value, $duration); + } + } + + private function insert($type, $key1, $key2, $value, $duration = null) + { + $expire = is_null($duration) ? null : (time() + $duration); + + $query = "INSERT INTO data (key1, key2, type, created, updated, expire, value)". + " VALUES(:key1, :key2, :type, :created, :updated, :expire, :value)"; + $prepared = $this->db->prepare($query); + $data = array(':key1' => $key1, ':key2' => $key2, + ':type' => $type, ':created' => time(), + ':updated' => time(), ':expire' => $expire, + ':value' => serialize($value)); + $prepared->execute($data); + $results = $prepared->fetchAll(PDO::FETCH_ASSOC); + return $results; + } + + private function update($type, $key1, $key2, $value, $duration = null) + { + $expire = is_null($duration) ? null : (time() + $duration); + + $query = "UPDATE data SET updated = :updated, value = :value, expire = :expire WHERE key1 = :key1 AND key2 = :key2 AND type = :type"; + $prepared = $this->db->prepare($query); + $data = array(':key1' => $key1, ':key2' => $key2, + ':type' => $type, ':updated' => time(), + ':expire' => $expire, ':value' => serialize($value)); + $prepared->execute($data); + $results = $prepared->fetchAll(PDO::FETCH_ASSOC); + return $results; + } + + public function get($type = null, $key1 = null, $key2 = null) + { + $conditions = self::getCondition($type, $key1, $key2); + $query = 'SELECT * FROM data WHERE '.$conditions; + + $prepared = $this->db->prepare($query); + $prepared->execute(); + $results = $prepared->fetchAll(PDO::FETCH_ASSOC); + if (count($results) !== 1) { + return null; + } + + $res = $results[0]; + $res['value'] = unserialize($res['value']); + return $res; + } + + /* + * Return the value directly (not in a container) + */ + public function getValue($type = null, $key1 = null, $key2 = null) + { + $res = $this->get($type, $key1, $key2); + if ($res === null) { + return null; + } + return $res['value']; + } + + public function exists($type, $key1, $key2) + { + $query = 'SELECT * FROM data WHERE type = :type AND key1 = :key1 AND key2 = :key2 LIMIT 1'; + $prepared = $this->db->prepare($query); + $data = array(':type' => $type, ':key1' => $key1, ':key2' => $key2); + $prepared->execute($data); + $results = $prepared->fetchAll(PDO::FETCH_ASSOC); + return (count($results) == 1); + } + + public function getList($type = null, $key1 = null, $key2 = null) + { + $conditions = self::getCondition($type, $key1, $key2); + $query = 'SELECT * FROM data WHERE '.$conditions; + $prepared = $this->db->prepare($query); + $prepared->execute(); + + $results = $prepared->fetchAll(PDO::FETCH_ASSOC); + if (count($results) == 0) { + return null; + } + + foreach ($results as $key => $value) { + $results[$key]['value'] = unserialize($results[$key]['value']); + } + return $results; + } + + public function getKeys($type = null, $key1 = null, $key2 = null, $whichKey = 'type') + { + if (!in_array($whichKey, array('key1', 'key2', 'type'), true)) { + throw new Exception('Invalid key type'); + } + + $conditions = self::getCondition($type, $key1, $key2); + $query = 'SELECT DISTINCT :whichKey FROM data WHERE '.$conditions; + $prepared = $this->db->prepare($query); + $data = array('whichKey' => $whichKey); + $prepared->execute($data); + $results = $prepared->fetchAll(PDO::FETCH_ASSOC); + + if (count($results) == 0) { + return null; + } + + $resarray = array(); + foreach ($results as $key => $value) { + $resarray[] = $value[$whichKey]; + } + return $resarray; + } + + public function remove($type, $key1, $key2) + { + $query = 'DELETE FROM data WHERE type = :type AND key1 = :key1 AND key2 = :key2'; + $prepared = $this->db->prepare($query); + $data = array(':type' => $type, ':key1' => $key1, ':key2' => $key2); + $prepared->execute($data); + $results = $prepared->fetchAll(PDO::FETCH_ASSOC); + return (count($results) == 1); + } + + public function removeExpired() + { + $query = "DELETE FROM data WHERE expire NOT NULL AND expire < :expire"; + $prepared = $this->db->prepare($query); + $data = array(':expire' => time()); + $prepared->execute($data); + return $prepared->rowCount(); + } + + /** + * Create a SQL condition statement based on parameters + */ + private function getCondition($type = null, $key1 = null, $key2 = null) + { + $conditions = array(); + if (!is_null($type)) { + $conditions[] = "type = ".$this->db->quote($type); + } + if (!is_null($key1)) { + $conditions[] = "key1 = ".$this->db->quote($key1); + } + if (!is_null($key2)) { + $conditions[] = "key2 = ".$this->db->quote($key2); + } + if (count($conditions) === 0) { + return '1'; + } + return join(' AND ', $conditions); + } } diff --git a/tests/modules/core/lib/Storage/SQLPermanentStorageTest.php b/tests/modules/core/lib/Storage/SQLPermanentStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8e1071050440000315d29522754401565964ed76 --- /dev/null +++ b/tests/modules/core/lib/Storage/SQLPermanentStorageTest.php @@ -0,0 +1,84 @@ +<?php + +use PHPUnit\Framework\TestCase; + +/** + * Test for the SQLPermanentStorage class. + */ +class Test_Core_Storage_SQLPermanentStorage extends TestCase +{ + private static $sql; + + public static function setUpBeforeClass() + { + // Create instance + $config = \SimpleSAML_Configuration::loadFromArray([ + 'datadir' => sys_get_temp_dir(), + ]); + self::$sql = new sspmod_core_Storage_SQLPermanentStorage('test', $config); + } + + public static function tearDownAfterClass() + { + self::$sql = null; + unlink(sys_get_temp_dir().'/sqllite/test.sqlite'); + } + + public function testSet() + { + // Set a new value + self::$sql->set('testtype', 'testkey1', 'testkey2', 'testvalue', 0); + + // Test getCondition + $result = self::$sql->get(); + $this->assertEquals('testvalue', $result['value']); + } + + public function testSetOverwrite() + { + // Overwrite existing value + self::$sql->set('testtype', 'testkey1', 'testkey2', 'testvaluemodified', 0); + + // Test that the value was actually overwriten + $result = self::$sql->getValue('testtype', 'testkey1', 'testkey2'); + $this->assertEquals('testvaluemodified', $result); + + $result = self::$sql->getList('testtype', 'testkey1', 'testkey2'); + $this->assertEquals('testvaluemodified', $result[0]['value']); + } + + public function testNonexistentKey() + { + // Test that getting some non-existing key will return null + $result = self::$sql->getValue('testtype_nonexistent', 'testkey1_nonexistent', 'testkey2_nonexistent'); + $this->assertNull($result); + $result = self::$sql->getList('testtype_nonexistent', 'testkey1_nonexistent', 'testkey2_nonexistent'); + $this->assertNull($result); + $result = self::$sql->get('testtype_nonexistent', 'testkey1_nonexistent', 'testkey2_nonexistent'); + $this->assertNull($result); + } + + public function testExpiration() + { + // Make sure the earlier created entry has expired now + sleep(1); + + // Now add a second entry that never expires + self::$sql->set('testtype', 'testkey1_nonexpiring', 'testkey2_nonexpiring', 'testvalue_nonexpiring', null); + + // Expire entries and verify that only the second one is left + self::$sql->removeExpired(); + $result = self::$sql->getValue('testtype', 'testkey1', 'testkey2'); + $this->assertNull($result); + $result = self::$sql->getValue('testtype', 'testkey1_nonexpiring', 'testkey2_nonexpiring'); + $this->assertEquals('testvalue_nonexpiring', $result); + } + + public function testRemove() + { + // Now remove the nonexpiring entry and make sure it's gone + self::$sql->remove('testtype', 'testkey1_nonexpiring', 'testkey2_nonexpiring'); + $result = self::$sql->getValue('testtype', 'testkey1_nonexpiring', 'testkey2_nonexpiring'); + $this->assertNull($result); + } +}