From b2d40ed61e01e85064159e2e34ffc167814d34c6 Mon Sep 17 00:00:00 2001 From: helpfulrobot Date: Fri, 18 Dec 2015 07:11:01 +1300 Subject: [PATCH] Converted to PSR-2 --- code/SQLite3Connector.php | 303 ++--- code/SQLite3Database.php | 1022 +++++++++-------- code/SQLite3Query.php | 106 +- code/SQLite3QueryBuilder.php | 164 +-- code/SQLite3SchemaManager.php | 1185 +++++++++++--------- code/SQLiteDatabaseConfigurationHelper.php | 389 ++++--- 6 files changed, 1672 insertions(+), 1497 deletions(-) diff --git a/code/SQLite3Connector.php b/code/SQLite3Connector.php index b83d4ee..8468f14 100644 --- a/code/SQLite3Connector.php +++ b/code/SQLite3Connector.php @@ -5,167 +5,182 @@ * * @package SQLite3 */ -class SQLite3Connector extends DBConnector { +class SQLite3Connector extends DBConnector +{ - /** - * The name of the database. - * - * @var string - */ - protected $databaseName; + /** + * The name of the database. + * + * @var string + */ + protected $databaseName; - /** - * Connection to the DBMS. - * - * @var SQLite3 - */ - protected $dbConn; + /** + * Connection to the DBMS. + * + * @var SQLite3 + */ + protected $dbConn; - public function connect($parameters, $selectDB = false) { - $file = $parameters['filepath']; - $this->dbConn = empty($parameters['key']) - ? new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE) - : new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $parameters['key']); - $this->dbConn->busyTimeout(60000); - $this->databaseName = $parameters['database']; - } + public function connect($parameters, $selectDB = false) + { + $file = $parameters['filepath']; + $this->dbConn = empty($parameters['key']) + ? new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE) + : new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $parameters['key']); + $this->dbConn->busyTimeout(60000); + $this->databaseName = $parameters['database']; + } - public function affectedRows() { - return $this->dbConn->changes(); - } + public function affectedRows() + { + return $this->dbConn->changes(); + } - public function getGeneratedID($table) { - return $this->dbConn->lastInsertRowID(); - } + public function getGeneratedID($table) + { + return $this->dbConn->lastInsertRowID(); + } - public function getLastError() { - $message = $this->dbConn->lastErrorMsg(); - return $message === 'not an error' ? null : $message; - } + public function getLastError() + { + $message = $this->dbConn->lastErrorMsg(); + return $message === 'not an error' ? null : $message; + } - public function getSelectedDatabase() { - return $this->databaseName; - } + public function getSelectedDatabase() + { + return $this->databaseName; + } - public function getVersion() { - $version = SQLite3::version(); - return trim($version['versionString']); - } + public function getVersion() + { + $version = SQLite3::version(); + return trim($version['versionString']); + } - public function isActive() { - return $this->databaseName && $this->dbConn; - } + public function isActive() + { + return $this->databaseName && $this->dbConn; + } - /** - * Prepares the list of parameters in preparation for passing to mysqli_stmt_bind_param - * - * @param array $parameters List of parameters - * @return array List of parameters types and values - */ - public function parsePreparedParameters($parameters) { - $values = array(); - foreach($parameters as $value) { - $phpType = gettype($value); - $sqlType = null; + /** + * Prepares the list of parameters in preparation for passing to mysqli_stmt_bind_param + * + * @param array $parameters List of parameters + * @return array List of parameters types and values + */ + public function parsePreparedParameters($parameters) + { + $values = array(); + foreach ($parameters as $value) { + $phpType = gettype($value); + $sqlType = null; - // Allow overriding of parameter type using an associative array - if($phpType === 'array') { - $phpType = $value['type']; - $value = $value['value']; - } + // Allow overriding of parameter type using an associative array + if ($phpType === 'array') { + $phpType = $value['type']; + $value = $value['value']; + } - // Convert php variable type to one that makes mysqli_stmt_bind_param happy - // @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php - switch($phpType) { - case 'boolean': - case 'integer': - $sqlType = SQLITE3_INTEGER; - break; - case 'float': // Not actually returnable from gettype - case 'double': - $sqlType = SQLITE3_FLOAT; - break; - case 'object': // Allowed if the object or resource has a __toString method - case 'resource': - case 'string': - $sqlType = SQLITE3_TEXT; - break; - case 'NULL': - $sqlType = SQLITE3_NULL; - break; - case 'blob': - $sqlType = SQLITE3_BLOB; - break; - case 'array': - case 'unknown type': - default: - user_error("Cannot bind parameter \"$value\" as it is an unsupported type ($phpType)", E_USER_ERROR); - break; - } - $values[] = array( - 'type' => $sqlType, - 'value' => $value - ); - } - return $values; - } + // Convert php variable type to one that makes mysqli_stmt_bind_param happy + // @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php + switch ($phpType) { + case 'boolean': + case 'integer': + $sqlType = SQLITE3_INTEGER; + break; + case 'float': // Not actually returnable from gettype + case 'double': + $sqlType = SQLITE3_FLOAT; + break; + case 'object': // Allowed if the object or resource has a __toString method + case 'resource': + case 'string': + $sqlType = SQLITE3_TEXT; + break; + case 'NULL': + $sqlType = SQLITE3_NULL; + break; + case 'blob': + $sqlType = SQLITE3_BLOB; + break; + case 'array': + case 'unknown type': + default: + user_error("Cannot bind parameter \"$value\" as it is an unsupported type ($phpType)", E_USER_ERROR); + break; + } + $values[] = array( + 'type' => $sqlType, + 'value' => $value + ); + } + return $values; + } - public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) { - // Type check, identify, and prepare parameters for passing to the statement bind function - $parsedParameters = $this->parsePreparedParameters($parameters); + public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) + { + // Type check, identify, and prepare parameters for passing to the statement bind function + $parsedParameters = $this->parsePreparedParameters($parameters); - // Prepare statement - $statement = @$this->dbConn->prepare($sql); - if($statement) { - // Bind and run to statement - for($i = 0; $i < count($parsedParameters); $i++) { - $value = $parsedParameters[$i]['value']; - $type = $parsedParameters[$i]['type']; - $statement->bindValue($i+1, $value, $type); - } - - // Return successful result - $handle = $statement->execute(); - if ($handle) { - return new SQLite3Query($this, $handle); - } - } - - // Handle error - $values = $this->parameterValues($parameters); - $this->databaseError($this->getLastError(), $errorLevel, $sql, $values); - return null; - } + // Prepare statement + $statement = @$this->dbConn->prepare($sql); + if ($statement) { + // Bind and run to statement + for ($i = 0; $i < count($parsedParameters); $i++) { + $value = $parsedParameters[$i]['value']; + $type = $parsedParameters[$i]['type']; + $statement->bindValue($i+1, $value, $type); + } + + // Return successful result + $handle = $statement->execute(); + if ($handle) { + return new SQLite3Query($this, $handle); + } + } + + // Handle error + $values = $this->parameterValues($parameters); + $this->databaseError($this->getLastError(), $errorLevel, $sql, $values); + return null; + } - public function query($sql, $errorLevel = E_USER_ERROR) { - // Return successful result - $handle = @$this->dbConn->query($sql); - if ($handle) { - return new SQLite3Query($this, $handle); - } - - // Handle error - $this->databaseError($this->getLastError(), $errorLevel, $sql); - return null; - } + public function query($sql, $errorLevel = E_USER_ERROR) + { + // Return successful result + $handle = @$this->dbConn->query($sql); + if ($handle) { + return new SQLite3Query($this, $handle); + } + + // Handle error + $this->databaseError($this->getLastError(), $errorLevel, $sql); + return null; + } - public function quoteString($value) { - return "'".$this->escapeString($value)."'"; - } + public function quoteString($value) + { + return "'".$this->escapeString($value)."'"; + } - public function escapeString($value) { - return $this->dbConn->escapeString($value); - } + public function escapeString($value) + { + return $this->dbConn->escapeString($value); + } - public function selectDatabase($name) { - if($name !== $this->databaseName) { - user_error("SQLite3Connector can't change databases. Please create a new database connection", E_USER_ERROR); - } - return true; - } + public function selectDatabase($name) + { + if ($name !== $this->databaseName) { + user_error("SQLite3Connector can't change databases. Please create a new database connection", E_USER_ERROR); + } + return true; + } - public function unloadDatabase() { - $this->dbConn->close(); - $this->databaseName = null; - } + public function unloadDatabase() + { + $this->dbConn->close(); + $this->databaseName = null; + } } diff --git a/code/SQLite3Database.php b/code/SQLite3Database.php index 30d0e6c..b33d80c 100644 --- a/code/SQLite3Database.php +++ b/code/SQLite3Database.php @@ -1,488 +1,534 @@ - '"UTF-8"', - 'locking_mode' => 'NORMAL' - ); - - - /** - * Extension used to distinguish between sqllite database files and other files. - * Required to handle multiple databases. - * - * @return string - */ - public static function database_extension() { - return Config::inst()->get('SQLite3Database', 'database_extension'); - } - - /** - * Check if a database name has a valid extension - * - * @param string $name - * @return boolean - */ - public static function is_valid_database_name($name) { - $extension = self::database_extension(); - if(empty($extension)) return true; - - return substr_compare($name, $extension, -strlen($extension), strlen($extension)) === 0; - } - - /** - * Connect to a SQLite3 database. - * @param array $parameters An map of parameters, which should include: - * - database: The database to connect to, with the correct file extension (.sqlite) - * - path: the path to the SQLite3 database file - * - key: the encryption key (needs testing) - * - memory: use the faster In-Memory database for unit tests - */ - public function connect($parameters) { - - if(!empty($parameters['memory'])) { - Deprecation::notice( - '1.4.0', - "\$databaseConfig['memory'] is deprecated. Use \$databaseConfig['path'] = ':memory:' instead.", - Deprecation::SCOPE_GLOBAL - ); - unset($parameters['memory']); - $parameters['path'] = ':memory:'; - } - - //We will store these connection parameters for use elsewhere (ie, unit tests) - $this->parameters = $parameters; - $this->schemaManager->flushCache(); - - // Ensure database name is set - if(empty($parameters['database'])) { - $parameters['database'] = 'database' . self::database_extension(); - } - $dbName = $parameters['database']; - if(!self::is_valid_database_name($dbName)) { - // If not using the correct file extension for database files then the - // results of SQLite3SchemaManager::databaseList will be unpredictable - $extension = self::database_extension(); - Deprecation::notice('3.2', "SQLite3Database now expects a database file with extension \"$extension\". Behaviour may be unpredictable otherwise."); - } - - // use the very lightspeed SQLite In-Memory feature for testing - if($this->getLivesInMemory()) { - $file = ':memory:'; - } else { - // Ensure path is given - if(empty($parameters['path'])) { - $parameters['path'] = ASSETS_PATH . '/.sqlitedb'; - } - - //assumes that the path to dbname will always be provided: - $file = $parameters['path'] . '/' . $dbName; - if(!file_exists($parameters['path'])) { - SQLiteDatabaseConfigurationHelper::create_db_dir($parameters['path']); - SQLiteDatabaseConfigurationHelper::secure_db_dir($parameters['path']); - } - } - - // 'path' and 'database' are merged into the full file path, which - // is the format that connectors such as PDOConnector expect - $parameters['filepath'] = $file; - - // Ensure that driver is available (required by PDO) - if(empty($parameters['driver'])) { - $parameters['driver'] = $this->getDatabaseServer(); - } - - $this->connector->connect($parameters, true); - - foreach(self::$default_pragma as $pragma => $value) { - $this->setPragma($pragma, $value); - } - - if(empty(self::$default_pragma['locking_mode'])) { - self::$default_pragma['locking_mode'] = $this->getPragma('locking_mode'); - } - } - - /** - * Retrieve parameters used to connect to this SQLLite database - * - * @return array - */ - public function getParameters() { - return $this->parameters; - } - - public function getLivesInMemory() { - return isset($this->parameters['path']) && $this->parameters['path'] === ':memory:'; - } - - public function supportsCollations() { - return true; - } - - public function supportsTimezoneOverride() { - return false; - } - - /** - * Execute PRAGMA commands. - * - * @param string pragma name - * @param string value to set - */ - public function setPragma($pragma, $value) { - $this->query("PRAGMA $pragma = $value"); - } - - /** - * Gets pragma value. - * - * @param string pragma name - * @return string the pragma value - */ - public function getPragma($pragma) { - return $this->query("PRAGMA $pragma")->value(); - } - - public function getDatabaseServer() { - return "sqlite"; - } - - public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR) { - if (!$this->schemaManager->databaseExists($name)) { - // Check DB creation permisson - if (!$create) { - if ($errorLevel !== false) { - user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel); - } - // Unselect database - $this->connector->unloadDatabase(); - return false; - } - $this->schemaManager->createDatabase($name); - } - - // Reconnect using the existing parameters - $parameters = $this->parameters; - $parameters['database'] = $name; - $this->connect($parameters); - return true; - } - - function now(){ - return "datetime('now', 'localtime')"; - } - - function random(){ - return 'random()'; - } - - /** - * The core search engine configuration. - * @todo There is a fulltext search for SQLite making use of virtual tables, the fts3 extension and the - * MATCH operator - * there are a few issues with fts: - * - shared cached lock doesn't allow to create virtual tables on versions prior to 3.6.17 - * - there must not be more than one MATCH operator per statement - * - the fts3 extension needs to be available - * for now we use the MySQL implementation with the MATCH()AGAINST() uglily replaced with LIKE - * - * @param string $keywords Keywords as a space separated string - * @return object DataObjectSet of result pages - */ - public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", - $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false - ) { - $keywords = $this->escapeString(str_replace(array('*','+','-','"','\''), '', $keywords)); - $htmlEntityKeywords = htmlentities(utf8_decode($keywords)); - - $extraFilters = array('SiteTree' => '', 'File' => ''); - - if($extraFilter) { - $extraFilters['SiteTree'] = " AND $extraFilter"; - - if($alternativeFileFilter) $extraFilters['File'] = " AND $alternativeFileFilter"; - else $extraFilters['File'] = $extraFilters['SiteTree']; - } - - // Always ensure that only pages with ShowInSearch = 1 can be searched - $extraFilters['SiteTree'] .= ' AND ShowInSearch <> 0'; - // File.ShowInSearch was added later, keep the database driver backwards compatible - // by checking for its existence first - $fields = $this->getSchemaManager()->fieldList('File'); - if(array_key_exists('ShowInSearch', $fields)) { - $extraFilters['File'] .= " AND ShowInSearch <> 0"; - } - - $limit = $start . ", " . (int) $pageLength; - - $notMatch = $invertedMatch ? "NOT " : ""; - if($keywords) { - $match['SiteTree'] = " - (Title LIKE '%$keywords%' OR MenuTitle LIKE '%$keywords%' OR Content LIKE '%$keywords%' OR MetaDescription LIKE '%$keywords%' OR - Title LIKE '%$htmlEntityKeywords%' OR MenuTitle LIKE '%$htmlEntityKeywords%' OR Content LIKE '%$htmlEntityKeywords%' OR MetaDescription LIKE '%$htmlEntityKeywords%') - "; - $match['File'] = "(Name LIKE '%$keywords%' OR Title LIKE '%$keywords%') AND ClassName = 'File'"; - - // We make the relevance search by converting a boolean mode search into a normal one - $relevanceKeywords = $keywords; - $htmlEntityRelevanceKeywords = $htmlEntityKeywords; - $relevance['SiteTree'] = "(Title LIKE '%$relevanceKeywords%' OR MenuTitle LIKE '%$relevanceKeywords%' OR Content LIKE '%$relevanceKeywords%' OR MetaDescription LIKE '%$relevanceKeywords%') + (Title LIKE '%$htmlEntityRelevanceKeywords%' OR MenuTitle LIKE '%$htmlEntityRelevanceKeywords%' OR Content LIKE '%$htmlEntityRelevanceKeywords%' OR MetaDescription LIKE '%$htmlEntityRelevanceKeywords%')"; - $relevance['File'] = "(Name LIKE '%$relevanceKeywords%' OR Title LIKE '%$relevanceKeywords%')"; - } else { - $relevance['SiteTree'] = $relevance['File'] = 1; - $match['SiteTree'] = $match['File'] = "1 = 1"; - } - - // Generate initial queries and base table names - $baseClasses = array('SiteTree' => '', 'File' => ''); - $queries = array(); - foreach($classesToSearch as $class) { - $queries[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class], "")->dataQuery()->query(); - $fromArr = $queries[$class]->getFrom(); - $baseClasses[$class] = reset($fromArr); - } - - // Make column selection lists - $select = array( - 'SiteTree' => array( - "\"ClassName\"", - "\"ID\"", - "\"ParentID\"", - "\"Title\"", - "\"URLSegment\"", - "\"Content\"", - "\"LastEdited\"", - "\"Created\"", - "NULL AS \"Name\"", - "\"CanViewType\"", - "$relevance[SiteTree] AS Relevance" - ), - 'File' => array( - "\"ClassName\"", - "\"ID\"", - "NULL AS \"ParentID\"", - "\"Title\"", - "NULL AS \"URLSegment\"", - "NULL AS \"Content\"", - "\"LastEdited\"", - "\"Created\"", - "\"Name\"", - "NULL AS \"CanViewType\"", - "$relevance[File] AS Relevance" - ) - ); - - // Process queries - foreach($classesToSearch as $class) { - // There's no need to do all that joining - $queries[$class]->setFrom($baseClasses[$class]); - - $queries[$class]->setSelect(array()); - foreach($select[$class] as $clause) { - if(preg_match('/^(.*) +AS +"?([^"]*)"?/i', $clause, $matches)) { - $queries[$class]->selectField($matches[1], $matches[2]); - } else { - $queries[$class]->selectField(str_replace('"', '', $clause)); - } - } - - $queries[$class]->setOrderBy(array()); - } - - // Combine queries - $querySQLs = array(); - $queryParameters = array(); - $totalCount = 0; - foreach($queries as $query) { - $querySQLs[] = $query->sql($parameters); - $queryParameters = array_merge($queryParameters, $parameters); - $totalCount += $query->unlimitedRowCount(); - } - - $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit"; - // Get records - $records = $this->preparedQuery($fullQuery, $queryParameters); - - foreach($records as $record) { - $objects[] = new $record['ClassName']($record); - } - - if(isset($objects)) $doSet = new ArrayList($objects); - else $doSet = new ArrayList(); - $list = new PaginatedList($doSet); - $list->setPageStart($start); - $list->setPageLEngth($pageLength); - $list->setTotalItems($totalCount); - return $list; - } - - /* - * Does this database support transactions? - */ - public function supportsTransactions(){ - return version_compare($this->getVersion(), '3.6', '>='); - } - - public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering')){ - - if(isset($extensions['partitions'])) - return true; - elseif(isset($extensions['tablespaces'])) - return true; - elseif(isset($extensions['clustering'])) - return true; - else - return false; - } - - public function transactionStart($transaction_mode = false, $session_characteristics = false) { - $this->query('BEGIN'); - } - - public function transactionSavepoint($savepoint) { - $this->query("SAVEPOINT \"$savepoint\""); - } - - public function transactionRollback($savepoint = false){ - - if($savepoint) { - $this->query("ROLLBACK TO $savepoint;"); - } else { - $this->query('ROLLBACK;'); - } - } - - public function transactionEnd($chain = false){ - $this->query('COMMIT;'); - } - - public function clearTable($table) { - $this->query("DELETE FROM \"$table\""); - } - - public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, - $parameterised = false - ) { - if($exact && !$caseSensitive) { - $comp = ($negate) ? '!=' : '='; - } else { - if($caseSensitive) { - // GLOB uses asterisks as wildcards. - // Replace them in search string, without replacing escaped percetage signs. - $comp = 'GLOB'; - $value = preg_replace('/^%([^\\\\])/', '*$1', $value); - $value = preg_replace('/([^\\\\])%$/', '$1*', $value); - $value = preg_replace('/([^\\\\])%/', '$1*', $value); - } else { - $comp = 'LIKE'; - } - if($negate) $comp = 'NOT ' . $comp; - } - - if($parameterised) { - return sprintf("%s %s ?", $field, $comp); - } else { - return sprintf("%s %s '%s'", $field, $comp, $value); - } - } - - function formattedDatetimeClause($date, $format) { - preg_match_all('/%(.)/', $format, $matches); - foreach($matches[1] as $match) if(array_search($match, array('Y','m','d','H','i','s','U')) === false) user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING); - - $translate = array( - '/%i/' => '%M', - '/%s/' => '%S', - '/%U/' => '%s', - ); - $format = preg_replace(array_keys($translate), array_values($translate), $format); - - $modifiers = array(); - if($format == '%s' && $date != 'now') $modifiers[] = 'utc'; - if($format != '%s' && $date == 'now') $modifiers[] = 'localtime'; - - if(preg_match('/^now$/i', $date)) { - $date = "'now'"; - } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) { - $date = "'$date'"; - } - - $modifier = empty($modifiers) ? '' : ", '" . implode("', '", $modifiers) . "'"; - return "strftime('$format', $date$modifier)"; - } - - function datetimeIntervalClause($date, $interval) { - $modifiers = array(); - if($date == 'now') $modifiers[] = 'localtime'; - - if(preg_match('/^now$/i', $date)) { - $date = "'now'"; - } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) { - $date = "'$date'"; - } - - $modifier = empty($modifiers) ? '' : ", '" . implode("', '", $modifiers) . "'"; - return "datetime($date$modifier, '$interval')"; - } - - function datetimeDifferenceClause($date1, $date2) { - $modifiers1 = array(); - $modifiers2 = array(); - - if($date1 == 'now') $modifiers1[] = 'localtime'; - if($date2 == 'now') $modifiers2[] = 'localtime'; - - if(preg_match('/^now$/i', $date1)) { - $date1 = "'now'"; - } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) { - $date1 = "'$date1'"; - } - - if(preg_match('/^now$/i', $date2)) { - $date2 = "'now'"; - } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) { - $date2 = "'$date2'"; - } - - $modifier1 = empty($modifiers1) ? '' : ", '" . implode("', '", $modifiers1) . "'"; - $modifier2 = empty($modifiers2) ? '' : ", '" . implode("', '", $modifiers2) . "'"; - - return "strftime('%s', $date1$modifier1) - strftime('%s', $date2$modifier2)"; - } -} + '"UTF-8"', + 'locking_mode' => 'NORMAL' + ); + + + /** + * Extension used to distinguish between sqllite database files and other files. + * Required to handle multiple databases. + * + * @return string + */ + public static function database_extension() + { + return Config::inst()->get('SQLite3Database', 'database_extension'); + } + + /** + * Check if a database name has a valid extension + * + * @param string $name + * @return boolean + */ + public static function is_valid_database_name($name) + { + $extension = self::database_extension(); + if (empty($extension)) { + return true; + } + + return substr_compare($name, $extension, -strlen($extension), strlen($extension)) === 0; + } + + /** + * Connect to a SQLite3 database. + * @param array $parameters An map of parameters, which should include: + * - database: The database to connect to, with the correct file extension (.sqlite) + * - path: the path to the SQLite3 database file + * - key: the encryption key (needs testing) + * - memory: use the faster In-Memory database for unit tests + */ + public function connect($parameters) + { + if (!empty($parameters['memory'])) { + Deprecation::notice( + '1.4.0', + "\$databaseConfig['memory'] is deprecated. Use \$databaseConfig['path'] = ':memory:' instead.", + Deprecation::SCOPE_GLOBAL + ); + unset($parameters['memory']); + $parameters['path'] = ':memory:'; + } + + //We will store these connection parameters for use elsewhere (ie, unit tests) + $this->parameters = $parameters; + $this->schemaManager->flushCache(); + + // Ensure database name is set + if (empty($parameters['database'])) { + $parameters['database'] = 'database' . self::database_extension(); + } + $dbName = $parameters['database']; + if (!self::is_valid_database_name($dbName)) { + // If not using the correct file extension for database files then the + // results of SQLite3SchemaManager::databaseList will be unpredictable + $extension = self::database_extension(); + Deprecation::notice('3.2', "SQLite3Database now expects a database file with extension \"$extension\". Behaviour may be unpredictable otherwise."); + } + + // use the very lightspeed SQLite In-Memory feature for testing + if ($this->getLivesInMemory()) { + $file = ':memory:'; + } else { + // Ensure path is given + if (empty($parameters['path'])) { + $parameters['path'] = ASSETS_PATH . '/.sqlitedb'; + } + + //assumes that the path to dbname will always be provided: + $file = $parameters['path'] . '/' . $dbName; + if (!file_exists($parameters['path'])) { + SQLiteDatabaseConfigurationHelper::create_db_dir($parameters['path']); + SQLiteDatabaseConfigurationHelper::secure_db_dir($parameters['path']); + } + } + + // 'path' and 'database' are merged into the full file path, which + // is the format that connectors such as PDOConnector expect + $parameters['filepath'] = $file; + + // Ensure that driver is available (required by PDO) + if (empty($parameters['driver'])) { + $parameters['driver'] = $this->getDatabaseServer(); + } + + $this->connector->connect($parameters, true); + + foreach (self::$default_pragma as $pragma => $value) { + $this->setPragma($pragma, $value); + } + + if (empty(self::$default_pragma['locking_mode'])) { + self::$default_pragma['locking_mode'] = $this->getPragma('locking_mode'); + } + } + + /** + * Retrieve parameters used to connect to this SQLLite database + * + * @return array + */ + public function getParameters() + { + return $this->parameters; + } + + public function getLivesInMemory() + { + return isset($this->parameters['path']) && $this->parameters['path'] === ':memory:'; + } + + public function supportsCollations() + { + return true; + } + + public function supportsTimezoneOverride() + { + return false; + } + + /** + * Execute PRAGMA commands. + * + * @param string pragma name + * @param string value to set + */ + public function setPragma($pragma, $value) + { + $this->query("PRAGMA $pragma = $value"); + } + + /** + * Gets pragma value. + * + * @param string pragma name + * @return string the pragma value + */ + public function getPragma($pragma) + { + return $this->query("PRAGMA $pragma")->value(); + } + + public function getDatabaseServer() + { + return "sqlite"; + } + + public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR) + { + if (!$this->schemaManager->databaseExists($name)) { + // Check DB creation permisson + if (!$create) { + if ($errorLevel !== false) { + user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel); + } + // Unselect database + $this->connector->unloadDatabase(); + return false; + } + $this->schemaManager->createDatabase($name); + } + + // Reconnect using the existing parameters + $parameters = $this->parameters; + $parameters['database'] = $name; + $this->connect($parameters); + return true; + } + + public function now() + { + return "datetime('now', 'localtime')"; + } + + public function random() + { + return 'random()'; + } + + /** + * The core search engine configuration. + * @todo There is a fulltext search for SQLite making use of virtual tables, the fts3 extension and the + * MATCH operator + * there are a few issues with fts: + * - shared cached lock doesn't allow to create virtual tables on versions prior to 3.6.17 + * - there must not be more than one MATCH operator per statement + * - the fts3 extension needs to be available + * for now we use the MySQL implementation with the MATCH()AGAINST() uglily replaced with LIKE + * + * @param string $keywords Keywords as a space separated string + * @return object DataObjectSet of result pages + */ + public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", + $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false + ) { + $keywords = $this->escapeString(str_replace(array('*', '+', '-', '"', '\''), '', $keywords)); + $htmlEntityKeywords = htmlentities(utf8_decode($keywords)); + + $extraFilters = array('SiteTree' => '', 'File' => ''); + + if ($extraFilter) { + $extraFilters['SiteTree'] = " AND $extraFilter"; + + if ($alternativeFileFilter) { + $extraFilters['File'] = " AND $alternativeFileFilter"; + } else { + $extraFilters['File'] = $extraFilters['SiteTree']; + } + } + + // Always ensure that only pages with ShowInSearch = 1 can be searched + $extraFilters['SiteTree'] .= ' AND ShowInSearch <> 0'; + // File.ShowInSearch was added later, keep the database driver backwards compatible + // by checking for its existence first + $fields = $this->getSchemaManager()->fieldList('File'); + if (array_key_exists('ShowInSearch', $fields)) { + $extraFilters['File'] .= " AND ShowInSearch <> 0"; + } + + $limit = $start . ", " . (int) $pageLength; + + $notMatch = $invertedMatch ? "NOT " : ""; + if ($keywords) { + $match['SiteTree'] = " + (Title LIKE '%$keywords%' OR MenuTitle LIKE '%$keywords%' OR Content LIKE '%$keywords%' OR MetaDescription LIKE '%$keywords%' OR + Title LIKE '%$htmlEntityKeywords%' OR MenuTitle LIKE '%$htmlEntityKeywords%' OR Content LIKE '%$htmlEntityKeywords%' OR MetaDescription LIKE '%$htmlEntityKeywords%') + "; + $match['File'] = "(Name LIKE '%$keywords%' OR Title LIKE '%$keywords%') AND ClassName = 'File'"; + + // We make the relevance search by converting a boolean mode search into a normal one + $relevanceKeywords = $keywords; + $htmlEntityRelevanceKeywords = $htmlEntityKeywords; + $relevance['SiteTree'] = "(Title LIKE '%$relevanceKeywords%' OR MenuTitle LIKE '%$relevanceKeywords%' OR Content LIKE '%$relevanceKeywords%' OR MetaDescription LIKE '%$relevanceKeywords%') + (Title LIKE '%$htmlEntityRelevanceKeywords%' OR MenuTitle LIKE '%$htmlEntityRelevanceKeywords%' OR Content LIKE '%$htmlEntityRelevanceKeywords%' OR MetaDescription LIKE '%$htmlEntityRelevanceKeywords%')"; + $relevance['File'] = "(Name LIKE '%$relevanceKeywords%' OR Title LIKE '%$relevanceKeywords%')"; + } else { + $relevance['SiteTree'] = $relevance['File'] = 1; + $match['SiteTree'] = $match['File'] = "1 = 1"; + } + + // Generate initial queries and base table names + $baseClasses = array('SiteTree' => '', 'File' => ''); + $queries = array(); + foreach ($classesToSearch as $class) { + $queries[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class], "")->dataQuery()->query(); + $fromArr = $queries[$class]->getFrom(); + $baseClasses[$class] = reset($fromArr); + } + + // Make column selection lists + $select = array( + 'SiteTree' => array( + "\"ClassName\"", + "\"ID\"", + "\"ParentID\"", + "\"Title\"", + "\"URLSegment\"", + "\"Content\"", + "\"LastEdited\"", + "\"Created\"", + "NULL AS \"Name\"", + "\"CanViewType\"", + "$relevance[SiteTree] AS Relevance" + ), + 'File' => array( + "\"ClassName\"", + "\"ID\"", + "NULL AS \"ParentID\"", + "\"Title\"", + "NULL AS \"URLSegment\"", + "NULL AS \"Content\"", + "\"LastEdited\"", + "\"Created\"", + "\"Name\"", + "NULL AS \"CanViewType\"", + "$relevance[File] AS Relevance" + ) + ); + + // Process queries + foreach ($classesToSearch as $class) { + // There's no need to do all that joining + $queries[$class]->setFrom($baseClasses[$class]); + + $queries[$class]->setSelect(array()); + foreach ($select[$class] as $clause) { + if (preg_match('/^(.*) +AS +"?([^"]*)"?/i', $clause, $matches)) { + $queries[$class]->selectField($matches[1], $matches[2]); + } else { + $queries[$class]->selectField(str_replace('"', '', $clause)); + } + } + + $queries[$class]->setOrderBy(array()); + } + + // Combine queries + $querySQLs = array(); + $queryParameters = array(); + $totalCount = 0; + foreach ($queries as $query) { + $querySQLs[] = $query->sql($parameters); + $queryParameters = array_merge($queryParameters, $parameters); + $totalCount += $query->unlimitedRowCount(); + } + + $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit"; + // Get records + $records = $this->preparedQuery($fullQuery, $queryParameters); + + foreach ($records as $record) { + $objects[] = new $record['ClassName']($record); + } + + if (isset($objects)) { + $doSet = new ArrayList($objects); + } else { + $doSet = new ArrayList(); + } + $list = new PaginatedList($doSet); + $list->setPageStart($start); + $list->setPageLEngth($pageLength); + $list->setTotalItems($totalCount); + return $list; + } + + /* + * Does this database support transactions? + */ + public function supportsTransactions() + { + return version_compare($this->getVersion(), '3.6', '>='); + } + + public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering')) + { + if (isset($extensions['partitions'])) { + return true; + } elseif (isset($extensions['tablespaces'])) { + return true; + } elseif (isset($extensions['clustering'])) { + return true; + } else { + return false; + } + } + + public function transactionStart($transaction_mode = false, $session_characteristics = false) + { + $this->query('BEGIN'); + } + + public function transactionSavepoint($savepoint) + { + $this->query("SAVEPOINT \"$savepoint\""); + } + + public function transactionRollback($savepoint = false) + { + if ($savepoint) { + $this->query("ROLLBACK TO $savepoint;"); + } else { + $this->query('ROLLBACK;'); + } + } + + public function transactionEnd($chain = false) + { + $this->query('COMMIT;'); + } + + public function clearTable($table) + { + $this->query("DELETE FROM \"$table\""); + } + + public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, + $parameterised = false + ) { + if ($exact && !$caseSensitive) { + $comp = ($negate) ? '!=' : '='; + } else { + if ($caseSensitive) { + // GLOB uses asterisks as wildcards. + // Replace them in search string, without replacing escaped percetage signs. + $comp = 'GLOB'; + $value = preg_replace('/^%([^\\\\])/', '*$1', $value); + $value = preg_replace('/([^\\\\])%$/', '$1*', $value); + $value = preg_replace('/([^\\\\])%/', '$1*', $value); + } else { + $comp = 'LIKE'; + } + if ($negate) { + $comp = 'NOT ' . $comp; + } + } + + if ($parameterised) { + return sprintf("%s %s ?", $field, $comp); + } else { + return sprintf("%s %s '%s'", $field, $comp, $value); + } + } + + public function formattedDatetimeClause($date, $format) + { + preg_match_all('/%(.)/', $format, $matches); + foreach ($matches[1] as $match) { + if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) { + user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING); + } + } + + $translate = array( + '/%i/' => '%M', + '/%s/' => '%S', + '/%U/' => '%s', + ); + $format = preg_replace(array_keys($translate), array_values($translate), $format); + + $modifiers = array(); + if ($format == '%s' && $date != 'now') { + $modifiers[] = 'utc'; + } + if ($format != '%s' && $date == 'now') { + $modifiers[] = 'localtime'; + } + + if (preg_match('/^now$/i', $date)) { + $date = "'now'"; + } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) { + $date = "'$date'"; + } + + $modifier = empty($modifiers) ? '' : ", '" . implode("', '", $modifiers) . "'"; + return "strftime('$format', $date$modifier)"; + } + + public function datetimeIntervalClause($date, $interval) + { + $modifiers = array(); + if ($date == 'now') { + $modifiers[] = 'localtime'; + } + + if (preg_match('/^now$/i', $date)) { + $date = "'now'"; + } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) { + $date = "'$date'"; + } + + $modifier = empty($modifiers) ? '' : ", '" . implode("', '", $modifiers) . "'"; + return "datetime($date$modifier, '$interval')"; + } + + public function datetimeDifferenceClause($date1, $date2) + { + $modifiers1 = array(); + $modifiers2 = array(); + + if ($date1 == 'now') { + $modifiers1[] = 'localtime'; + } + if ($date2 == 'now') { + $modifiers2[] = 'localtime'; + } + + if (preg_match('/^now$/i', $date1)) { + $date1 = "'now'"; + } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) { + $date1 = "'$date1'"; + } + + if (preg_match('/^now$/i', $date2)) { + $date2 = "'now'"; + } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) { + $date2 = "'$date2'"; + } + + $modifier1 = empty($modifiers1) ? '' : ", '" . implode("', '", $modifiers1) . "'"; + $modifier2 = empty($modifiers2) ? '' : ", '" . implode("', '", $modifiers2) . "'"; + + return "strftime('%s', $date1$modifier1) - strftime('%s', $date2$modifier2)"; + } +} diff --git a/code/SQLite3Query.php b/code/SQLite3Query.php index ee6706e..ff6500f 100644 --- a/code/SQLite3Query.php +++ b/code/SQLite3Query.php @@ -5,58 +5,70 @@ * * @package SQLite3 */ -class SQLite3Query extends SS_Query { +class SQLite3Query extends SS_Query +{ - /** - * The SQLite3Connector object that created this result set. - * - * @var SQLite3Connector - */ - protected $database; + /** + * The SQLite3Connector object that created this result set. + * + * @var SQLite3Connector + */ + protected $database; - /** - * The internal sqlite3 handle that points to the result set. - * - * @var SQLite3Result - */ - protected $handle; + /** + * The internal sqlite3 handle that points to the result set. + * + * @var SQLite3Result + */ + protected $handle; - /** - * Hook the result-set given into a Query class, suitable for use by framework. - * @param SQLite3Connector $database The database object that created this query. - * @param SQLite3Result $handle the internal sqlite3 handle that is points to the resultset. - */ - public function __construct(SQLite3Connector $database, SQLite3Result $handle) { - $this->database = $database; - $this->handle = $handle; - } + /** + * Hook the result-set given into a Query class, suitable for use by framework. + * @param SQLite3Connector $database The database object that created this query. + * @param SQLite3Result $handle the internal sqlite3 handle that is points to the resultset. + */ + public function __construct(SQLite3Connector $database, SQLite3Result $handle) + { + $this->database = $database; + $this->handle = $handle; + } - public function __destruct() { - if($this->handle) $this->handle->finalize(); - } + public function __destruct() + { + if ($this->handle) { + $this->handle->finalize(); + } + } - public function seek($row) { - $this->handle->reset(); - $i=0; - while($i < $row && $row = @$this->handle->fetchArray()) $i++; - return true; - } + public function seek($row) + { + $this->handle->reset(); + $i=0; + while ($i < $row && $row = @$this->handle->fetchArray()) { + $i++; + } + return true; + } - /** - * @todo This looks terrible but there is no SQLite3::get_num_rows() implementation - */ - public function numRecords() { - $c=0; - while($this->handle->fetchArray()) $c++; - $this->handle->reset(); - return $c; - } + /** + * @todo This looks terrible but there is no SQLite3::get_num_rows() implementation + */ + public function numRecords() + { + $c=0; + while ($this->handle->fetchArray()) { + $c++; + } + $this->handle->reset(); + return $c; + } - public function nextRecord() { - if($data = $this->handle->fetchArray(SQLITE3_ASSOC)) { - return $data; - } else { - return false; - } - } + public function nextRecord() + { + if ($data = $this->handle->fetchArray(SQLITE3_ASSOC)) { + return $data; + } else { + return false; + } + } } diff --git a/code/SQLite3QueryBuilder.php b/code/SQLite3QueryBuilder.php index 198426a..1f0b4db 100644 --- a/code/SQLite3QueryBuilder.php +++ b/code/SQLite3QueryBuilder.php @@ -5,90 +5,94 @@ * * @package SQLite3 */ -class SQLite3QueryBuilder extends DBQueryBuilder { - - /** - * @param SQLInsert $query - * @param array $parameters - * @return string - */ - protected function buildInsertQuery(SQLInsert $query, array &$parameters) { - // Multi-row insert requires SQLite specific syntax prior to 3.7.11 - // For backwards compatibility reasons include the "union all select" syntax - - $nl = $this->getSeparator(); - $into = $query->getInto(); - - // Column identifiers - $columns = $query->getColumns(); - - // Build all rows - $rowParts = array(); - foreach($query->getRows() as $row) { - // Build all columns in this row - $assignments = $row->getAssignments(); - // Join SET components together, considering parameters - $parts = array(); - foreach($columns as $column) { - // Check if this column has a value for this row - if(isset($assignments[$column])) { - // Assigment is a single item array, expand with a loop here - foreach($assignments[$column] as $assignmentSQL => $assignmentParameters) { - $parts[] = $assignmentSQL; - $parameters = array_merge($parameters, $assignmentParameters); - break; - } - } else { - // This row is missing a value for a column used by another row - $parts[] = '?'; - $parameters[] = null; - } - } - $rowParts[] = implode(', ', $parts); - } - $columnSQL = implode(', ', $columns); - $sql = "INSERT INTO {$into}{$nl}($columnSQL){$nl}SELECT " . implode("{$nl}UNION ALL SELECT ", $rowParts); - - return $sql; - } +class SQLite3QueryBuilder extends DBQueryBuilder +{ + + /** + * @param SQLInsert $query + * @param array $parameters + * @return string + */ + protected function buildInsertQuery(SQLInsert $query, array &$parameters) + { + // Multi-row insert requires SQLite specific syntax prior to 3.7.11 + // For backwards compatibility reasons include the "union all select" syntax - /** - * Return the LIMIT clause ready for inserting into a query. - * - * @param SQLSelect $query The expression object to build from - * @param array $parameters Out parameter for the resulting query parameters - * @return string The finalised limit SQL fragment - */ - public function buildLimitFragment(SQLSelect $query, array &$parameters) { - $nl = $this->getSeparator(); + $nl = $this->getSeparator(); + $into = $query->getInto(); + + // Column identifiers + $columns = $query->getColumns(); + + // Build all rows + $rowParts = array(); + foreach ($query->getRows() as $row) { + // Build all columns in this row + $assignments = $row->getAssignments(); + // Join SET components together, considering parameters + $parts = array(); + foreach ($columns as $column) { + // Check if this column has a value for this row + if (isset($assignments[$column])) { + // Assigment is a single item array, expand with a loop here + foreach ($assignments[$column] as $assignmentSQL => $assignmentParameters) { + $parts[] = $assignmentSQL; + $parameters = array_merge($parameters, $assignmentParameters); + break; + } + } else { + // This row is missing a value for a column used by another row + $parts[] = '?'; + $parameters[] = null; + } + } + $rowParts[] = implode(', ', $parts); + } + $columnSQL = implode(', ', $columns); + $sql = "INSERT INTO {$into}{$nl}($columnSQL){$nl}SELECT " . implode("{$nl}UNION ALL SELECT ", $rowParts); + + return $sql; + } - // Ensure limit is given - $limit = $query->getLimit(); - if(empty($limit)) return ''; + /** + * Return the LIMIT clause ready for inserting into a query. + * + * @param SQLSelect $query The expression object to build from + * @param array $parameters Out parameter for the resulting query parameters + * @return string The finalised limit SQL fragment + */ + public function buildLimitFragment(SQLSelect $query, array &$parameters) + { + $nl = $this->getSeparator(); - // For literal values return this as the limit SQL - if( ! is_array($limit)) { - return "{$nl}LIMIT $limit"; - } + // Ensure limit is given + $limit = $query->getLimit(); + if (empty($limit)) { + return ''; + } - // Assert that the array version provides the 'limit' key - if( ! array_key_exists('limit', $limit) || ($limit['limit'] !== null && ! is_numeric($limit['limit']))) { - throw new InvalidArgumentException( - 'SQLite3QueryBuilder::buildLimitSQL(): Wrong format for $limit: '. var_export($limit, true) - ); - } + // For literal values return this as the limit SQL + if (! is_array($limit)) { + return "{$nl}LIMIT $limit"; + } - $clause = "{$nl}"; - if($limit['limit'] !== null) { - $clause .= "LIMIT {$limit['limit']} "; - } else { - $clause .= "LIMIT -1 "; - } - - if(isset($limit['start']) && is_numeric($limit['start']) && $limit['start'] !== 0) { - $clause .= "OFFSET {$limit['start']}"; - } - return $clause; - } + // Assert that the array version provides the 'limit' key + if (! array_key_exists('limit', $limit) || ($limit['limit'] !== null && ! is_numeric($limit['limit']))) { + throw new InvalidArgumentException( + 'SQLite3QueryBuilder::buildLimitSQL(): Wrong format for $limit: '. var_export($limit, true) + ); + } + $clause = "{$nl}"; + if ($limit['limit'] !== null) { + $clause .= "LIMIT {$limit['limit']} "; + } else { + $clause .= "LIMIT -1 "; + } + + if (isset($limit['start']) && is_numeric($limit['start']) && $limit['start'] !== 0) { + $clause .= "OFFSET {$limit['start']}"; + } + return $clause; + } } diff --git a/code/SQLite3SchemaManager.php b/code/SQLite3SchemaManager.php index 1e5e636..47a566b 100644 --- a/code/SQLite3SchemaManager.php +++ b/code/SQLite3SchemaManager.php @@ -5,611 +5,690 @@ * * @package SQLite3 */ -class SQLite3SchemaManager extends DBSchemaManager { +class SQLite3SchemaManager extends DBSchemaManager +{ - /** - * Instance of the database controller this schema belongs to - * - * @var SQLite3Database - */ - protected $database = null; + /** + * Instance of the database controller this schema belongs to + * + * @var SQLite3Database + */ + protected $database = null; - /** - * Flag indicating whether or not the database has been checked and repaired - * - * @var boolean - */ - protected static $checked_and_repaired = false; + /** + * Flag indicating whether or not the database has been checked and repaired + * + * @var boolean + */ + protected static $checked_and_repaired = false; - /** - * Should schema be vacuumed during checkeAndRepairTable? - * - * @var boolean - */ - public static $vacuum = true; - - public function createDatabase($name) { - // Ensure that any existing database is cleared before connection - $this->dropDatabase($name); - } + /** + * Should schema be vacuumed during checkeAndRepairTable? + * + * @var boolean + */ + public static $vacuum = true; + + public function createDatabase($name) + { + // Ensure that any existing database is cleared before connection + $this->dropDatabase($name); + } - public function dropDatabase($name) { - // No need to delete database files if operating purely within memory - if($this->database->getLivesInMemory()) return; - - // If using file based database ensure any existing file is removed - $parameters = $this->database->getParameters(); - $fullpath = $parameters['path'] . '/' . $name; - if(is_writable($fullpath)) unlink($fullpath); - } - - function databaseList() { - $parameters = $this->database->getParameters(); - - // If in-memory use the current database name only - if($this->database->getLivesInMemory()) { - return array($parameters['database']); - } - - // If using file based database enumerate files in the database directory - $directory = $parameters['path']; - $files = scandir($directory); - - // Filter each file in this directory - $databases = array(); - if($files !== false) foreach($files as $file) { - - // Filter non-files - if(!is_file("$directory/$file")) continue; - - // Filter those with correct extension - if(!SQLite3Database::is_valid_database_name($file)) continue; - - $databases[] = $file; - } - return $databases; - } + public function dropDatabase($name) + { + // No need to delete database files if operating purely within memory + if ($this->database->getLivesInMemory()) { + return; + } + + // If using file based database ensure any existing file is removed + $parameters = $this->database->getParameters(); + $fullpath = $parameters['path'] . '/' . $name; + if (is_writable($fullpath)) { + unlink($fullpath); + } + } + + public function databaseList() + { + $parameters = $this->database->getParameters(); + + // If in-memory use the current database name only + if ($this->database->getLivesInMemory()) { + return array($parameters['database']); + } + + // If using file based database enumerate files in the database directory + $directory = $parameters['path']; + $files = scandir($directory); + + // Filter each file in this directory + $databases = array(); + if ($files !== false) { + foreach ($files as $file) { + + // Filter non-files + if (!is_file("$directory/$file")) { + continue; + } + + // Filter those with correct extension + if (!SQLite3Database::is_valid_database_name($file)) { + continue; + } + + $databases[] = $file; + } + } + return $databases; + } - public function databaseExists($name) { - $databases = $this->databaseList(); - return in_array($name, $databases); - } - - /** - * Empties any cached enum values - */ - public function flushCache() { - $this->enum_map = array(); - } - - function schemaUpdate($callback) { - // Set locking mode - $this->database->setPragma('locking_mode', 'EXCLUSIVE'); - $this->checkAndRepairTable(); - $this->flushCache(); - - // Initiate schema update - $error = null; - try { - parent::schemaUpdate($callback); - } catch(Exception $ex) { - $error = $ex; - } - - // Revert locking mode - $this->database->setPragma('locking_mode', SQLite3Database::$default_pragma['locking_mode']); - - if($error) throw $error; - } + public function databaseExists($name) + { + $databases = $this->databaseList(); + return in_array($name, $databases); + } + + /** + * Empties any cached enum values + */ + public function flushCache() + { + $this->enum_map = array(); + } + + public function schemaUpdate($callback) + { + // Set locking mode + $this->database->setPragma('locking_mode', 'EXCLUSIVE'); + $this->checkAndRepairTable(); + $this->flushCache(); + + // Initiate schema update + $error = null; + try { + parent::schemaUpdate($callback); + } catch (Exception $ex) { + $error = $ex; + } + + // Revert locking mode + $this->database->setPragma('locking_mode', SQLite3Database::$default_pragma['locking_mode']); + + if ($error) { + throw $error; + } + } - /** - * Empty a specific table - * - * @param string $table - */ - public function clearTable($table) { - if($table != 'SQLiteEnums') $this->dbConn->query("DELETE FROM \"$table\""); - } + /** + * Empty a specific table + * + * @param string $table + */ + public function clearTable($table) + { + if ($table != 'SQLiteEnums') { + $this->dbConn->query("DELETE FROM \"$table\""); + } + } - public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) { - if(!isset($fields['ID'])) $fields['ID'] = $this->IdColumn(); + public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) + { + if (!isset($fields['ID'])) { + $fields['ID'] = $this->IdColumn(); + } - $fieldSchemata = array(); - if($fields) foreach($fields as $k => $v) { - $fieldSchemata[] = "\"$k\" $v"; - } - $fieldSchemas = implode(",\n", $fieldSchemata); + $fieldSchemata = array(); + if ($fields) { + foreach ($fields as $k => $v) { + $fieldSchemata[] = "\"$k\" $v"; + } + } + $fieldSchemas = implode(",\n", $fieldSchemata); - // Switch to "CREATE TEMPORARY TABLE" for temporary tables - $temporary = empty($options['temporary']) ? "" : "TEMPORARY"; - $this->query("CREATE $temporary TABLE \"$table\" ( + // Switch to "CREATE TEMPORARY TABLE" for temporary tables + $temporary = empty($options['temporary']) ? "" : "TEMPORARY"; + $this->query("CREATE $temporary TABLE \"$table\" ( $fieldSchemas )"); - if($indexes) { - foreach($indexes as $indexName => $indexDetails) { - $this->createIndex($table, $indexName, $indexDetails); - } - } + if ($indexes) { + foreach ($indexes as $indexName => $indexDetails) { + $this->createIndex($table, $indexName, $indexDetails); + } + } - return $table; - } + return $table; + } - public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null, - $alteredIndexes = null, $alteredOptions = null, $advancedOptions = null - ) { - if($newFields) foreach($newFields as $fieldName => $fieldSpec) { - $this->createField($tableName, $fieldName, $fieldSpec); - } + public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null, + $alteredIndexes = null, $alteredOptions = null, $advancedOptions = null + ) { + if ($newFields) { + foreach ($newFields as $fieldName => $fieldSpec) { + $this->createField($tableName, $fieldName, $fieldSpec); + } + } - if($alteredFields) foreach($alteredFields as $fieldName => $fieldSpec) { - $this->alterField($tableName, $fieldName, $fieldSpec); - } + if ($alteredFields) { + foreach ($alteredFields as $fieldName => $fieldSpec) { + $this->alterField($tableName, $fieldName, $fieldSpec); + } + } - if($newIndexes) foreach($newIndexes as $indexName => $indexSpec) { - $this->createIndex($tableName, $indexName, $indexSpec); - } + if ($newIndexes) { + foreach ($newIndexes as $indexName => $indexSpec) { + $this->createIndex($tableName, $indexName, $indexSpec); + } + } - if($alteredIndexes) foreach($alteredIndexes as $indexName => $indexSpec) { - $this->alterIndex($tableName, $indexName, $indexSpec); - } - } - - public function renameTable($oldTableName, $newTableName) { - $this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\""); - } + if ($alteredIndexes) { + foreach ($alteredIndexes as $indexName => $indexSpec) { + $this->alterIndex($tableName, $indexName, $indexSpec); + } + } + } + + public function renameTable($oldTableName, $newTableName) + { + $this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\""); + } - public function checkAndRepairTable($tableName = null) { - $ok = true; + public function checkAndRepairTable($tableName = null) + { + $ok = true; - if(!SapphireTest::using_temp_db() && !self::$checked_and_repaired) { - $this->alterationMessage("Checking database integrity", "repaired"); - - // Check for any tables with failed integrity - if($messages = $this->query('PRAGMA integrity_check')) { - foreach($messages as $message) if($message['integrity_check'] != 'ok') { - Debug::show($message['integrity_check']); - $ok = false; - } - } - - // If enabled vacuum (clean and rebuild) the database - if(self::$vacuum) { - $this->query('VACUUM', E_USER_NOTICE); - $message = $this->database->getConnector()->getLastError(); - if(preg_match('/authoriz/', $message)) { - $this->alterationMessage("VACUUM | $message", "error"); - } else { - $this->alterationMessage("VACUUMing", "repaired"); - } - } - self::$checked_and_repaired = true; - } - - return $ok; - } + if (!SapphireTest::using_temp_db() && !self::$checked_and_repaired) { + $this->alterationMessage("Checking database integrity", "repaired"); + + // Check for any tables with failed integrity + if ($messages = $this->query('PRAGMA integrity_check')) { + foreach ($messages as $message) { + if ($message['integrity_check'] != 'ok') { + Debug::show($message['integrity_check']); + $ok = false; + } + } + } + + // If enabled vacuum (clean and rebuild) the database + if (self::$vacuum) { + $this->query('VACUUM', E_USER_NOTICE); + $message = $this->database->getConnector()->getLastError(); + if (preg_match('/authoriz/', $message)) { + $this->alterationMessage("VACUUM | $message", "error"); + } else { + $this->alterationMessage("VACUUMing", "repaired"); + } + } + self::$checked_and_repaired = true; + } + + return $ok; + } - public function createField($table, $field, $spec) { - $this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec"); - } + public function createField($table, $field, $spec) + { + $this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec"); + } - /** - * Change the database type of the given field. - * @param string $tableName The name of the tbale the field is in. - * @param string $fieldName The name of the field to change. - * @param string $fieldSpec The new field specification - */ - public function alterField($tableName, $fieldName, $fieldSpec) { - $oldFieldList = $this->fieldList($tableName); - $fieldNameList = '"' . implode('","', array_keys($oldFieldList)) . '"'; + /** + * Change the database type of the given field. + * @param string $tableName The name of the tbale the field is in. + * @param string $fieldName The name of the field to change. + * @param string $fieldSpec The new field specification + */ + public function alterField($tableName, $fieldName, $fieldSpec) + { + $oldFieldList = $this->fieldList($tableName); + $fieldNameList = '"' . implode('","', array_keys($oldFieldList)) . '"'; - if(!empty($_REQUEST['avoidConflict']) && Director::isDev()) { - $fieldSpec = preg_replace('/\snot null\s/i', ' NOT NULL ON CONFLICT REPLACE ', $fieldSpec); - } + if (!empty($_REQUEST['avoidConflict']) && Director::isDev()) { + $fieldSpec = preg_replace('/\snot null\s/i', ' NOT NULL ON CONFLICT REPLACE ', $fieldSpec); + } - // Skip non-existing columns - if(!array_key_exists($fieldName, $oldFieldList)) return; + // Skip non-existing columns + if (!array_key_exists($fieldName, $oldFieldList)) { + return; + } - // Update field spec - $newColsSpec = array(); - foreach($oldFieldList as $name => $oldSpec) { - $newColsSpec[] = "\"$name\" " . ($name == $fieldName ? $fieldSpec : $oldSpec); - } + // Update field spec + $newColsSpec = array(); + foreach ($oldFieldList as $name => $oldSpec) { + $newColsSpec[] = "\"$name\" " . ($name == $fieldName ? $fieldSpec : $oldSpec); + } - $queries = array( - "BEGIN TRANSACTION", - "CREATE TABLE \"{$tableName}_alterfield_{$fieldName}\"(" . implode(',', $newColsSpec) . ")", - "INSERT INTO \"{$tableName}_alterfield_{$fieldName}\" SELECT {$fieldNameList} FROM \"$tableName\"", - "DROP TABLE \"$tableName\"", - "ALTER TABLE \"{$tableName}_alterfield_{$fieldName}\" RENAME TO \"$tableName\"", - "COMMIT" - ); + $queries = array( + "BEGIN TRANSACTION", + "CREATE TABLE \"{$tableName}_alterfield_{$fieldName}\"(" . implode(',', $newColsSpec) . ")", + "INSERT INTO \"{$tableName}_alterfield_{$fieldName}\" SELECT {$fieldNameList} FROM \"$tableName\"", + "DROP TABLE \"$tableName\"", + "ALTER TABLE \"{$tableName}_alterfield_{$fieldName}\" RENAME TO \"$tableName\"", + "COMMIT" + ); - // Remember original indexes - $indexList = $this->indexList($tableName); - - // Then alter the table column - foreach($queries as $query) $this->query($query.';'); + // Remember original indexes + $indexList = $this->indexList($tableName); + + // Then alter the table column + foreach ($queries as $query) { + $this->query($query.';'); + } - // Recreate the indexes - foreach($indexList as $indexName => $indexSpec) { - $this->createIndex($tableName, $indexName, $indexSpec); - } - } + // Recreate the indexes + foreach ($indexList as $indexName => $indexSpec) { + $this->createIndex($tableName, $indexName, $indexSpec); + } + } - public function renameField($tableName, $oldName, $newName) { - $oldFieldList = $this->fieldList($tableName); + public function renameField($tableName, $oldName, $newName) + { + $oldFieldList = $this->fieldList($tableName); - // Skip non-existing columns - if(!array_key_exists($oldName, $oldFieldList)) return; - - // Determine column mappings - $oldCols = array(); - $newColsSpec = array(); - foreach($oldFieldList as $name => $spec) { - $oldCols[] = "\"$name\"" . (($name == $oldName) ? " AS $newName" : ''); - $newColsSpec[] = "\"" . (($name == $oldName) ? $newName : $name) . "\" $spec"; - } + // Skip non-existing columns + if (!array_key_exists($oldName, $oldFieldList)) { + return; + } + + // Determine column mappings + $oldCols = array(); + $newColsSpec = array(); + foreach ($oldFieldList as $name => $spec) { + $oldCols[] = "\"$name\"" . (($name == $oldName) ? " AS $newName" : ''); + $newColsSpec[] = "\"" . (($name == $oldName) ? $newName : $name) . "\" $spec"; + } - // SQLite doesn't support direct renames through ALTER TABLE - $queries = array( - "BEGIN TRANSACTION", - "CREATE TABLE \"{$tableName}_renamefield_{$oldName}\" (" . implode(',', $newColsSpec) . ")", - "INSERT INTO \"{$tableName}_renamefield_{$oldName}\" SELECT " . implode(',', $oldCols) . " FROM \"$tableName\"", - "DROP TABLE \"$tableName\"", - "ALTER TABLE \"{$tableName}_renamefield_{$oldName}\" RENAME TO \"$tableName\"", - "COMMIT" - ); + // SQLite doesn't support direct renames through ALTER TABLE + $queries = array( + "BEGIN TRANSACTION", + "CREATE TABLE \"{$tableName}_renamefield_{$oldName}\" (" . implode(',', $newColsSpec) . ")", + "INSERT INTO \"{$tableName}_renamefield_{$oldName}\" SELECT " . implode(',', $oldCols) . " FROM \"$tableName\"", + "DROP TABLE \"$tableName\"", + "ALTER TABLE \"{$tableName}_renamefield_{$oldName}\" RENAME TO \"$tableName\"", + "COMMIT" + ); - // Remember original indexes - $oldIndexList = $this->indexList($tableName); + // Remember original indexes + $oldIndexList = $this->indexList($tableName); - // Then alter the table column - foreach($queries as $query) $this->query($query.';'); + // Then alter the table column + foreach ($queries as $query) { + $this->query($query.';'); + } - // Recreate the indexes - foreach($oldIndexList as $indexName => $indexSpec) { - // Rename columns to new columns - $indexSpec['value'] = preg_replace("/\"$oldName\"/i", "\"$newName\"", $indexSpec['value']); - $this->createIndex($tableName, $indexName, $indexSpec); - } - } + // Recreate the indexes + foreach ($oldIndexList as $indexName => $indexSpec) { + // Rename columns to new columns + $indexSpec['value'] = preg_replace("/\"$oldName\"/i", "\"$newName\"", $indexSpec['value']); + $this->createIndex($tableName, $indexName, $indexSpec); + } + } - public function fieldList($table) { - $sqlCreate = $this->preparedQuery( - 'SELECT sql FROM sqlite_master WHERE type = ? AND name = ?', - array('table', $table) - )->record(); - - $fieldList = array(); - if($sqlCreate && $sqlCreate['sql']) { - preg_match('/^[\s]*CREATE[\s]+TABLE[\s]+[\'"]?[a-zA-Z0-9_\\\]+[\'"]?[\s]*\((.+)\)[\s]*$/ims', - $sqlCreate['sql'], $matches - ); - $fields = isset($matches[1]) - ? preg_split('/,(?=(?:[^\'"]*$)|(?:[^\'"]*[\'"][^\'"]*[\'"][^\'"]*)*$)/x', $matches[1]) - : array(); - foreach($fields as $field) { - $details = preg_split('/\s/', trim($field)); - $name = array_shift($details); - $name = str_replace('"', '', trim($name)); - $fieldList[$name] = implode(' ', $details); - } - } - return $fieldList; - } + public function fieldList($table) + { + $sqlCreate = $this->preparedQuery( + 'SELECT sql FROM sqlite_master WHERE type = ? AND name = ?', + array('table', $table) + )->record(); + + $fieldList = array(); + if ($sqlCreate && $sqlCreate['sql']) { + preg_match('/^[\s]*CREATE[\s]+TABLE[\s]+[\'"]?[a-zA-Z0-9_\\\]+[\'"]?[\s]*\((.+)\)[\s]*$/ims', + $sqlCreate['sql'], $matches + ); + $fields = isset($matches[1]) + ? preg_split('/,(?=(?:[^\'"]*$)|(?:[^\'"]*[\'"][^\'"]*[\'"][^\'"]*)*$)/x', $matches[1]) + : array(); + foreach ($fields as $field) { + $details = preg_split('/\s/', trim($field)); + $name = array_shift($details); + $name = str_replace('"', '', trim($name)); + $fieldList[$name] = implode(' ', $details); + } + } + return $fieldList; + } - /** - * Create an index on a table. - * - * @param string $tableName The name of the table. - * @param string $indexName The name of the index. - * @param array $indexSpec The specification of the index, see Database::requireIndex() for more details. - */ - public function createIndex($tableName, $indexName, $indexSpec) { - $parsedSpec = $this->parseIndexSpec($indexName, $indexSpec); - $sqliteName = $this->buildSQLiteIndexName($tableName, $indexName); - $columns = $parsedSpec['value']; - $unique = ($parsedSpec['type'] == 'unique') ? 'UNIQUE' : ''; - $this->query("CREATE $unique INDEX IF NOT EXISTS \"$sqliteName\" ON \"$tableName\" ($columns)"); - } + /** + * Create an index on a table. + * + * @param string $tableName The name of the table. + * @param string $indexName The name of the index. + * @param array $indexSpec The specification of the index, see Database::requireIndex() for more details. + */ + public function createIndex($tableName, $indexName, $indexSpec) + { + $parsedSpec = $this->parseIndexSpec($indexName, $indexSpec); + $sqliteName = $this->buildSQLiteIndexName($tableName, $indexName); + $columns = $parsedSpec['value']; + $unique = ($parsedSpec['type'] == 'unique') ? 'UNIQUE' : ''; + $this->query("CREATE $unique INDEX IF NOT EXISTS \"$sqliteName\" ON \"$tableName\" ($columns)"); + } - public function alterIndex($tableName, $indexName, $indexSpec) { - // Drop existing index - $sqliteName = $this->buildSQLiteIndexName($tableName, $indexName); - $this->query("DROP INDEX IF EXISTS \"$sqliteName\""); - - // Create the index - $this->createIndex($tableName, $indexName, $indexSpec); - } - - /** - * Builds the internal SQLLite index name given the silverstripe table and index name. - * - * The name is built using the table and index name in order to prevent name collisions - * between indexes of the same name across multiple tables - * - * @param string $tableName - * @param string $indexName - * @return string The SQLite3 name of the index - */ - protected function buildSQLiteIndexName($tableName, $indexName) { - return "{$tableName}_{$indexName}"; - } - - protected function parseIndexSpec($name, $spec) { - $spec = parent::parseIndexSpec($name, $spec); - - // Only allow index / unique index types - if(!in_array($spec['type'], array('index', 'unique'))) { - $spec['type'] = 'index'; - } - - return $spec; - } - - public function indexKey($table, $index, $spec) { - return $this->buildSQLiteIndexName($table, $index); - } + public function alterIndex($tableName, $indexName, $indexSpec) + { + // Drop existing index + $sqliteName = $this->buildSQLiteIndexName($tableName, $indexName); + $this->query("DROP INDEX IF EXISTS \"$sqliteName\""); + + // Create the index + $this->createIndex($tableName, $indexName, $indexSpec); + } + + /** + * Builds the internal SQLLite index name given the silverstripe table and index name. + * + * The name is built using the table and index name in order to prevent name collisions + * between indexes of the same name across multiple tables + * + * @param string $tableName + * @param string $indexName + * @return string The SQLite3 name of the index + */ + protected function buildSQLiteIndexName($tableName, $indexName) + { + return "{$tableName}_{$indexName}"; + } + + protected function parseIndexSpec($name, $spec) + { + $spec = parent::parseIndexSpec($name, $spec); + + // Only allow index / unique index types + if (!in_array($spec['type'], array('index', 'unique'))) { + $spec['type'] = 'index'; + } + + return $spec; + } + + public function indexKey($table, $index, $spec) + { + return $this->buildSQLiteIndexName($table, $index); + } - public function indexList($table) { - $indexList = array(); - - // Enumerate each index and related fields - foreach($this->query("PRAGMA index_list(\"$table\")") as $index) { - - // The SQLite internal index name, not the actual Silverstripe name - $indexName = $index["name"]; - $indexType = $index['unique'] ? 'unique' : 'index'; - - // Determine a clean list of column names within this index - $list = array(); - foreach($this->query("PRAGMA index_info(\"$indexName\")") as $details) { - $list[] = preg_replace('/^"?(.*)"?$/', '$1', $details['name']); - } - - // Safely encode this spec - $indexList[$indexName] = $this->parseIndexSpec($indexName, array( - 'name' => $indexName, - 'value' => $this->implodeColumnList($list), - 'type' => $indexType - )); - } + public function indexList($table) + { + $indexList = array(); + + // Enumerate each index and related fields + foreach ($this->query("PRAGMA index_list(\"$table\")") as $index) { + + // The SQLite internal index name, not the actual Silverstripe name + $indexName = $index["name"]; + $indexType = $index['unique'] ? 'unique' : 'index'; + + // Determine a clean list of column names within this index + $list = array(); + foreach ($this->query("PRAGMA index_info(\"$indexName\")") as $details) { + $list[] = preg_replace('/^"?(.*)"?$/', '$1', $details['name']); + } + + // Safely encode this spec + $indexList[$indexName] = $this->parseIndexSpec($indexName, array( + 'name' => $indexName, + 'value' => $this->implodeColumnList($list), + 'type' => $indexType + )); + } - return $indexList; - } + return $indexList; + } - public function tableList() { - $tables = array(); - $result = $this->preparedQuery('SELECT name FROM sqlite_master WHERE type = ?', array('table')); - foreach($result as $record) { - $table = reset($record); - $tables[strtolower($table)] = $table; - } - return $tables; - } - - /** - * Return a boolean type-formatted string - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function boolean($values) { - $default = empty($values['default']) ? 0 : (int)$values['default']; - return "BOOL NOT NULL DEFAULT $default"; - } + public function tableList() + { + $tables = array(); + $result = $this->preparedQuery('SELECT name FROM sqlite_master WHERE type = ?', array('table')); + foreach ($result as $record) { + $table = reset($record); + $tables[strtolower($table)] = $table; + } + return $tables; + } + + /** + * Return a boolean type-formatted string + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function boolean($values) + { + $default = empty($values['default']) ? 0 : (int)$values['default']; + return "BOOL NOT NULL DEFAULT $default"; + } - /** - * Return a date type-formatted string - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function date($values){ - return "TEXT"; - } + /** + * Return a date type-formatted string + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function date($values) + { + return "TEXT"; + } - /** - * Return a decimal type-formatted string - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function decimal($values, $asDbValue = false) { - $default = isset($values['default']) && is_numeric($values['default']) ? $values['default'] : 0; - return "NUMERIC NOT NULL DEFAULT $default"; - } + /** + * Return a decimal type-formatted string + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function decimal($values, $asDbValue = false) + { + $default = isset($values['default']) && is_numeric($values['default']) ? $values['default'] : 0; + return "NUMERIC NOT NULL DEFAULT $default"; + } - /** - * Cached list of enum values indexed by table.column - * - * @var array - */ - protected $enum_map = array(); - - /** - * Return a enum type-formatted string - * - * enums are not supported. as a workaround to store allowed values we creates an additional table - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function enum($values){ - $tablefield = $values['table'] . '.' . $values['name']; - $enumValues = implode(',', $values['enums']); - - // Ensure the cache table exists - if(empty($this->enum_map)) { - $this->query("CREATE TABLE IF NOT EXISTS \"SQLiteEnums\" (\"TableColumn\" TEXT PRIMARY KEY, \"EnumList\" TEXT)"); - } - - // Ensure the table row exists - if(empty($this->enum_map[$tablefield]) || $this->enum_map[$tablefield] != $enumValues) { - $this->preparedQuery( - "REPLACE INTO SQLiteEnums (TableColumn, EnumList) VALUES (?, ?)", - array($tablefield, $enumValues) - ); - $this->enum_map[$tablefield] = $enumValues; - } - - // Set default - if(!empty($values['default'])) { - $default = str_replace(array('"',"'","\\","\0"), "", $values['default']); - return "TEXT DEFAULT '$default'"; - } else { - return 'TEXT'; - } - } - - /** - * Return a set type-formatted string - * This type doesn't exist in SQLite either - * - * @see SQLite3SchemaManager::enum() - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function set($values) { - return $this->enum($values); - } + /** + * Cached list of enum values indexed by table.column + * + * @var array + */ + protected $enum_map = array(); + + /** + * Return a enum type-formatted string + * + * enums are not supported. as a workaround to store allowed values we creates an additional table + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function enum($values) + { + $tablefield = $values['table'] . '.' . $values['name']; + $enumValues = implode(',', $values['enums']); + + // Ensure the cache table exists + if (empty($this->enum_map)) { + $this->query("CREATE TABLE IF NOT EXISTS \"SQLiteEnums\" (\"TableColumn\" TEXT PRIMARY KEY, \"EnumList\" TEXT)"); + } + + // Ensure the table row exists + if (empty($this->enum_map[$tablefield]) || $this->enum_map[$tablefield] != $enumValues) { + $this->preparedQuery( + "REPLACE INTO SQLiteEnums (TableColumn, EnumList) VALUES (?, ?)", + array($tablefield, $enumValues) + ); + $this->enum_map[$tablefield] = $enumValues; + } + + // Set default + if (!empty($values['default'])) { + $default = str_replace(array('"', "'", "\\", "\0"), "", $values['default']); + return "TEXT DEFAULT '$default'"; + } else { + return 'TEXT'; + } + } + + /** + * Return a set type-formatted string + * This type doesn't exist in SQLite either + * + * @see SQLite3SchemaManager::enum() + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function set($values) + { + return $this->enum($values); + } - /** - * Return a float type-formatted string - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function float($values, $asDbValue = false){ - return "REAL"; - } + /** + * Return a float type-formatted string + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function float($values, $asDbValue = false) + { + return "REAL"; + } - /** - * Return a Double type-formatted string - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function double($values, $asDbValue = false){ - return "REAL"; - } + /** + * Return a Double type-formatted string + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function double($values, $asDbValue = false) + { + return "REAL"; + } - /** - * Return a int type-formatted string - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function int($values, $asDbValue = false){ - return "INTEGER({$values['precision']}) " . strtoupper($values['null']) . " DEFAULT " . (int)$values['default']; - } + /** + * Return a int type-formatted string + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function int($values, $asDbValue = false) + { + return "INTEGER({$values['precision']}) " . strtoupper($values['null']) . " DEFAULT " . (int)$values['default']; + } - /** - * Return a bigint type-formatted string - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function bigint($values, $asDbValue = false){ - return $this->int($values, $asDbValue); - } + /** + * Return a bigint type-formatted string + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function bigint($values, $asDbValue = false) + { + return $this->int($values, $asDbValue); + } - /** - * Return a datetime type-formatted string - * For SQLite3, we simply return the word 'TEXT', no other parameters are necessary - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function ss_datetime($values, $asDbValue = false) { - return "DATETIME"; - } + /** + * Return a datetime type-formatted string + * For SQLite3, we simply return the word 'TEXT', no other parameters are necessary + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function ss_datetime($values, $asDbValue = false) + { + return "DATETIME"; + } - /** - * Return a text type-formatted string - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function text($values, $asDbValue = false) { - return 'TEXT'; - } + /** + * Return a text type-formatted string + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function text($values, $asDbValue = false) + { + return 'TEXT'; + } - /** - * Return a time type-formatted string - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function time($values) { - return "TEXT"; - } + /** + * Return a time type-formatted string + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function time($values) + { + return "TEXT"; + } - /** - * Return a varchar type-formatted string - * - * @params array $values Contains a tokenised list of info about this data type - * @return string - */ - public function varchar($values, $asDbValue = false) { - return "VARCHAR({$values['precision']}) COLLATE NOCASE"; - } + /** + * Return a varchar type-formatted string + * + * @params array $values Contains a tokenised list of info about this data type + * @return string + */ + public function varchar($values, $asDbValue = false) + { + return "VARCHAR({$values['precision']}) COLLATE NOCASE"; + } - /* - * Return a 4 digit numeric type. MySQL has a proprietary 'Year' type. - * For SQLite3 we use TEXT - */ - public function year($values, $asDbValue = false){ - return "TEXT"; - } + /* + * Return a 4 digit numeric type. MySQL has a proprietary 'Year' type. + * For SQLite3 we use TEXT + */ + public function year($values, $asDbValue = false) + { + return "TEXT"; + } - public function IdColumn($asDbValue = false, $hasAutoIncPK = true){ - return 'INTEGER PRIMARY KEY AUTOINCREMENT'; - } + public function IdColumn($asDbValue = false, $hasAutoIncPK = true) + { + return 'INTEGER PRIMARY KEY AUTOINCREMENT'; + } - function hasTable($tableName) { - return (bool)$this->preparedQuery( - 'SELECT name FROM sqlite_master WHERE type = ? AND name = ?', - array('table', $tableName) - )->first(); - } + public function hasTable($tableName) + { + return (bool)$this->preparedQuery( + 'SELECT name FROM sqlite_master WHERE type = ? AND name = ?', + array('table', $tableName) + )->first(); + } - /** - * Return enum values for the given field - * - * @return array - */ - public function enumValuesForField($tableName, $fieldName) { - $tablefield = "$tableName.$fieldName"; - - // Check already cached values for this field - if(!empty($this->enum_map[$tablefield])) { - return explode(',', $this->enum_map[$tablefield]); - } - - // Retrieve and cache these details from the database - $classnameinfo = $this->preparedQuery( - "SELECT EnumList FROM SQLiteEnums WHERE TableColumn = ?", - array($tablefield) - )->first(); - if($classnameinfo) { - $valueList = $classnameinfo['EnumList']; - $this->enum_map[$tablefield] = $valueList; - return explode(',', $valueList); - } - - // Fallback to empty list - return array(); - } - - function dbDataType($type){ - $values = array( - 'unsigned integer' => 'INT' - ); - - if(isset($values[$type])) return $values[$type]; - else return ''; - } + /** + * Return enum values for the given field + * + * @return array + */ + public function enumValuesForField($tableName, $fieldName) + { + $tablefield = "$tableName.$fieldName"; + + // Check already cached values for this field + if (!empty($this->enum_map[$tablefield])) { + return explode(',', $this->enum_map[$tablefield]); + } + + // Retrieve and cache these details from the database + $classnameinfo = $this->preparedQuery( + "SELECT EnumList FROM SQLiteEnums WHERE TableColumn = ?", + array($tablefield) + )->first(); + if ($classnameinfo) { + $valueList = $classnameinfo['EnumList']; + $this->enum_map[$tablefield] = $valueList; + return explode(',', $valueList); + } + + // Fallback to empty list + return array(); + } + + public function dbDataType($type) + { + $values = array( + 'unsigned integer' => 'INT' + ); + + if (isset($values[$type])) { + return $values[$type]; + } else { + return ''; + } + } } diff --git a/code/SQLiteDatabaseConfigurationHelper.php b/code/SQLiteDatabaseConfigurationHelper.php index c415d43..ae96f1e 100644 --- a/code/SQLiteDatabaseConfigurationHelper.php +++ b/code/SQLiteDatabaseConfigurationHelper.php @@ -8,200 +8,219 @@ * * @package SQLite3 */ -class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper { - - /** - * Create a connection of the appropriate type - * - * @param array $databaseConfig - * @param string $error Error message passed by value - * @return mixed|null Either the connection object, or null if error - */ - protected function createConnection($databaseConfig, &$error) { - $error = null; - try { - if(!file_exists($databaseConfig['path'])) { - self::create_db_dir($databaseConfig['path']); - self::secure_db_dir($databaseConfig['path']); - } - $file = $databaseConfig['path'] . '/' . $databaseConfig['database']; - $conn = null; - - switch($databaseConfig['type']) { - case 'SQLite3Database': - if(empty($databaseConfig['key'])) { - $conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE); - } else { - $conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $databaseConfig['key']); - } - break; - case 'SQLite3PDODatabase': - // May throw a PDOException if fails - $conn = @new PDO("sqlite:$file"); - break; - default: - $error = 'Invalid connection type'; - return null; - } - - if($conn) { - return $conn; - } else { - $error = 'Unknown connection error'; - return null; - } - } catch(Exception $ex) { - $error = $ex->getMessage(); - return null; - } - } - - public function requireDatabaseFunctions($databaseConfig) { - $data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']); - return !empty($data['supported']); - } +class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper +{ + + /** + * Create a connection of the appropriate type + * + * @param array $databaseConfig + * @param string $error Error message passed by value + * @return mixed|null Either the connection object, or null if error + */ + protected function createConnection($databaseConfig, &$error) + { + $error = null; + try { + if (!file_exists($databaseConfig['path'])) { + self::create_db_dir($databaseConfig['path']); + self::secure_db_dir($databaseConfig['path']); + } + $file = $databaseConfig['path'] . '/' . $databaseConfig['database']; + $conn = null; + + switch ($databaseConfig['type']) { + case 'SQLite3Database': + if (empty($databaseConfig['key'])) { + $conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE); + } else { + $conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $databaseConfig['key']); + } + break; + case 'SQLite3PDODatabase': + // May throw a PDOException if fails + $conn = @new PDO("sqlite:$file"); + break; + default: + $error = 'Invalid connection type'; + return null; + } + + if ($conn) { + return $conn; + } else { + $error = 'Unknown connection error'; + return null; + } + } catch (Exception $ex) { + $error = $ex->getMessage(); + return null; + } + } + + public function requireDatabaseFunctions($databaseConfig) + { + $data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']); + return !empty($data['supported']); + } - public function requireDatabaseServer($databaseConfig) { - $path = $databaseConfig['path']; - $error = ''; - $success = false; + public function requireDatabaseServer($databaseConfig) + { + $path = $databaseConfig['path']; + $error = ''; + $success = false; - if(!$path) { - $error = 'No database path provided'; - } elseif(is_writable($path) || (!file_exists($path) && is_writable(dirname($path)))) { - // check if folder is writeable - $success = true; - } else { - $error = "Permission denied"; - } + if (!$path) { + $error = 'No database path provided'; + } elseif (is_writable($path) || (!file_exists($path) && is_writable(dirname($path)))) { + // check if folder is writeable + $success = true; + } else { + $error = "Permission denied"; + } - return array( - 'success' => $success, - 'error' => $error, - 'path' => $path - ); - } + return array( + 'success' => $success, + 'error' => $error, + 'path' => $path + ); + } - /** - * Ensure a database connection is possible using credentials provided. - * - * @todo Validate path - * - * @param array $databaseConfig Associative array of db configuration, e.g. "type", "path" etc - * @return array Result - e.g. array('success' => true, 'error' => 'details of error') - */ - public function requireDatabaseConnection($databaseConfig) { - // Do additional validation around file paths - if(empty($databaseConfig['path'])) return array( - 'success' => false, - 'error' => "Missing directory path" - ); - if(empty($databaseConfig['database'])) return array( - 'success' => false, - 'error' => "Missing database filename" - ); - - // Create and secure db directory - $path = $databaseConfig['path']; - $dirCreated = self::create_db_dir($path); - if(!$dirCreated) return array( - 'success' => false, - 'error' => sprintf('Cannot create path: "%s"', $path) - ); - $dirSecured = self::secure_db_dir($path); - if(!$dirSecured) return array( - 'success' => false, - 'error' => sprintf('Cannot secure path through .htaccess: "%s"', $path) - ); + /** + * Ensure a database connection is possible using credentials provided. + * + * @todo Validate path + * + * @param array $databaseConfig Associative array of db configuration, e.g. "type", "path" etc + * @return array Result - e.g. array('success' => true, 'error' => 'details of error') + */ + public function requireDatabaseConnection($databaseConfig) + { + // Do additional validation around file paths + if (empty($databaseConfig['path'])) { + return array( + 'success' => false, + 'error' => "Missing directory path" + ); + } + if (empty($databaseConfig['database'])) { + return array( + 'success' => false, + 'error' => "Missing database filename" + ); + } + + // Create and secure db directory + $path = $databaseConfig['path']; + $dirCreated = self::create_db_dir($path); + if (!$dirCreated) { + return array( + 'success' => false, + 'error' => sprintf('Cannot create path: "%s"', $path) + ); + } + $dirSecured = self::secure_db_dir($path); + if (!$dirSecured) { + return array( + 'success' => false, + 'error' => sprintf('Cannot secure path through .htaccess: "%s"', $path) + ); + } - $conn = $this->createConnection($databaseConfig, $error); - $success = !empty($conn); - - return array( - 'success' => $success, - 'connection' => $conn, - 'error' => $error - ); - } + $conn = $this->createConnection($databaseConfig, $error); + $success = !empty($conn); + + return array( + 'success' => $success, + 'connection' => $conn, + 'error' => $error + ); + } - public function getDatabaseVersion($databaseConfig) { - $version = 0; - - switch($databaseConfig['type']) { - case 'SQLite3Database': - $info = SQLite3::version(); - $version = trim($info['versionString']); - break; - case 'SQLite3PDODatabase': - // Fallback to using sqlite_version() query - $conn = $this->createConnection($databaseConfig, $error); - if($conn) { - $version = $conn->getAttribute(PDO::ATTR_SERVER_VERSION); - } - break; - } + public function getDatabaseVersion($databaseConfig) + { + $version = 0; + + switch ($databaseConfig['type']) { + case 'SQLite3Database': + $info = SQLite3::version(); + $version = trim($info['versionString']); + break; + case 'SQLite3PDODatabase': + // Fallback to using sqlite_version() query + $conn = $this->createConnection($databaseConfig, $error); + if ($conn) { + $version = $conn->getAttribute(PDO::ATTR_SERVER_VERSION); + } + break; + } - return $version; - } + return $version; + } - public function requireDatabaseVersion($databaseConfig) { - $success = false; - $error = ''; - $version = $this->getDatabaseVersion($databaseConfig); + public function requireDatabaseVersion($databaseConfig) + { + $success = false; + $error = ''; + $version = $this->getDatabaseVersion($databaseConfig); - if($version) { - $success = version_compare($version, '3.3', '>='); - if(!$success) { - $error = "Your SQLite3 library version is $version. It's recommended you use at least 3.3."; - } - } + if ($version) { + $success = version_compare($version, '3.3', '>='); + if (!$success) { + $error = "Your SQLite3 library version is $version. It's recommended you use at least 3.3."; + } + } - return array( - 'success' => $success, - 'error' => $error - ); - } + return array( + 'success' => $success, + 'error' => $error + ); + } - public function requireDatabaseOrCreatePermissions($databaseConfig) { - $conn = $this->createConnection($databaseConfig, $error); - $success = $alreadyExists = !empty($conn); - return array( - 'success' => $success, - 'alreadyExists' => $alreadyExists, - ); - } - - /** - * Creates the provided directory and prepares it for - * storing SQLlite. Use {@link secure_db_dir()} to - * secure it against unauthorized access. - * - * @param String $path Absolute path, usually with a hidden folder. - * @return boolean - */ - public static function create_db_dir($path) { - return file_exists($path) || mkdir($path); - } - - /** - * Secure the provided directory via web-access - * by placing a .htaccess file in it. - * This is just required if the database directory - * is placed within a publically accessible webroot (the - * default path is in a hidden folder within assets/). - * - * @param String $path Absolute path, containing a SQLite datatbase - * @return boolean - */ - public static function secure_db_dir($path) { - return (is_writeable($path)) ? file_put_contents($path . '/.htaccess', 'deny from all') : false; - } - - public function requireDatabaseAlterPermissions($databaseConfig) { - // no concept of table-specific permissions; If you can connect you can alter schema - return array( - 'success' => true, - 'applies' => false - ); - } + public function requireDatabaseOrCreatePermissions($databaseConfig) + { + $conn = $this->createConnection($databaseConfig, $error); + $success = $alreadyExists = !empty($conn); + return array( + 'success' => $success, + 'alreadyExists' => $alreadyExists, + ); + } + + /** + * Creates the provided directory and prepares it for + * storing SQLlite. Use {@link secure_db_dir()} to + * secure it against unauthorized access. + * + * @param String $path Absolute path, usually with a hidden folder. + * @return boolean + */ + public static function create_db_dir($path) + { + return file_exists($path) || mkdir($path); + } + + /** + * Secure the provided directory via web-access + * by placing a .htaccess file in it. + * This is just required if the database directory + * is placed within a publically accessible webroot (the + * default path is in a hidden folder within assets/). + * + * @param String $path Absolute path, containing a SQLite datatbase + * @return boolean + */ + public static function secure_db_dir($path) + { + return (is_writeable($path)) ? file_put_contents($path . '/.htaccess', 'deny from all') : false; + } + + public function requireDatabaseAlterPermissions($databaseConfig) + { + // no concept of table-specific permissions; If you can connect you can alter schema + return array( + 'success' => true, + 'applies' => false + ); + } }