diff --git a/include/db/db.inc.php b/include/db/db.inc.php index b77245bb..14eed78f 100644 --- a/include/db/db.inc.php +++ b/include/db/db.inc.php @@ -1,186 +1,167 @@ connect(); } -if (@include(S9Y_INCLUDE_PATH . "include/db/{$serendipity['dbType']}.inc.php")) { - @define('S9Y_DB_INCLUDED', TRUE); +function serendipity_db_reconnect() +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->reconnect(); } -#include_once(S9Y_INCLUDE_PATH . "include/db/generic.inc.php"); -#define('S9Y_DB_INCLUDED', TRUE); -/** - * Perform a query to update the data of a certain table row - * - * You can pass the tablename and an array of keys to select the row, - * and an array of values to UPDATE in the DB table. - * - * @access public - * @param string Name of the DB table - * @param array Input array that controls the "WHERE" condition part. Pass it an associative array like array('key1' => 'value1', 'key2' => 'value2') to get a statement like "WHERE key1 = value1 AND key2 = value2". Escaping is done automatically in this function. - * @param array Input array that controls the "SET" condition part. Pass it an associative array like array('key1' => 'value1', 'key2' => 'value2') to get a statement like "SET key1 = value1, key2 = value2". Escaping is done automatically in this function. - * @param string What do do with the SQL query (execute, display) - * @return array Returns the result of the SQL query - */ function serendipity_db_update($table, $keys, $values, $action = 'execute') { global $serendipity; - - $set = ''; - - foreach ($values as $k => $v) { - if (strlen($set)) - $set .= ', '; - $set .= $k . '=\'' . serendipity_db_escape_string($v) . '\''; - } - - $where = ''; - foreach ($keys as $k => $v) { - if (strlen($where)) - $where .= ' AND '; - $where .= $k . '=\'' . serendipity_db_escape_string($v) . '\''; - } - - if (strlen($where)) { - $where = " WHERE $where"; - } - - $q = "UPDATE {$serendipity['dbPrefix']}$table SET $set $where"; - if ($action == 'execute') { - return serendipity_db_query($q); - } else { - return $q; - } + $db = DbFactory::createFromConfig($serendipity); + return $db->update($table, $keys, $values, $action); } -/** - * Perform a query to insert an associative array into a specific SQL table - * - * You can pass a tablename and an array of input data to insert into an array. - * - * @access public - * @param string Name of the SQL table - * @param array Associative array of keys/values to insert into the table. Escaping is done automatically. - * @param string What do do with the SQL query (execute, display) - * @return array Returns the result of the SQL query - */ function serendipity_db_insert($table, $values, $action = 'execute') { global $serendipity; - - $names = implode(',', array_keys($values)); - - $vals = ''; - foreach ($values as $k => $v) { - if (strlen($vals)) - $vals .= ', '; - $vals .= '\'' . serendipity_db_escape_string($v) . '\''; - } - - $q = "INSERT INTO {$serendipity['dbPrefix']}$table ($names) values ($vals)"; - - if ($action == 'execute') { - return serendipity_db_query($q); - } else { - return $q; - } + $db = DbFactory::createFromConfig($serendipity); + return $db->insert($table, $values, $action); } -/** - * Check whether an input value corresponds to a TRUE/FALSE option in the SQL database. - * - * Because older DBs could not store TRUE/FALSE values to be restored into a PHP variable, - * this function tries to detect what the return code of a SQL column is, and convert it - * to a PHP native boolean. - * - * Values that will be recognized as TRUE are 'true', 't' and '1'. - * - * @access public - * @param string input value to compare - * @return boolean boolean conversion of the input value - */ function serendipity_db_bool($val) { - if(($val === true) || ($val == 'true') || ($val == 't') || ($val == '1')) - return true; - #elseif (($val === false || $val == 'false' || $val == 'f')) - else - return false; -} - -/** - * Return a SQL statement for a time interval or timestamp, specific to certain SQL backends - * - * @access public - * @param string Indicate whether to return a timestamp, or an Interval - * @param int The interval one might want to use, if Interval return was selected - * @return string SQL statement - */ -function serendipity_db_get_interval($val, $ival = 900) { global $serendipity; - - switch($serendipity['dbType']) { - case 'sqlite': - case 'sqlite3': - case 'sqlite3oo': - case 'pdo-sqlite': - $interval = $ival; - $ts = time(); - break; - - case 'pdo-postgres': - case 'postgres': - $interval = "interval '$ival'"; - $ts = 'NOW()'; - break; - - case 'mysql': - case 'mysqli': - default: - $interval = $ival; - $ts = 'NOW()'; - break; - } - - switch($val) { - case 'interval': - return $interval; - - default: - case 'ts': - return $ts; - } + $db = DbFactory::createFromConfig($serendipity); + return $db->bool($val); } -/** - * Operates on an array to prepare it for SQL usage. - * - * @access public - * @param string Concatenation character - * @param array Input array - * @param string How to convert (int: Only numbers, string: serendipity_db_escape_String) - * @return string Imploded string - */ -function serendipity_db_implode($string, &$array, $type = 'int') { - $new_array = array(); - if (!is_array($array)) { - return ''; - } - - foreach($array AS $idx => $key) { - if ($type == 'int') { - $new_array[$idx] = (int)$key; - } else { - $new_array[$idx] = serendipity_db_escape_string($key); - } - } - - $string = implode($string, $new_array); - return $string; +function serendipity_db_get_interval($val, $ival = 900) +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->getInterval($val, $ival); } -/* vim: set sts=4 ts=4 expandtab : */ +function serendipity_db_implode($string, &$array, $type = 'int') +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->implode($string, $array, $type); +} + +function serendipity_db_concat($string) +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->concat($string); +} + +function serendipity_db_logmsg($msgstr) +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->logmsg($msgstr); +} + +function serendipity_db_begin_transaction() +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->beginTransaction(); +} + +function serendipity_db_end_transaction($commit) +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->endTransaction($commit); +} + +function serendipity_db_in_sql($col, &$search_ids, $type = ' OR ') +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->inSql($col, $search_ids, $type); +} + +function serendipity_db_escape_string($string) +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->escapeString($string); +} + +function serendipity_db_limit($start, $offset) +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->limit($start, $offset); +} + +function serendipity_db_limit_sql($limitstring) +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->limitSql($limitstring); +} + +function serendipity_db_affected_rows() +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->affectedRows(); +} + +function serendipity_db_updated_rows() +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->updatedRows(); +} + +function serendipity_db_matched_rows() +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->matchedRows(); +} + +function serendipity_db_insert_id($table = '', $id = '') +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->insertId($table, $id); +} + +function &serendipity_db_query($sql, $single = false, $result_type = "both", $reportErr = false, $assocKey = false, $assocVal = false, $expectError = false) +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->query($sql, $single, $result_type, $reportErr, $assocKey, $assocVal, $expectError); +} + +function serendipity_db_schema_import($query) +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->schemaImport($query); +} + +function serendipity_db_probe($hash, &$errs) +{ + global $serendipity; + $db = DbFactory::createFromConfig($serendipity); + return $db->probe($hash, $errs); +} diff --git a/include/db/mysqli.inc.php b/include/db/mysqli.inc.php deleted file mode 100644 index 5dacaa0b..00000000 --- a/include/db/mysqli.inc.php +++ /dev/null @@ -1,486 +0,0 @@ - MYSQLI_ASSOC, 'num' => MYSQLI_NUM, 'both' => MYSQLI_BOTH, 'true' => true, 'false' => false); - - if ($expectError) { - $c = @mysqli_query($serendipity['dbConn'], $sql); - } else { - $c = mysqli_query($serendipity['dbConn'], $sql); - } - - if (!$expectError && mysqli_error($serendipity['dbConn']) != '') { - $msg = mysqli_error($serendipity['dbConn']); - return $msg; - } - - if (!$c) { - if (!$expectError && !$serendipity['production']) { - print mysqli_error($serendipity['dbConn']); - if (function_exists('debug_backtrace') && $reportErr == true) { - highlight_string(var_export(debug_backtrace(), 1)); - } - } - - return $type_map['false']; - } - - if ($c === true) { - return $type_map['true']; - } - - $result_type = $type_map[$result_type]; - - switch(mysqli_num_rows($c)) { - case 0: - if ($single) { - return $type_map['false']; - } - return $type_map['true']; - case 1: - if ($single) { - return mysqli_fetch_array($c, $result_type); - } - default: - if ($single) { - return mysqli_fetch_array($c, $result_type); - } - - $rows = array(); - while ($row = mysqli_fetch_array($c, $result_type)) { - if (!empty($assocKey)) { - // You can fetch a key-associated array via the two function parameters assocKey and assocVal - if (empty($assocVal)) { - $rows[$row[$assocKey]] = $row; - } else { - $rows[$row[$assocKey]] = $row[$assocVal]; - } - } else { - $rows[] = $row; - } - } - return $rows; - } -} - -/** - * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns - * - * @access public - * @return int Value of the auto-increment column - */ -function serendipity_db_insert_id() { - global $serendipity; - return mysqli_insert_id($serendipity['dbConn']); -} - -/** - * Returns the number of affected rows of a SQL query - * - * @access public - * @return int Number of affected rows - */ -function serendipity_db_affected_rows() { - global $serendipity; - return mysqli_affected_rows($serendipity['dbConn']); -} - -/** - * Returns the number of updated rows in a SQL query - * - * @access public - * @return int Number of updated rows - */ -function serendipity_db_updated_rows() { - global $serendipity; - - preg_match( - "/^[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)/", - mysqli_info($serendipity['dbConn']), - $arr); - // mysqli_affected_rows returns 0 if rows were matched but not changed. - // mysqli_info returns rows matched - return $arr[1]; -} - -/** - * Returns the number of matched rows in a SQL query - * - * @access public - * @return int Number of matched rows - */ -function serendipity_db_matched_rows() { - global $serendipity; - - preg_match( - "/^[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)/", - mysqli_info($serendipity['dbConn']), - $arr); - // mysqli_affected_rows returns 0 if rows were matched but not changed. - // mysqli_info returns rows matched - return $arr[0]; -} - -/** - * Returns a escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. - * - * @access public - * @param string input string - * @return string output string - */ -function serendipity_db_escape_string($string) { - global $serendipity; - return mysqli_escape_string($serendipity['dbConn'], $string); -} - -/** - * Returns the option to a LIMIT SQL statement, because it varies across DB systems - * - * @access public - * @param int Number of the first row to return data from - * @param int Number of rows to return - * @return string SQL string to pass to a LIMIT statement - */ -function serendipity_db_limit($start, $offset) { - return $start . ', ' . $offset; -} - -/** - * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement - * - * @access public - * @param SQL string of a LIMIT option - * @return SQL string containing a full LIMIT statement - */ -function serendipity_db_limit_sql($limitstring) { - return ' LIMIT ' . $limitstring; -} - -/** - * Connect to the configured Database - * - * @access public - * @return resource connection handle - */ -function serendipity_db_connect() { - global $serendipity; - - if (isset($serendipity['dbConn'])) { - return $serendipity['dbConn']; - } - - $function = 'mysqli_connect'; - - $connparts = explode(':', $serendipity['dbHost']); - if (!empty($connparts[1])) { - // A "hostname:port" connection was specified - $serendipity['dbConn'] = $function($connparts[0], $serendipity['dbUser'], $serendipity['dbPass'], $serendipity['dbName'], $connparts[1]); - } else { - // Connect with default ports - $serendipity['dbConn'] = $function($connparts[0], $serendipity['dbUser'], $serendipity['dbPass']); - } - mysqli_select_db($serendipity['dbConn'], $serendipity['dbName']); - serendipity_db_reconnect(); - - return $serendipity['dbConn']; -} - -function serendipity_db_reconnect() { - global $serendipity; - - $use_charset = ''; - if (isset($serendipity['dbCharset']) && !empty($serendipity['dbCharset'])) { - $use_charset = $serendipity['dbCharset']; - if (!defined('SQL_CHARSET_INIT')) { define('SQL_CHARSET_INIT', true); } - } elseif (defined('SQL_CHARSET') && $serendipity['dbNames'] && !defined('SQL_CHARSET_INIT')) { - $use_charset = SQL_CHARSET; - } - - if ($use_charset != '') { - mysqli_set_charset($serendipity['dbConn'], $use_charset); - } -} - -/** - * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. - * - * @access public - * @param string SQL query with template variables to convert - * @return resource SQL resource handle of the executed query - */ -function serendipity_db_schema_import($query) { - static $search = array('{AUTOINCREMENT}', '{PRIMARY}', - '{UNSIGNED}', '{FULLTEXT}', '{FULLTEXT_MYSQL}', '{BOOLEAN}', '{TEXT}'); - static $replace = array('int(11) not null auto_increment', 'primary key', - 'unsigned' , 'FULLTEXT', 'FULLTEXT', 'enum (\'true\', \'false\') NOT NULL default \'true\'', 'LONGTEXT'); - global $serendipity; - - $search[] = '{UTF_8}'; - if ( $_POST['charset'] == 'UTF-8/' || - $serendipity['charset'] == 'UTF-8/' || - $serendipity['POST']['charset'] == 'UTF-8/' || - LANG_CHARSET == 'UTF-8' ) { - if (serendipity_utf8mb4_ready()) { - $replace[] = 'ROW_FORMAT=DYNAMIC /*!40100 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */'; - } else { - # in old versions we stick to the three byte pseudo utf8 to not trip - # over the max index restriction of 1000 bytes - $replace[] = '/*!40100 CHARACTER SET utf8 COLLATE utf8_unicode_ci */'; - } - } else { - $replace[] = ''; - } - - - if (serendipity_utf8mb4_ready()) { - # InnoDB enables us to use utf8mb4 with the higher max index size - serendipity_db_query("SET storage_engine=INNODB"); - } else { - # Before 5.6.4/10.0.5 InnoDB did not support fulltext indexes, which we use, - # thus we stay with MyISAM here - serendipity_db_query("SET storage_engine=MYISAM"); - } - - $query = trim(str_replace($search, $replace, $query)); - if ($query[0] == '@') { - // Errors are expected to happen (like duplicate index creation) - return serendipity_db_query(substr($query, 1), false, 'both', false, false, false, true); - } else { - return serendipity_db_query($query); - } -} - - -/** - * Check if we think that it is safe to ugprade to utf8mb4. This checks version numbers and applied settings. - * Depending on the version of mariadb/mysql we need to check either one or three settings. We check for - * innodb being available with fulltext index and large index support, so that our database scheme can work - * - * @return boolean Whether the database could support utf8mb4 - */ -function serendipity_utf8mb4_ready() { - global $serendipity; - - $mysql_version = mysqli_get_server_info($serendipity['dbConn']); - $maria = false; - if (strpos($mysql_version, 'MariaDB') !== false) { - $maria = true; - } - if ($maria) { - # maria trips up our version detection scheme by prepending 5.5.5 to newer versions - $mysql_version = str_replace('5.5.5-', '', $mysql_version); - } - - if ($maria) { - # see https://mariadb.com/kb/en/innodb-file-format/ for when barracuda is available, and when it's the only option - # see https://docs.nextcloud.com/server/15/admin_manual/configuration_database/mysql_4byte_support.html for which - # variables we have to check to assume utf8mb4 it can work (with the large indexes we need) - - if (version_compare($mysql_version, '10.3.1', '>=')) { - # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_file_per_table - $rows = serendipity_db_query("SHOW VARIABLES LIKE 'innodb_file_per_table'"); - try { - return $rows[0][1] == 'ON'; - } catch (Exception $e) { - return false; - } - } - - # see https://mariadb.com/kb/en/full-text-index-overview/. We need 10.0.5 to have fulltext indexes with innodb - if (version_compare($mysql_version, '10.0.5', '>=')) { - # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_file_per_table - $rows = serendipity_db_query("SHOW VARIABLES LIKE 'innodb_file_per_table'"); - try { - if ($rows[0][1] != 'ON') { - return false; - } - } catch (Exception $e) { - return false; - } - - # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_file_format - $rows = serendipity_db_query("SHOW VARIABLES LIKE 'innodb_file_format'"); - try { - if ($rows[0][1] != 'barracuda') { - return false; - } - } catch (Exception $e) { - return false; - } - - # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_large_prefix - $rows = serendipity_db_query("SHOW VARIABLES LIKE 'innodb_large_prefix'"); - try { - if ($rows[0][1] != 'ON') { - return false; - } - } catch (Exception $e) { - return false; - } - } - return false; # version too old - } - - # now we know it is not mariadb, but "real" mysql - - # These versions might need to be changed based on testing feedback - if (version_compare($mysql_version, '8.0.0', '>=')) { - $rows = serendipity_db_query("SHOW VARIABLES LIKE 'innodb_file_per_table'"); - try { - return $rows[0][1] == 'ON'; - } catch (Exception $e) { - return false; - } - } - - # see https://dev.mysql.com/doc/refman/5.6/en/innodb-fulltext-index.html. We need 5.6 for fulltext indexes - if (version_compare($mysql_version, '5.6', '>=')) { - # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_file_per_table - $rows = serendipity_db_query("SHOW VARIABLES LIKE 'innodb_file_per_table'"); - try { - if ($rows[0][1] != 'ON') { - return false; - } - } catch (Exception $e) { - return false; - } - - # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_file_format - $rows = serendipity_db_query("SHOW VARIABLES LIKE 'innodb_file_format'"); - try { - if ($rows[0][1] != 'barracuda') { - return false; - } - } catch (Exception $e) { - return false; - } - - # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_large_prefix - $rows = serendipity_db_query("SHOW VARIABLES LIKE 'innodb_large_prefix'"); - try { - if ($rows[0][1] != 'ON') { - return false; - } - } catch (Exception $e) { - return false; - } - return true; - } - - return false; # version too old -} - -/** - * Try to connect to the configured Database (during installation) - * - * @access public - * @param array input configuration array, holding the connection info - * @param array referenced array which holds the errors that might be encountered - * @return boolean return true on success, false on error - */ -function serendipity_db_probe($hash, &$errs) { - global $serendipity; - - if (!function_exists('mysqli_connect')) { - $errs[] = 'No mySQLi extension found. Please check your webserver installation or contact your systems administrator regarding this problem.'; - return false; - } - - $function = 'mysqli_connect'; - $connparts = explode(':', $hash['dbHost']); - if (!empty($connparts[1])) { - // A "hostname:port" connection was specified - $c = @$function($connparts[0], $hash['dbUser'], $hash['dbPass'], $hash['dbName'], $connparts[1]); - } else { - // Connect with default ports - $c = @$function($connparts[0], $hash['dbUser'], $hash['dbPass']); - } - - if (!$c) { - $errs[] = 'Could not connect to database; check your settings.'; - $errs[] = 'The mySQL error was: ' . serendipity_specialchars(mysqli_connect_error()); - return false; - } - - $serendipity['dbConn'] = $c; - - if ( !@mysqli_select_db($c, $hash['dbName']) ) { - $errs[] = 'The database you specified does not exist.'; - $errs[] = 'The mySQL error was: ' . serendipity_specialchars(mysqli_error($c)); - return false; - } - - return true; -} - -/** - * Returns the SQL code used for concatenating strings - * - * @access public - * @param string Input string/column to concatenate - * @return string SQL parameter - */ -function serendipity_db_concat($string) { - return 'concat(' . $string . ')'; -} - -/* vim: set sts=4 ts=4 expandtab : */ diff --git a/include/db/pdo-postgres.inc.php b/include/db/pdo-postgres.inc.php deleted file mode 100644 index 7d6b1dc3..00000000 --- a/include/db/pdo-postgres.inc.php +++ /dev/null @@ -1,336 +0,0 @@ -beginTransaction(); -} - -/** - * Tells the DB Layer to end a DB transaction. - * - * @access public - * @param boolean If true, perform the query. If false, rollback. - */ -function serendipity_db_end_transaction($commit){ - global $serendipity; - if ($commit){ - $serendipity['dbConn']->commit(); - }else{ - $serendipity['dbConn']->rollback(); - } -} - -/** - * Assemble and return SQL condition for a "IN (...)" clause - * - * @access public - * @param string table column name - * @param array referenced array of values to search for in the "IN (...)" clause - * @param string condition of how to associate the different input values of the $search_ids parameter - * @return string resulting SQL string - */ -function serendipity_db_in_sql($col, &$search_ids, $type = ' OR ') { - return $col . " IN (" . implode(', ', $search_ids) . ")"; -} - -/** - * Connect to the configured Database - * - * @access public - * @return resource connection handle - */ -function serendipity_db_connect() { - global $serendipity; - - $host = $port = ''; - if (strlen($serendipity['dbHost'])) { - if (false !== strstr($serendipity['dbHost'], ':')) { - $tmp = explode(':', $serendipity['dbHost']); - $host = "host={$tmp[0]};"; - $port = "port={$tmp[1]};"; - } else { - $host = "host={$serendipity['dbHost']};"; - } - } - - $serendipity['dbConn'] = new PDO( - sprintf( - 'pgsql:%sdbname=%s', - "$host$port", - $serendipity['dbName'] - ), - $serendipity['dbUser'], - $serendipity['dbPass'] - ); - - return $serendipity['dbConn']; -} - -function serendipity_db_reconnect() { -} - -/** - * Returns a escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. - * - * @access public - * @param string input string - * @return string output string - */ -function serendipity_db_escape_string($string) { - global $serendipity; - return substr($serendipity['dbConn']->quote($string), 1, -1); -} - -/** - * Returns the option to a LIMIT SQL statement, because it varies across DB systems - * - * @access public - * @param int Number of the first row to return data from - * @param int Number of rows to return - * @return string SQL string to pass to a LIMIT statement - */ -function serendipity_db_limit($start, $offset) { - return $offset . ', ' . $start; -} - -/** - * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement - * - * @access public - * @param SQL string of a LIMIT option - * @return SQL string containing a full LIMIT statement - */ -function serendipity_db_limit_sql($limitstring) { - $limit_split = explode(',', $limitstring); - if (count($limit_split) > 1) { - $limit = ' LIMIT ' . $limit_split[0] . ' OFFSET ' . $limit_split[1]; - } else { - $limit = ' LIMIT ' . $limit_split[0]; - } - return $limit; -} - -/** - * Returns the number of affected rows of a SQL query - * - * @access public - * @return int Number of affected rows - */ -function serendipity_db_affected_rows() { - global $serendipity; - return $serendipity['dbSth']->rowCount(); -} - -/** - * Returns the number of updated rows in a SQL query - * - * @access public - * @return int Number of updated rows - */ -function serendipity_db_updated_rows() { - global $serendipity; - // it is unknown whether pg_affected_rows returns number of rows - // UPDATED or MATCHED on an UPDATE statement. - return $serendipity['dbSth']->rowCount(); -} - -/** - * Returns the number of matched rows in a SQL query - * - * @access public - * @return int Number of matched rows - */ -function serendipity_db_matched_rows() { - global $serendipity; - // it is unknown whether pg_affected_rows returns number of rows - // UPDATED or MATCHED on an UPDATE statement. - return $serendipity['dbSth']->rowCount(); -} - -/** - * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns - * - * @access public - * @param string Name of the table to get a INSERT ID for - * @param string Name of the column to get a INSERT ID for - * @return int Value of the auto-increment column - */ -function serendipity_db_insert_id($table = '', $id = '') { - global $serendipity; - if (empty($table) || empty($id)) { - // BC - will/should never be called with empty parameters! - return $serendipity['dbConn']->lastInsertId(); - } else { - $query = "SELECT currval('{$serendipity['dbPrefix']}{$table}_{$id}_seq'::text) AS {$id}"; - $res = $serendipity['dbConn']->prepare($query); - $res->execute(); - foreach($res->fetchAll(PDO::FETCH_ASSOC) AS $row) { - return $row[$id]; - } - return $serendipity['dbConn']->lastInsertId(); - } -} - -/** - * Perform a DB Layer SQL query. - * - * This function returns values dependin on the input parameters and the result of the query. - * It can return: - * false or a string if there was an error (depends on $expectError), - * true if the query succeeded but did not generate any rows - * array of field values if it returned a single row and $single is true - * array of array of field values if it returned row(s) [stacked array] - * - * @access public - * @param string SQL query to execute - * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! - * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) - * @param boolean If true, errors will be reported. If false, errors will be ignored. - * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column - * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. - * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) - * @return mixed Returns the result of the SQL query, depending on the input parameters - */ -function &serendipity_db_query($sql, $single = false, $result_type = "both", $reportErr = false, $assocKey = false, $assocVal = false, $expectError = false) { - global $serendipity; - $type_map = array( - 'assoc' => PDO::FETCH_ASSOC, - 'num' => PDO::FETCH_NUM, - 'both' => PDO::FETCH_BOTH, - 'true' => true, - 'false' => false - ); - - if (!$expectError && ($reportErr || !$serendipity['production'])) { - $serendipity['dbSth'] = $serendipity['dbConn']->prepare($sql); - } else { - $serendipity['dbSth'] = $serendipity['dbConn']->prepare($sql); - } - - if (!$serendipity['dbSth']) { - if (!$expectError && !$serendipity['production']) { - print "Error in $sql"; - print $serendipity['dbConn']->errorInfo() . "
\n"; - if (function_exists('debug_backtrace')) { - highlight_string(var_export(debug_backtrace(), 1)); - } - print "
$sql
\n"; - } - return $type_map['false']; - } - - $serendipity['dbSth']->execute(); - - if ($serendipity['dbSth'] === true) { - return $type_map['true']; - } - - $result_type = $type_map[$result_type]; - - $n = 0; - - $rows = array(); - foreach($serendipity['dbSth']->fetchAll($result_type) AS $row) { - if (!empty($assocKey)) { - // You can fetch a key-associated array via the two function parameters assocKey and assocVal - if (empty($assocVal)) { - $rows[$row[$assocKey]] = $row; - } else { - $rows[$row[$assocKey]] = $row[$assocVal]; - } - } else { - $rows[] = $row; - } - } - if(count($rows) == 0) { - if ($single) { - return $type_map['false']; - } - return $type_map['true']; - } - if(count($rows) == 1 && $single) { - return $rows[0]; - } - return $rows; -} - -/** - * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. - * - * @access public - * @param string SQL query with template variables to convert - * @return resource SQL resource handle of the executed query - */ -function serendipity_db_schema_import($query) { - static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', - '{FULLTEXT}', '{BOOLEAN}', 'int(1)', 'int(10)', 'int(11)', 'int(4)', '{UTF_8}', '{TEXT}'); - static $replace = array('SERIAL', 'primary key', '', '', 'BOOLEAN NOT NULL', 'int2', - 'int4', 'int4', 'int4', '', 'text'); - - if (stristr($query, '{FULLTEXT_MYSQL}')) { - return true; - } - - $query = trim(str_replace($search, $replace, $query)); - if ($query[0] == '@') { - // Errors are expected to happen (like duplicate index creation) - return serendipity_db_query(substr($query, 1), false, 'both', false, false, false, true); - } else { - return serendipity_db_query($query); - } -} - -/** - * Try to connect to the configured Database (during installation) - * - * @access public - * @param array input configuration array, holding the connection info - * @param array referenced array which holds the errors that might be encountered - * @return boolean return true on success, false on error - */ -function serendipity_db_probe($hash, &$errs) { - global $serendipity; - - if(!in_array('pgsql', PDO::getAvailableDrivers())) { - $errs[] = 'PDO_PGSQL driver not avialable'; - return false; - } - - $serendipity['dbConn'] = new PDO( - sprintf( - 'pgsql:%sdbname=%s', - strlen($hash['dbHost']) ? ('host=' . $hash['dbHost'] . ';') : '', - $hash['dbName'] - ), - $hash['dbUser'], - $hash['dbPass'] - ); - - if (!$serendipity['dbConn']) { - $errs[] = 'Could not connect to database; check your settings.'; - return false; - } - - return true; -} - -/** - * Returns the SQL code used for concatenating strings - * - * @access public - * @param string Input string/column to concatenate - * @return string SQL parameter - */ -function serendipity_db_concat($string) { - return '(' . str_replace(', ', '||', $string) . ')'; -} - -/* vim: set sts=4 ts=4 expandtab : */ diff --git a/include/db/pdo-sqlite.inc.php b/include/db/pdo-sqlite.inc.php deleted file mode 100644 index 55cfd601..00000000 --- a/include/db/pdo-sqlite.inc.php +++ /dev/null @@ -1,356 +0,0 @@ - $v) { - // TODO: If a query of the format 'SELECT a.id, b.text FROM table' is used, - // the sqlite extension will give us key indizes 'a.id' and 'b.text' - // instead of just 'id' and 'text' like in mysql/postgresql extension. - // To fix that, we use a preg-regex; but that is quite performance costy. - // Either we always need to use 'SELECT a.id AS id, b.text AS text' in query, - // or the sqlite extension may get fixed. :-) - $newrow[preg_replace('@^.+\.(.*)@', '\1', $i)] = str_replace($search, $replace, $v); - } - - return $newrow; -} - -/** - * Tells the DB Layer to start a DB transaction. - * - * @access public - */ - -function serendipity_db_begin_transaction(){ - global $serendipity; - $serendipity['dbConn']->beginTransaction(); -} - -/** - * Tells the DB Layer to end a DB transaction. - * - * @access public - * @param boolean If true, perform the query. If false, rollback. - */ -function serendipity_db_end_transaction($commit){ - global $serendipity; - if ($commit){ - $serendipity['dbConn']->commit(); - }else{ - $serendipity['dbConn']->rollback(); - } -} - -/** - * Assemble and return SQL condition for a "IN (...)" clause - * - * @access public - * @param string table column name - * @param array referenced array of values to search for in the "IN (...)" clause - * @param string condition of how to associate the different input values of the $search_ids parameter - * @return string resulting SQL string - */ -function serendipity_db_in_sql($col, &$search_ids, $type = ' OR ') { - $sql = array(); - if (!is_array($search_ids)) { - return false; - } - - foreach($search_ids AS $id) { - $sql[] = $col . ' = ' . $id; - } - - $cond = '(' . implode($type, $sql) . ')'; - return $cond; -} - -/** - * Connect to the configured Database - * - * @access public - * @return resource connection handle - */ -function serendipity_db_connect() { - global $serendipity; - - $serendipity['dbConn'] = new PDO( - 'sqlite:' . (defined('S9Y_DATA_PATH') ? S9Y_DATA_PATH : $serendipity['serendipityPath']) . $serendipity['dbName'] . '.db' - ); - - return $serendipity['dbConn']; -} - -function serendipity_db_reconnect() { -} - -/** - * Returns a escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. - * - * @access public - * @param string input string - * @return string output string - */ -function serendipity_db_escape_string($string) { - global $serendipity; - return substr($serendipity['dbConn']->quote($string), 1, -1); -} - -/** - * Returns the option to a LIMIT SQL statement, because it varies across DB systems - * - * @access public - * @param int Number of the first row to return data from - * @param int Number of rows to return - * @return string SQL string to pass to a LIMIT statement - */ -function serendipity_db_limit($start, $offset) { - return $start . ', ' . $offset; -} - -/** - * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement - * - * @access public - * @param SQL string of a LIMIT option - * @return SQL string containing a full LIMIT statement - */ -function serendipity_db_limit_sql($limitstring) { - return ' LIMIT ' . $limitstring; -} - -/** - * Returns the number of affected rows of a SQL query - * - * @access public - * @return int Number of affected rows - */ -function serendipity_db_affected_rows() { - global $serendipity; - return $serendipity['dbSth']->rowCount(); -} - -/** - * Returns the number of updated rows in a SQL query - * - * @access public - * @return int Number of updated rows - */ -function serendipity_db_updated_rows() { - global $serendipity; - // it is unknown whether pg_affected_rows returns number of rows - // UPDATED or MATCHED on an UPDATE statement. - return $serendipity['dbSth']->rowCount(); -} - -/** - * Returns the number of matched rows in a SQL query - * - * @access public - * @return int Number of matched rows - */ -function serendipity_db_matched_rows() { - global $serendipity; - // it is unknown whether pg_affected_rows returns number of rows - // UPDATED or MATCHED on an UPDATE statement. - return $serendipity['dbSth']->rowCount(); -} - -/** - * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns - * - * @access public - * @param string Name of the table to get a INSERT ID for - * @param string Name of the column to get a INSERT ID for - * @return int Value of the auto-increment column - */ -function serendipity_db_insert_id($table = '', $id = '') { - global $serendipity; - return $serendipity['dbConn']->lastInsertId(); -} - -/** - * Perform a DB Layer SQL query. - * - * This function returns values dependin on the input parameters and the result of the query. - * It can return: - * false or a string if there was an error (depends on $expectError), - * true if the query succeeded but did not generate any rows - * array of field values if it returned a single row and $single is true - * array of array of field values if it returned row(s) [stacked array] - * - * @access public - * @param string SQL query to execute - * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! - * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) - * @param boolean If true, errors will be reported. If false, errors will be ignored. - * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column - * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. - * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) - * @return mixed Returns the result of the SQL query, depending on the input parameters - */ -function &serendipity_db_query($sql, $single = false, $result_type = "both", $reportErr = false, $assocKey = false, $assocVal = false, $expectError = false) { - global $serendipity; - $type_map = array( - 'assoc' => PDO::FETCH_ASSOC, - 'num' => PDO::FETCH_NUM, - 'both' => PDO::FETCH_BOTH, - 'true' => true, - 'false' => false - ); - - //serendipity_db_logmsg('SQLQUERY: ' . $sql); - if (!$expectError && ($reportErr || !$serendipity['production'])) { - $serendipity['dbSth'] = $serendipity['dbConn']->prepare($sql); - } else { - $serendipity['dbSth'] = $serendipity['dbConn']->prepare($sql); - } - - if (!$serendipity['dbSth']) { - if (!$expectError && !$serendipity['production']) { - print "Error in $sql"; - print $serendipity['dbConn']->errorInfo() . "
\n"; - if (function_exists('debug_backtrace')) { - highlight_string(var_export(debug_backtrace(), 1)); - } - print "
$sql
"; - } - return $type_map['false']; - } - - $serendipity['dbSth']->execute(); - - if ($serendipity['dbSth'] === true) { - return $type_map['true']; - } - - $result_type = $type_map[$result_type]; - - $rows = array(); - - foreach($serendipity['dbSth']->fetchAll($result_type) AS $row) { - $row = serendipity_db_sqlite_fetch_array($row, $result_type); - if (!empty($assocKey)) { - // You can fetch a key-associated array via the two function parameters assocKey and assocVal - if (empty($assocVal)) { - $rows[$row[$assocKey]] = $row; - } else { - $rows[$row[$assocKey]] = $row[$assocVal]; - } - } else { - $rows[] = $row; - } - } - //serendipity_db_logmsg('SQLRESULT: ' . print_r($rows,true)); - - if(count($rows) == 0) { - if ($single) { - return $type_map['false']; - } - return $type_map['true']; - } - if(count($rows) == 1 && $single) { - return $rows[0]; - } - return $rows; -} - -/** - * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. - * - * @access public - * @param string SQL query with template variables to convert - * @return resource SQL resource handle of the executed query - */ -function serendipity_db_schema_import($query) { - static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', '{FULLTEXT}', '{BOOLEAN}', '{UTF_8}', '{TEXT}'); - static $replace = array('INTEGER AUTOINCREMENT', 'PRIMARY KEY', '', '', 'BOOLEAN NOT NULL', '', 'LONGTEXT'); - - if (stristr($query, '{FULLTEXT_MYSQL}')) { - return true; - } - - $query = trim(str_replace($search, $replace, $query)); - $query = str_replace('INTEGER AUTOINCREMENT PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT', $query); - if ($query[0] == '@') { - // Errors are expected to happen (like duplicate index creation) - return serendipity_db_query(substr($query, 1), false, 'both', false, false, false, true); - } else { - return serendipity_db_query($query); - } -} - -/** - * Try to connect to the configured Database (during installation) - * - * @access public - * @param array input configuration array, holding the connection info - * @param array referenced array which holds the errors that might be encountered - * @return boolean return true on success, false on error - */ -function serendipity_db_probe($hash, &$errs) { - global $serendipity; - - if(!in_array('sqlite', PDO::getAvailableDrivers())) { - $errs[] = 'PDO_SQLITE driver not available'; - return false; - } - - $dbName = (isset($hash['sqlitedbName']) ? $hash['sqlitedbName'] : $hash['dbName']); - if (defined('S9Y_DATA_PATH')) { - // Shared installations! - $dbfile = S9Y_DATA_PATH . $dbName . '.db'; - } else { - $dbfile = $serendipity['serendipityPath'] . $dbName . '.db'; - } - - $serendipity['dbConn'] = new PDO( - 'sqlite:' . $dbfile - ); - - if (!$serendipity['dbConn']) { - $errs[] = "Unable to open \"$dbfile\" - check permissions (directory needs to be writeable for webserver)!"; - return false; - } - - return true; -} - -/** - * Returns the SQL code used for concatenating strings - * - * @access public - * @param string Input string/column to concatenate - * @return string SQL parameter - */ -function serendipity_db_concat($string) { - return 'concat(' . $string . ')'; -} - -/* vim: set sts=4 ts=4 expandtab : */ diff --git a/include/db/postgres.inc.php b/include/db/postgres.inc.php deleted file mode 100644 index ec32b383..00000000 --- a/include/db/postgres.inc.php +++ /dev/null @@ -1,348 +0,0 @@ - 1) { - $limit = ' LIMIT ' . $limit_split[0] . ' OFFSET ' . $limit_split[1]; - } else { - $limit = ' LIMIT ' . $limit_split[0]; - } - return $limit; -} - -/** - * Returns the number of affected rows of a SQL query - * - * @access public - * @return int Number of affected rows - */ -function serendipity_db_affected_rows() { - global $serendipity; - return pg_affected_rows($serendipity['dbLastResult']); -} - -/** - * Returns the number of updated rows in a SQL query - * - * @access public - * @return int Number of updated rows - */ -function serendipity_db_updated_rows() { - global $serendipity; - // it is unknown whether pg_affected_rows returns number of rows - // UPDATED or MATCHED on an UPDATE statement. - return pg_affected_rows($serendipity['dbLastResult']); -} - -/** - * Returns the number of matched rows in a SQL query - * - * @access public - * @return int Number of matched rows - */ -function serendipity_db_matched_rows() { - global $serendipity; - // it is unknown whether pg_affected_rows returns number of rows - // UPDATED or MATCHED on an UPDATE statement. - return pg_affected_rows($serendipity['dbLastResult']); -} - -/** - * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns - * - * @access public - * @param string Name of the table to get a INSERT ID for - * @param string Name of the column to get a INSERT ID for - * @return int Value of the auto-increment column - */ -function serendipity_db_insert_id($table = '', $id = '') { - global $serendipity; - if (empty($table) || empty($id)) { - // BC - will/should never be called with empty parameters! - return pg_last_oid($serendipity['dbLastResult']); - } else { - $query = "SELECT currval('{$serendipity['dbPrefix']}{$table}_{$id}_seq'::text) AS {$id}"; - $res = pg_query($serendipity['dbConn'], $query); - if (pg_num_rows($res)) { - $insert_id = pg_fetch_array($res, 0, PGSQL_ASSOC); - return $insert_id[$id]; - } else { - return pg_last_oid($serendipity['dbLastResult']); // BC - should not happen! - } - } -} - -/** - * Perform a DB Layer SQL query. - * - * This function returns values dependin on the input parameters and the result of the query. - * It can return: - * false or a string if there was an error (depends on $expectError), - * true if the query succeeded but did not generate any rows - * array of field values if it returned a single row and $single is true - * array of array of field values if it returned row(s) [stacked array] - * - * @access public - * @param string SQL query to execute - * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! - * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) - * @param boolean If true, errors will be reported. If false, errors will be ignored. - * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column - * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. - * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) - * @return mixed Returns the result of the SQL query, depending on the input parameters - */ -function &serendipity_db_query($sql, $single = false, $result_type = "both", $reportErr = false, $assocKey = false, $assocVal = false, $expectError = false) { - global $serendipity; - $type_map = array( - 'assoc' => PGSQL_ASSOC, - 'num' => PGSQL_NUM, - 'both' => PGSQL_BOTH, - 'true' => true, - 'false' => false - ); - - if (!isset($serendipity['dbPgsqlOIDS'])) { - $serendipity['dbPgsqlOIDS'] = true; - @serendipity_db_query('SET default_with_oids = true', true, 'both', false, false, false, true); - } - - if (!$expectError && ($reportErr || !$serendipity['production'])) { - $serendipity['dbLastResult'] = pg_query($serendipity['dbConn'], $sql); - } else { - $serendipity['dbLastResult'] = @pg_query($serendipity['dbConn'], $sql); - } - - if (!$serendipity['dbLastResult']) { - if (!$expectError && !$serendipity['production']) { - print "Error in $sql"; - print pg_last_error($serendipity['dbConn']) . "
\n"; - if (function_exists('debug_backtrace')) { - highlight_string(var_export(debug_backtrace(), 1)); - } - print "
$sql
"; - } - return $type_map['false']; - } - - if ($serendipity['dbLastResult'] === true) { - return $type_map['true']; - } - - $result_type = $type_map[$result_type]; - - $n = pg_num_rows($serendipity['dbLastResult']); - - switch ($n) { - case 0: - if ($single) { - return $type_map['false']; - } - return $type_map['true']; - case 1: - if ($single) { - return pg_fetch_array($serendipity['dbLastResult'], 0, $result_type); - } - default: - $rows = array(); - for ($i = 0; $i < $n; $i++) { - if (!empty($assocKey)) { - // You can fetch a key-associated array via the two function parameters assocKey and assocVal - $row = pg_fetch_array($serendipity['dbLastResult'], $i, $result_type); - if (empty($assocVal)) { - $rows[$row[$assocKey]] = $row; - } else { - $rows[$row[$assocKey]] = $row[$assocVal]; - } - } else { - $rows[] = pg_fetch_array($serendipity['dbLastResult'], $i, $result_type); - } - } - return $rows; - } -} - -/** - * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. - * - * @access public - * @param string SQL query with template variables to convert - * @return resource SQL resource handle of the executed query - */ -function serendipity_db_schema_import($query) { - static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', - '{FULLTEXT}', '{BOOLEAN}', 'int(1)', 'int(10)', 'int(11)', 'int(4)', '{UTF_8}', '{TEXT}'); - static $replace = array('SERIAL', 'primary key', '', '', 'BOOLEAN NOT NULL', 'int2', - 'int4', 'int4', 'int4', '', 'text'); - - if (stristr($query, '{FULLTEXT_MYSQL}')) { - return true; - } - - $query = trim(str_replace($search, $replace, $query)); - if ($query[0] == '@') { - // Errors are expected to happen (like duplicate index creation) - return serendipity_db_query(substr($query, 1), false, 'both', false, false, false, true); - } else { - return serendipity_db_query($query); - } -} - -/** - * Try to connect to the configured Database (during installation) - * - * @access public - * @param array input configuration array, holding the connection info - * @param array referenced array which holds the errors that might be encountered - * @return boolean return true on success, false on error - */ -function serendipity_db_probe($hash, &$errs) { - global $serendipity; - - if (!function_exists('pg_connect')) { - $errs[] = 'No PostgreSQL extension found. Please check your webserver installation or contact your systems administrator regarding this problem.'; - return false; - } - - $serendipity['dbConn'] = pg_connect( - sprintf( - '%sdbname=%s user=%s password=%s', - - strlen($hash['dbHost']) ? ('host=' . $hash['dbHost'] . ' ') : '', - $hash['dbName'], - $hash['dbUser'], - $hash['dbPass'] - ) - ); - - if (!$serendipity['dbConn']) { - $errs[] = 'Could not connect to database; check your settings.'; - return false; - } - - return true; -} - -/** - * Returns the SQL code used for concatenating strings - * - * @access public - * @param string Input string/column to concatenate - * @return string SQL parameter - */ -function serendipity_db_concat($string) { - return '(' . str_replace(', ', '||', $string) . ')'; -} - -/* vim: set sts=4 ts=4 expandtab : */ diff --git a/include/db/sqlite.inc.php b/include/db/sqlite.inc.php deleted file mode 100644 index dff4b844..00000000 --- a/include/db/sqlite.inc.php +++ /dev/null @@ -1,388 +0,0 @@ - $v) { - // TODO: If a query of the format 'SELECT a.id, b.text FROM table' is used, - // the sqlite extension will give us key indizes 'a.id' and 'b.text' - // instead of just 'id' and 'text' like in mysql/postgresql extension. - // To fix that, we use a preg-regex; but that is quite performance costy. - // Either we always need to use 'SELECT a.id AS id, b.text AS text' in query, - // or the sqlite extension may get fixed. :-) - $row[preg_replace('@^.+\.(.*)@', '\1', $i)] = str_replace($search, $replace, $v); - } - - return $row; -} - -/** - * Assemble and return SQL condition for a "IN (...)" clause - * - * @access public - * @param string table column name - * @param array referenced array of values to search for in the "IN (...)" clause - * @param string condition of how to associate the different input values of the $search_ids parameter - * @return string resulting SQL string - */ -function serendipity_db_in_sql($col, &$search_ids, $type = ' OR ') { - $sql = array(); - if (!is_array($search_ids)) { - return false; - } - - foreach($search_ids AS $id) { - $sql[] = $col . ' = ' . $id; - } - - $cond = '(' . implode($type, $sql) . ')'; - return $cond; -} - -/** - * Perform a DB Layer SQL query. - * - * This function returns values dependin on the input parameters and the result of the query. - * It can return: - * false or a string if there was an error (depends on $expectError), - * true if the query succeeded but did not generate any rows - * array of field values if it returned a single row and $single is true - * array of array of field values if it returned row(s) [stacked array] - * - * @access public - * @param string SQL query to execute - * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! - * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) - * @param boolean If true, errors will be reported. If false, errors will be ignored. - * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column - * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. - * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignored on DUPLICATE INDEX queries and the likes) - * @return mixed Returns the result of the SQL query, depending on the input parameters - */ -function &serendipity_db_query($sql, $single = false, $result_type = "both", $reportErr = true, $assocKey = false, $assocVal = false, $expectError = false) -{ - global $serendipity; - $type_map = array( - 'assoc' => SQLITE_ASSOC, - 'num' => SQLITE_NUM, - 'both' => SQLITE_BOTH, - 'true' => true, - 'false' => false - ); - - static $debug = false; - - if ($debug) { - // Open file and write directly. In case of crashes, the pointer needs to be killed. - $fp = @fopen('sqlite.log', 'a'); - fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE QUERY: ' . $sql . "\n\n"); - fclose($fp); - } - - if ($reportErr && !$expectError) { - $res = sqlite_query($sql, $serendipity['dbConn']); - } else { - $res = @sqlite_query($sql, $serendipity['dbConn']); - } - - if (!$res) { - if (!$expectError && !$serendipity['production']) { - var_dump($res); - var_dump($sql); - $msg = "problem with query"; - return $msg; - } - if ($debug) { - $fp = @fopen('sqlite.log', 'a'); - fwrite($fp, '[' . date('d.m.Y H:i') . '] [ERROR] ' . "\n\n"); - fclose($fp); - } - - return $type_map['false']; - } - - if ($res === true) { - return $type_map['true']; - } - - if (sqlite_num_rows($res) == 0) { - if ($single) { - return $type_map['false']; - } - return $type_map['true']; - } else { - $rows = array(); - - while (($row = serendipity_db_sqlite_fetch_array($res, $type_map[$result_type]))) { - if (!empty($assocKey)) { - // You can fetch a key-associated array via the two function parameters assocKey and assocVal - if (empty($assocVal)) { - $rows[$row[$assocKey]] = $row; - } else { - $rows[$row[$assocKey]] = $row[$assocVal]; - } - } else { - $rows[] = $row; - } - } - - if ($debug) { - $fp = @fopen('sqlite.log', 'a'); - fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE RESULT: ' . print_r($rows, true). "\n\n"); - fclose($fp); - } - - if ($single && count($rows) == 1) { - return $rows[0]; - } - - return $rows; - } -} - -/** - * Try to connect to the configured Database (during installation) - * - * @access public - * @param array input configuration array, holding the connection info - * @param array referenced array which holds the errors that might be encountered - * @return boolean return true on success, false on error - */ -function serendipity_db_probe($hash, &$errs) -{ - global $serendipity; - - $dbName = (isset($hash['sqlitedbName']) ? $hash['sqlitedbName'] : $hash['dbName']); - - if (!function_exists('sqlite_open')) { - $errs[] = 'SQLite extension not installed. Run "pear install sqlite" on your webserver or contact your systems administrator regarding this problem.'; - return false; - } - - if (defined('S9Y_DATA_PATH')) { - // Shared installations! - $dbfile = S9Y_DATA_PATH . $dbName . '.db'; - } else { - $dbfile = $serendipity['serendipityPath'] . $dbName . '.db'; - } - - $serendipity['dbConn'] = sqlite_open($dbfile); - - if ($serendipity['dbConn']) { - return true; - } - - $errs[] = "Unable to open \"$dbfile\" - check permissions (directory needs to be writeable for webserver)!"; - return false; -} - -/** - * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. - * - * @access public - * @param string SQL query with template variables to convert - * @return resource SQL resource handle of the executed query - */ -function serendipity_db_schema_import($query) -{ - static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', '{FULLTEXT}', '{BOOLEAN}', '{UTF_8}', '{TEXT}'); - static $replace = array('INTEGER', 'PRIMARY KEY', '', '', 'BOOLEAN NOT NULL', '', 'LONGTEXT'); - - if (stristr($query, '{FULLTEXT_MYSQL}')) { - return true; - } - - $query = trim(str_replace($search, $replace, $query)); - if ($query[0] == '@') { - // Errors are expected to happen (like duplicate index creation) - return serendipity_db_query(substr($query, 1), false, 'both', false, false, false, true); - } else { - return serendipity_db_query($query); - } -} - -/** - * Returns the option to a LIMIT SQL statement, because it varies across DB systems - * - * @access public - * @param int Number of the first row to return data from - * @param int Number of rows to return - * @return string SQL string to pass to a LIMIT statement - */ -function serendipity_db_limit($start, $offset) { - return $start . ', ' . $offset; -} - -/** - * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement - * - * @access public - * @param SQL string of a LIMIT option - * @return SQL string containing a full LIMIT statement - */ -function serendipity_db_limit_sql($limitstring) { - return ' LIMIT ' . $limitstring; -} - -/** - * Returns the SQL code used for concatenating strings - * - * @access public - * @param string Input string/column to concatenate - * @return string SQL parameter - */ -function serendipity_db_concat($string) { - return 'concat(' . $string . ')'; -} - -/* vim: set sts=4 ts=4 expandtab : */ diff --git a/include/db/sqlite3.inc.php b/include/db/sqlite3.inc.php deleted file mode 100644 index 727b1a1f..00000000 --- a/include/db/sqlite3.inc.php +++ /dev/null @@ -1,397 +0,0 @@ - $v) { - // TODO: If a query of the format 'SELECT a.id, b.text FROM table' is used, - // the sqlite extension will give us key indizes 'a.id' and 'b.text' - // instead of just 'id' and 'text' like in mysql/postgresql extension. - // To fix that, we use a preg-regex; but that is quite performance costy. - // Either we always need to use 'SELECT a.id AS id, b.text AS text' in query, - // or the sqlite extension may get fixed. :-) - $row[preg_replace('@^.+\.(.*)@', '\1', $i)] = str_replace($search, $replace, $v); - } - - if ($type == SQLITE3_NUM) - $frow = array(); - else - $frow = $row; - - if ($type != SQLITE3_ASSOC) { - $i = 0; - foreach($row AS $k => $v) { - $frow[$i] = $v; - $i++; - } - } - - return $frow; -} - -/** - * Assemble and return SQL condition for a "IN (...)" clause - * - * @access public - * @param string table column name - * @param array referenced array of values to search for in the "IN (...)" clause - * @param string condition of how to associate the different input values of the $search_ids parameter - * @return string resulting SQL string - */ -function serendipity_db_in_sql($col, &$search_ids, $type = ' OR ') { - $sql = array(); - if (!is_array($search_ids)) { - return false; - } - - foreach($search_ids AS $id) { - $sql[] = $col . ' = ' . $id; - } - - $cond = '(' . implode($type, $sql) . ')'; - return $cond; -} - -/** - * Perform a DB Layer SQL query. - * - * This function returns values dependin on the input parameters and the result of the query. - * It can return: - * false or a string if there was an error (depends on $expectError), - * true if the query succeeded but did not generate any rows - * array of field values if it returned a single row and $single is true - * array of array of field values if it returned row(s) [stacked array] - * - * @access public - * @param string SQL query to execute - * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! - * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) - * @param boolean If true, errors will be reported. If false, errors will be ignored. - * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column - * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. - * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) - * @return mixed Returns the result of the SQL query, depending on the input parameters - */ -function &serendipity_db_query($sql, $single = false, $result_type = "both", $reportErr = true, $assocKey = false, $assocVal = false, $expectError = false) -{ - global $serendipity; - $type_map = array( - 'assoc' => SQLITE3_ASSOC, - 'num' => SQLITE3_NUM, - 'both' => SQLITE3_BOTH, - 'true' => true, - 'false' => false - ); - - static $debug = false; - - if ($debug) { - // Open file and write directly. In case of crashes, the pointer needs to be killed. - $fp = @fopen('sqlite.log', 'a'); - fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE QUERY: ' . $sql . "\n\n"); - fclose($fp); - } - - if ($reportErr && !$expectError) { - $res = sqlite3_query($serendipity['dbConn'], $sql); - } else { - $res = @sqlite3_query($serendipity['dbConn'], $sql); - } - - if (!$res) { - if (!$expectError && !$serendipity['production']) { - var_dump($res); - var_dump($sql); - $msg = "problem with query"; - return $msg; - } - if ($debug) { - $fp = @fopen('sqlite.log', 'a'); - fwrite($fp, '[' . date('d.m.Y H:i') . '] [ERROR] ' . "\n\n"); - fclose($fp); - } - - return $type_map['false']; - } - - if ($res === true) { - return $type_map['true']; - } - - $rows = array(); - - while (($row = serendipity_db_sqlite_fetch_array($res, $type_map[$result_type]))) { - if (!empty($assocKey)) { - // You can fetch a key-associated array via the two function parameters assocKey and assocVal - if (empty($assocVal)) { - $rows[$row[$assocKey]] = $row; - } else { - $rows[$row[$assocKey]] = $row[$assocVal]; - } - } else { - $rows[] = $row; - } - } - - if ($debug) { - $fp = @fopen('sqlite.log', 'a'); - fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE RESULT: ' . print_r($rows, true). "\n\n"); - fclose($fp); - } - - if ($single && count($rows) == 1) { - return $rows[0]; - } - - if (count($rows) == 0) { - if ($single) - return $type_map['false']; - return $type_map['true']; - } - - return $rows; -} - -/** - * Try to connect to the configured Database (during installation) - * - * @access public - * @param array input configuration array, holding the connection info - * @param array referenced array which holds the errors that might be encountered - * @return boolean return true on success, false on error - */ -function serendipity_db_probe($hash, &$errs) -{ - global $serendipity; - - $dbName = (isset($hash['sqlitedbName']) ? $hash['sqlitedbName'] : $hash['dbName']); - - if (!function_exists('sqlite3_open')) { - $errs[] = 'SQLite extension not installed. Run "pear install sqlite" on your webserver or contact your systems administrator regarding this problem.'; - return false; - } - - if (defined('S9Y_DATA_PATH')) { - // Shared installations! - $dbfile = S9Y_DATA_PATH . $dbName . '.db'; - } else { - $dbfile = $serendipity['serendipityPath'] . $dbName . '.db'; - } - - $serendipity['dbConn'] = sqlite3_open($dbfile); - - if ($serendipity['dbConn']) { - return true; - } - - $errs[] = "Unable to open \"$dbfile\" - check permissions (directory needs to be writeable for webserver)!"; - return false; -} - -/** - * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. - * - * @access public - * @param string SQL query with template variables to convert - * @return resource SQL resource handle of the executed query - */ -function serendipity_db_schema_import($query) -{ - static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', '{FULLTEXT}', '{BOOLEAN}', '{UTF_8}', '{TEXT}'); - static $replace = array('INTEGER AUTOINCREMENT', 'PRIMARY KEY', '', '', 'BOOLEAN NOT NULL', '', 'LONGTEXT'); - - if (stristr($query, '{FULLTEXT_MYSQL}')) { - return true; - } - - $query = trim(str_replace($search, $replace, $query)); - $query = str_replace('INTEGER AUTOINCREMENT PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT', $query); - if ($query[0] == '@') { - // Errors are expected to happen (like duplicate index creation) - return serendipity_db_query(substr($query, 1), false, 'both', false, false, false, true); - } else { - return serendipity_db_query($query); - } -} - -/** - * Returns the option to a LIMIT SQL statement, because it varies across DB systems - * - * @access public - * @param int Number of the first row to return data from - * @param int Number of rows to return - * @return string SQL string to pass to a LIMIT statement - */ -function serendipity_db_limit($start, $offset) { - return $start . ', ' . $offset; -} - -/** - * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement - * - * @access public - * @param SQL string of a LIMIT option - * @return SQL string containing a full LIMIT statement - */ -function serendipity_db_limit_sql($limitstring) { - return ' LIMIT ' . $limitstring; -} - -/** - * Returns the SQL code used for concatenating strings - * - * @access public - * @param string Input string/column to concatenate - * @return string SQL parameter - */ -function serendipity_db_concat($string) { - return 'concat(' . $string . ')'; -} - -/* vim: set sts=4 ts=4 expandtab : */ diff --git a/include/db/sqlite3oo.inc.php b/include/db/sqlite3oo.inc.php deleted file mode 100644 index c2772c8e..00000000 --- a/include/db/sqlite3oo.inc.php +++ /dev/null @@ -1,412 +0,0 @@ -changes(); -} - -/** - * Returns the number of updated rows in a SQL query - * - * @access public - * @return int Number of updated rows - */ -function serendipity_db_updated_rows() -{ - global $serendipity; - // It is unknown whether sqllite returns rows MATCHED or rows UPDATED - return $serendipity['dbConn']->changes(); -} - -/** - * Returns the number of matched rows in a SQL query - * - * @access public - * @return int Number of matched rows - */ -function serendipity_db_matched_rows() -{ - global $serendipity; - // It is unknown whether sqllite returns rows MATCHED or rows UPDATED - return $serendipity['dbConn']->changes($serendipity['dbConn']); -} - -/** - * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns - * - * @access public - * @return int Value of the auto-increment column - */ -function serendipity_db_insert_id() -{ - global $serendipity; - - return $serendipity['dbConn']->lastInsertRowID(); -} - -/** - * Parse result arrays into expected format for further operations - * - * SQLite does not support to return "e.entryid" within a $row['entryid'] return. - * So this function manually iteratse through all result rows and rewrites 'X.yyyy' to 'yyyy'. - * Yeah. This sucks. Don't tell me! - * - * @access private - * @param resource The row resource handle - * @param int Bitmask to tell whether to fetch numerical/associative arrays - * @return array Propper array containing the resource results - */ -function serendipity_db_sqlite_fetch_array($res, $type = SQLITE3_BOTH) -{ - static $search = array('%00', '%25'); - static $replace = array("\x00", '%'); - - try { - $row = $res->fetchArray(); - } catch (Exception $e) { - $row = false; - echo "SQLITE-EXCEPTION: " . $e->getMessage() . "\n"; - } - - if (!is_array($row)) { - return $row; - } - - /* strip any slashes, correct fieldname */ - foreach ($row AS $i => $v) { - // TODO: If a query of the format 'SELECT a.id, b.text FROM table' is used, - // the sqlite extension will give us key indizes 'a.id' and 'b.text' - // instead of just 'id' and 'text' like in mysql/postgresql extension. - // To fix that, we use a preg-regex; but that is quite performance costy. - // Either we always need to use 'SELECT a.id AS id, b.text AS text' in query, - // or the sqlite extension may get fixed. :-) - $row[preg_replace('@^.+\.(.*)@', '\1', $i)] = str_replace($search, $replace, $v); - } - - if ($type == SQLITE3_NUM) - $frow = array(); - else - $frow = $row; - - if ($type != SQLITE3_ASSOC) { - $i = 0; - foreach($row AS $k => $v) { - $frow[$i] = $v; - $i++; - } - } - - return $frow; -} - -/** - * Assemble and return SQL condition for a "IN (...)" clause - * - * @access public - * @param string table column name - * @param array referenced array of values to search for in the "IN (...)" clause - * @param string condition of how to associate the different input values of the $search_ids parameter - * @return string resulting SQL string - */ -function serendipity_db_in_sql($col, &$search_ids, $type = ' OR ') { - $sql = array(); - if (!is_array($search_ids)) { - return false; - } - - foreach($search_ids AS $id) { - $sql[] = $col . ' = ' . $id; - } - - $cond = '(' . implode($type, $sql) . ')'; - return $cond; -} - -/** - * Perform a DB Layer SQL query. - * - * This function returns values dependin on the input parameters and the result of the query. - * It can return: - * false or a string if there was an error (depends on $expectError), - * true if the query succeeded but did not generate any rows - * array of field values if it returned a single row and $single is true - * array of array of field values if it returned row(s) [stacked array] - * - * @access public - * @param string SQL query to execute - * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! - * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) - * @param boolean If true, errors will be reported. If false, errors will be ignored. - * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column - * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. - * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) - * @return mixed Returns the result of the SQL query, depending on the input parameters - */ -function &serendipity_db_query($sql, $single = false, $result_type = "both", $reportErr = true, $assocKey = false, $assocVal = false, $expectError = false) -{ - global $serendipity; - $type_map = array( - 'assoc' => SQLITE3_ASSOC, - 'num' => SQLITE3_NUM, - 'both' => SQLITE3_BOTH, - 'true' => true, - 'false' => false - ); - - static $debug = false; - - if ($debug) { - // Open file and write directly. In case of crashes, the pointer needs to be killed. - $fp = @fopen('sqlite.log', 'a'); - fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE QUERY: ' . $sql . "\n\n"); - fclose($fp); - } - - if ($reportErr && !$expectError) { - $res = $serendipity['dbConn']->query($sql); - } else { - $res = @$serendipity['dbConn']->query($sql); - } - - if (!$res) { - if (!$expectError && !$serendipity['production']) { - var_dump($res); - var_dump($sql); - $msg = "problem with query"; - return $msg; - } - if ($debug) { - $fp = @fopen('sqlite.log', 'a'); - fwrite($fp, '[' . date('d.m.Y H:i') . '] [ERROR] ' . "\n\n"); - fclose($fp); - } - - return $type_map['false']; - } - - - if (!preg_match('@^SELECT@imsU', trim($sql))) { - // Everything that is not SELECT will not return rows. - // SQLite3 OO will always return an object though. - if ($serendipity['dbConn']->lastErrorCode() > 0) { - echo "SQLITE-ERROR: " . $serendipity['dbConn']->lastErrorMsg() . "
\n"; - return $type_map['false']; - } else { - return $type_map['true']; - } - } - - $rows = array(); - - while (($row = serendipity_db_sqlite_fetch_array($res, $type_map[$result_type]))) { - if (!empty($assocKey)) { - // You can fetch a key-associated array via the two function parameters assocKey and assocVal - if (empty($assocVal)) { - $rows[$row[$assocKey]] = $row; - } else { - $rows[$row[$assocKey]] = $row[$assocVal]; - } - } else { - $rows[] = $row; - } - } - - if ($debug) { - $fp = @fopen('sqlite.log', 'a'); - fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE RESULT: ' . print_r($rows, true). "\n\n"); - fclose($fp); - } - - if ($single && count($rows) == 1) { - return $rows[0]; - } - - if (count($rows) == 0) { - if ($single) - return $type_map['false']; - return $type_map['true']; - } - - return $rows; -} - -/** - * Try to connect to the configured Database (during installation) - * - * @access public - * @param array input configuration array, holding the connection info - * @param array referenced array which holds the errors that might be encountered - * @return boolean return true on success, false on error - */ -function serendipity_db_probe($hash, &$errs) -{ - global $serendipity; - - $dbName = (isset($hash['sqlitedbName']) ? $hash['sqlitedbName'] : $hash['dbName']); - - if (!class_exists('SQLite3')) { - $errs[] = 'SQLite extension not installed. Available on PHP 5.4+.'; - return false; - } - - if (defined('S9Y_DATA_PATH')) { - // Shared installations! - $dbfile = S9Y_DATA_PATH . $dbName . '.db'; - } else { - $dbfile = $serendipity['serendipityPath'] . $dbName . '.db'; - } - - - $serendipity['dbConn'] = new SQLite3($dbfile); - - if ($serendipity['dbConn']) { - return true; - } - - $errs[] = "Unable to open \"$dbfile\" - check permissions (directory needs to be writeable for webserver)!"; - return false; -} - -/** - * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. - * - * @access public - * @param string SQL query with template variables to convert - * @return resource SQL resource handle of the executed query - */ -function serendipity_db_schema_import($query) -{ - static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', '{FULLTEXT}', '{BOOLEAN}', '{UTF_8}', '{TEXT}'); - static $replace = array('INTEGER AUTOINCREMENT', 'PRIMARY KEY', '', '', 'BOOLEAN NOT NULL', '', 'LONGTEXT'); - - if (stristr($query, '{FULLTEXT_MYSQL}')) { - return true; - } - - $query = trim(str_replace($search, $replace, $query)); - $query = str_replace('INTEGER AUTOINCREMENT PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT', $query); - if ($query[0] == '@') { - // Errors are expected to happen (like duplicate index creation) - return serendipity_db_query(substr($query, 1), false, 'both', false, false, false, true); - } else { - return serendipity_db_query($query); - } -} - -/** - * Returns the option to a LIMIT SQL statement, because it varies across DB systems - * - * @access public - * @param int Number of the first row to return data from - * @param int Number of rows to return - * @return string SQL string to pass to a LIMIT statement - */ -function serendipity_db_limit($start, $offset) { - return $start . ', ' . $offset; -} - -/** - * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement - * - * @access public - * @param SQL string of a LIMIT option - * @return SQL string containing a full LIMIT statement - */ -function serendipity_db_limit_sql($limitstring) { - return ' LIMIT ' . $limitstring; -} - -/** - * Returns the SQL code used for concatenating strings - * - * @access public - * @param string Input string/column to concatenate - * @return string SQL parameter - */ -function serendipity_db_concat($string) { - return 'concat(' . $string . ')'; -} - -/* vim: set sts=4 ts=4 expandtab : */ diff --git a/include/db/sqlrelay.inc.php b/include/db/sqlrelay.inc.php deleted file mode 100644 index f67811b5..00000000 --- a/include/db/sqlrelay.inc.php +++ /dev/null @@ -1,558 +0,0 @@ - sqlr_ASSOC, - 'num' => sqlr_NUM, - 'both' => sqlr_BOTH, - 'true' => true, - 'false' => false - ); - static $benchmark = false; - - // highlight_string(var_export($sql, 1)); - - if (!is_resource($serendipity['dbConn'])) { - return false; - } - - $cur = sqlrcur_alloc($serendipity['dbConn']); - $serendipity['dbCursor'] = $cur; - - if ($benchmark) { - $start = microtime_float(); - } - - if ($expectError) { - $c = sqlrcur_sendQuery($cur, $sql); - } else { - $c = sqlrcur_sendQuery($cur, $sql); - } - - if ($benchmark) { - $end = microtime_float(); - - $cur = sqlrcur_alloc($serendipity['dbConn']); - $sql_b="INSERT INTO BLOGLOG (request, timestamp, sql, exec_time, ip) VALUES ('" . serendipity_db_escape_string($_SERVER['REQUEST_URI']) . "', NOW(), '" . serendipity_db_escape_string($sql) . "', '" . (number_format($end-$start, 10)) . "', '" . serendipity_db_escape_string($_SERVER['REMOTE_ADDR']) . "')"; - $c = sqlrcur_sendQuery($cur, $sql_b); - - $psql = $sql; - $psql = preg_replace('@[0-9]{10}@', 'TIMESTAMP', $psql); - $sql_U="UPDATE BLOGLOG_TOTAL SET counter = counter + 1 WHERE sql = '" . serendipity_db_escape_string($psql) . "'"; - $c = sqlrcur_sendQuery($cur, $sql_U); - if (sqlrcur_affectedRows() < 1) { - $sql_i="INSERT INTO BLOGLOG_TOTAL (sql, counter) VALUES ('" . serendipity_db_escape_string($psql) . "', 1)"; - $c = sqlrcur_sendQuery($cur,$sql_i); - } - } - - if (!$expectError && sqlrcur_errorMessage($cur) != '') { - $msg = '
' . serendipity_specialchars($sql) . '
/ ' . serendipity_specialchars(sqlrcur_errorMessage($cur)); - return $msg; - } - - if (!$c) { - if (!$expectError && !$serendipity['production']) { - print '
' . serendipity_specialchars($sql) . '
/ ' . serendipity_specialchars(sqlrcur_errorMessage($cur)); - if (function_exists('debug_backtrace') && $reportErr == true) { - highlight_string(var_export(debug_backtrace(), 1)); - } - } - - return $type_map['false']; - } - - if ($c === true) { - return $type_map['true']; - } - - $result_type = $type_map[$result_type]; - - switch(sqlrcur_rowCount($cur)) { - case 0: - if ($single) { - return $type_map['false']; - } - return $type_map['true']; - - case 1: - if ($single) { - $row = generate_resultset($cur, $result_type); - if ($result_type != 'sqlr_ASSOC') { - $row=$row[0]; - } - return $row; - } - - default: - if ($single) { - return generate_resultset($cur, $result_type); - } - - $row=generate_resultset($cur); - $rows=array(); - - for($idx=0, $idxc = count($row); $idx < $idxc ; $idx++) { - if (!empty($assocKey)) { - // You can fetch a key-associated array via the two function parameters assocKey and assocVal - if (empty($assocVal)) { - $rows[$assocKey] = $row[$idx][$assocKey]; - } else { - $rows[$row[$idx][$assocKey]] = $row[$idx][$assocVal]; - } - } else { - $rows = $row; - } - } - - return $rows; - - } -} - -/** - * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns - * - * @access public - * @return int Value of the auto-increment column - * - * If you insert multiple rows using a single INSERT statement, - * LAST_INSERT_ID() returns the value generated for the first inserted row only. - */ -function serendipity_db_insert_id($table = '', $id = '') { - global $serendipity; - - $type_of_database = sqlrcon_identify($serendipity['dbConn']); - switch($type_of_database) { - case 'mysql': - $sqlstr='SELECT LAST_INSERT_ID()'; - break; - - case 'postgresql': - $sqlstr = "SELECT currval('{$serendipity['dbPrefix']}{$table}_{$id}_seq'::text) AS {$id}"; - break; - - case 'sqlite': - $sqlstr = 'SELECT last_insert_rowid()'; - break; - - default: - $sqlstr = 'SELECT LAST_INSERT_ID()'; - } - - $row = serendipity_db_query($sqlstr, true); - - return $row[0]; -} - -/** - * Returns the number of affected rows of a SQL query - * - * @access public - * @return int Number of affected rows - */ -function serendipity_db_affected_rows() { - global $serendipity; - - /* int sqlrcur_affectedRows(int sqlrcurref) - * Returns the number of rows that were updated, inserted or deleted by the query. - * Not all databases support this call. - * Don't use it for applications which are designed to be portable across databases. - * -1 is returned by databases which don't support this option. - */ - - return sqlrcur_affectedRows($serendipity['dbCursor']); -} - -/** - * Returns the number of updated rows in a SQL query - * - * @access public - * @return int Number of updated rows - */ -function serendipity_db_updated_rows() { - /* - - preg_match( - "/^[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)/", - mysql_info(), - $arr); - // mysql_affected_rows returns 0 if rows were matched but not changed. - // mysql_info returns rows matched AND rows changed - return $arr[2]; - */ - return serendipity_db_affected_rows(); -} - -/** - * Returns the number of matched rows in a SQL query - * - * @access public - * @return int Number of matched rows - */ -function serendipity_db_matched_rows() { - /* - - preg_match( - "/^[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)/", - mysql_info(), - $arr); - // mysql_affected_rows returns 0 if rows were matched but not changed. - // mysql_info returns rows matched AND rows changed - return $arr[1]; - */ - return serendipity_db_affected_rows(); -} - -/** - * Returns a escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. - * - * @access public - * @param string input string - * @return string output string - */ - -function serendipity_db_escape_string($str) { - global $serendipity; - static $search = array("\x00", '%', "'", '\"'); - static $replace = array('%00', '%25', "''", '\\\"'); - - $type_of_database = sqlrcon_identify($serendipity['dbConn']); - switch($type_of_database) { - case 'mysql': - if (function_exists('mysql_real_escape_string')) { - $rtnstr = mysql_real_escape_string($str, $serendipity['dbConn']); - } else { - $rtnstr = addslashes($str); - } - break; - - case 'postgresql': - if (function_exists('pg_escape_string')) { - $rtnstr = pg_escape_string($str); - } else { - $rtnstr = addslashes($str); - } - break; - - case 'sqlite': - $rtnstr = str_replace($search, $replace, $str); - break; - - default: - $rtnstr = addslashes($str); - } - - return $rtnstr; -} - -/** - * Returns the option to a LIMIT SQL statement, because it varies across DB systems - * - * @access public - * @param int Number of the first row to return data from - * @param int Number of rows to return - * @return string SQL string to pass to a LIMIT statement - */ -function serendipity_db_limit($start, $offset) { - return $start . ', ' . $offset; -} - -/** - * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement - * - * @access public - * @param SQL string of a LIMIT option - * @return SQL string containing a full LIMIT statement - */ -function serendipity_db_limit_sql($limitstring) { - global $serendipity; - - $type_of_database = sqlrcon_identify($serendipity['dbConn']); - switch($type_of_database) { - case "mysql": - return ' LIMIT ' . $limitstring; - case "postgresql": - $limit_split = explode(',', $limitstring); - if (count($limit_split) > 1) { - $limit = ' LIMIT ' . $limit_split[0] . ' OFFSET ' . $limit_split[1]; - } else { - $limit = ' LIMIT ' . $limit_split[0]; - } - return $limit; - default: - return ' LIMIT ' . $limitstring; - } -} - -/** - * Connect to the configured Database - * - * @access public - * @return resource connection handle - */ -function serendipity_db_connect() { - global $serendipity; - - if (isset($serendipity['dbConn'])) { - return $serendipity['dbConn']; - } - - if (isset($serendipity['dbPersistent']) && $serendipity['dbPersistent']) { - $function = 'sqlrcon_alloc'; - } else { - $function = 'sqlrcon_alloc'; - } - - #$serendipity['dbHost']="localhost:9000" - $dbHostPort=explode(":", $serendipity['dbHost']); - $serendipity['dbConn'] = $function($dbHostPort[0], $dbHostPort[1], "", $serendipity['dbUser'], $serendipity['dbPass'], 0, 1); - sqlrcon_debugOff($serendipity['dbConn']); - - if( sqlrcon_identify($serendipity['dbConn']) == "mysql") { - serendipity_db_reconnect(); - } - - return $serendipity['dbConn']; -} - -function serendipity_db_reconnect() { - global $serendipity; - - if (isset($serendipity['dbCharset'])) { - serendipity_db_query("SET NAMES " . $serendipity['dbCharset']); - @define('SQL_CHARSET_INIT', true); - } elseif (defined('SQL_CHARSET') && $serendipity['dbNames'] && !defined('SQL_CHARSET_INIT')) { - serendipity_db_query("SET NAMES " . SQL_CHARSET); - } -} - -/** - * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. - * - * @access public - * @param string SQL query with template variables to convert - * @return resource SQL resource handle of the executed query - */ -function serendipity_db_schema_import($query) { - global $serendipity; - - $type_of_database = sqlrcon_identify($serendipity['dbConn']); - switch($type_of_database) { - case "mysql": - static $search = array('{AUTOINCREMENT}', '{PRIMARY}', - '{UNSIGNED}', '{FULLTEXT}', '{FULLTEXT_MYSQL}', '{BOOLEAN}', '{TEXT}'); - static $replace = array('int(11) not null auto_increment', 'primary key', - 'unsigned' , 'FULLTEXT', 'FULLTEXT', 'enum (\'true\', \'false\') NOT NULL default \'true\'', 'LONGTEXT'); - static $is_utf8 = null; - - if ($is_utf8 === null) { - $search[] = '{UTF_8}'; - if ((isset($_POST['charset']) && $_POST['charset'] == 'UTF-8/') || - $serendipity['charset'] == 'UTF-8/' || - $serendipity['POST']['charset'] == 'UTF-8/' || - LANG_CHARSET == 'UTF-8' ) { - $replace[] = '/*!40100 CHARACTER SET utf8 COLLATE utf8_unicode_ci */'; - } else { - $replace[] = ''; - } - } - break; - - case "postgresql": - static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', - '{FULLTEXT}', '{FULLTEXT_MYSQL}', '{BOOLEAN}', 'int(1)', 'int(10)', 'int(11)', 'int(4)', '{UTF_8}', '{TEXT}'); - static $replace = array('SERIAL', 'primary key', '', - '', '', 'BOOLEAN NOT NULL', 'int2', 'int4', 'int4', 'int4', '', 'text'); - break; - - case "sqlite": - static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', '{FULLTEXT}', '{FULLTEXT_MYSQL}', '{BOOLEAN}', '{UTF_8}', '{TEXT}'); - static $replace = array('INTEGER', 'PRIMARY KEY', '', '', '', 'BOOLEAN NOT NULL', '', 'LONGTEXT'); - break; - - default: - return true; - } - - $query = trim(str_replace($search, $replace, $query)); - - if ($query{0} == '@') { - // Errors are expected to happen (like duplicate index creation) - return serendipity_db_query(substr($query, 1), false, 'both', false, false, false, true); - } else { - return serendipity_db_query($query); - } -} - -/** - * Try to connect to the configured Database (during installation) - * - * @access public - * @param array input configuration array, holding the connection info - * @param array referenced array which holds the errors that might be encountered - * @return boolean return true on success, false on error - */ -function serendipity_db_probe($hash, &$errs) { - global $serendipity; - $c = null; - - if (!function_exists('sqlrcon_alloc')) { - $errs[] = 'No sql_relay extension found. Please check your webserver installation or contact your systems administrator regarding this problem.'; - return false; - } - - if (!($c=@sqlrcon_alloc($hash['dbHost'], '', '', $hash['dbUser'], $hash['dbPass'], 0, 5))) { - $errs[] = 'Could not connect to database; check your settings.'; - return false; - } - - - $serendipity['dbConn'] = $c; - - /* which db should be used is defined in sqlrelay.conf */ - - return true; -} - -/** - * Returns the SQL code used for concatenating strings - * - * @access public - * @param string Input string/column to concatenate - * @return string SQL parameter - */ -function serendipity_db_concat($string) { - global $serendipity; - - $type_of_database = sqlrcon_identify($serendipity['dbConn']); - switch($type_of_database) { - case 'mysql': - return 'concat(' . $string . ')'; - - case 'postgresql': - return '(' . str_replace(', ', '||', $string) . ')'; - - case 'sqlite': - return 'concat(' . $string . ')'; - - default: - return 'concat(' . $string . ')'; - } -} - -/* vim: set sts=4 ts=4 expandtab : */ diff --git a/lib/Serendipity/Database/DbAbstract.php b/lib/Serendipity/Database/DbAbstract.php new file mode 100644 index 00000000..a76465c1 --- /dev/null +++ b/lib/Serendipity/Database/DbAbstract.php @@ -0,0 +1,362 @@ +serendipity =& $serendipity; + $this->db_type = $serendipity['dbType']; + $this->db_hostname = $serendipity['dbHost']; + $this->db_username = $serendipity['dbUser']; + $this->db_password = $serendipity['dbPass']; + $this->db_name = $serendipity['dbName']; + $this->db_prefix = $serendipity['dbPrefix']; + } + + public function connect() + { + } + + public function reconnect() + { + } + + public function beginTransaction() + { + } + + public function endTransaction(bool $commit) + { + } + + /** + * Returns an escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. + * + * @access public + * @param string input string + * @return string output string + */ + public function escapeString($string) + { + return $string; + } + + /** + * Assemble and return SQL condition for a "IN (...)" clause + * + * @access public + * @param string table column name + * @param array referenced array of values to search for in the "IN (...)" clause + * @param string condition of how to associate the different input values of the $search_ids parameter + * @return string resulting SQL string + */ + public function inSql($col, &$searchIds, $type = ' OR ') + { + } + + /** + * Returns the option to a LIMIT SQL statement, because it varies across DB systems + * + * @access public + * @param int Number of the first row to return data from + * @param int Number of rows to return + * @return string SQL string to pass to a LIMIT statement + */ + public function limit($start, $offset) + { + } + + /** + * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement + * + * @access public + * @param SQL string of a LIMIT option + * @return SQL string containing a full LIMIT statement + */ + public function limitSql($limitstring) + { + } + + /** + * Returns the number of affected rows of a SQL query + * + * @access public + * @return int Number of affected rows + */ + public function affectedRows() + { + } + + /** + * Returns the number of updated rows in a SQL query + * + * @access public + * @return int Number of updated rows + */ + public function updatedRows() + { + } + + /** + * Returns the number of matched rows in a SQL query + * + * @access public + * @return int Number of matched rows + */ + public function matchedRows() + { + } + + /** + * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns + * + * @access public + * @param string Name of the table to get a INSERT ID for + * @param string Name of the column to get a INSERT ID for + * @return int Value of the auto-increment column + */ + public function insertId($table = '', $id = '') + { + } + + /** + * Perform a query to update the data of a certain table row + * + * You can pass the tablename and an array of keys to select the row, + * and an array of values to UPDATE in the DB table. + * + * @access public + * @param string Name of the DB table + * @param array Input array that controls the "WHERE" condition part. Pass it an associative array like array('key1' => 'value1', 'key2' => 'value2') to get a statement like "WHERE key1 = value1 AND key2 = value2". Escaping is done automatically in this function. + * @param array Input array that controls the "SET" condition part. Pass it an associative array like array('key1' => 'value1', 'key2' => 'value2') to get a statement like "SET key1 = value1, key2 = value2". Escaping is done automatically in this function. + * @param string What do do with the SQL query (execute, display) + * @return array Returns the result of the SQL query + */ + public function update($table, $keys, $values, $action = 'execute') + { + $set = ''; + + foreach ($values as $k => $v) { + if (strlen($set)) { + $set .= ', '; + } + $set .= $k . '=\'' . $this->escapeString($v) . '\''; + } + + $where = ''; + foreach ($keys as $k => $v) { + if (strlen($where)) { + $where .= ' AND '; + } + $where .= $k . '=\'' . $this->escapeString($v) . '\''; + } + + if (strlen($where)) { + $where = " WHERE $where"; + } + + $q = "UPDATE {$this->db_prefix}$table SET $set $where"; + if ($action == 'execute') { + return $this->query($q); + } else { + return $q; + } + } + + /** + * Perform a DB Layer SQL query. + * + * This function returns values depending on the input parameters and the result of the query. + * It can return: + * false or a string if there was an error (depends on $expectError), + * true if the query succeeded but did not generate any rows + * array of field values if it returned a single row and $single is true + * array of array of field values if it returned row(s) [stacked array] + * + * @access public + * @param string SQL query to execute + * @param bool Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! + * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) + * @param bool If true, errors will be reported. If false, errors will be ignored. + * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column + * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. + * @param bool If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignored on DUPLICATE INDEX queries and the likes) + * @return mixed Returns the result of the SQL query, depending on the input parameters + */ + public function &query($sql, $single = false, $result_type = "both", $reportErr = false, $assocKey = false, $assocVal = false, $expectError = false) + { + } + + /** + * Perform a query to insert an associative array into a specific SQL table + * + * You can pass a tablename and an array of input data to insert into an array. + * + * @access public + * @param string Name of the SQL table + * @param array Associative array of keys/values to insert into the table. Escaping is done automatically. + * @param string What do do with the SQL query (execute, display) + * @return array Returns the result of the SQL query + */ + public function insert($table, $values, $action = 'execute') + { + $names = implode(',', array_keys($values)); + + $vals = ''; + foreach ($values as $k => $v) { + if (strlen($vals)) { + $vals .= ', '; + } + $vals .= '\'' . $this->escapeString($v) . '\''; + } + + $q = "INSERT INTO {$this->db_prefix}$table ($names) values ($vals)"; + + if ($action == 'execute') { + return $this->query($q); + } else { + return $q; + } + } + + /** + * Check whether an input value corresponds to a TRUE/FALSE option in the SQL database. + * + * Because older DBs could not store TRUE/FALSE values to be restored into a PHP variable, + * this function tries to detect what the return code of a SQL column is, and convert it + * to a PHP native boolean. + * + * Values that will be recognized as TRUE are 'true', 't' and '1'. + * + * @access public + * @param string input value to compare + * @return boolean boolean conversion of the input value + */ + public function bool($val) + { + if (($val === true) || ($val == 'true') || ($val == 't') || ($val == '1')) { + return true; + } + #elseif (($val === false || $val == 'false' || $val == 'f')) + return false; + } + + /** + * Prepares a Serendipity query input to fully valid SQL. Replaces certain "template" variables. + * + * @access public + * @param string SQL query with template variables to convert + * @return resource SQL resource handle of the executed query + */ + public function schemaImport($query) + { + } + + /** + * Return a SQL statement for a time interval or timestamp, specific to certain SQL backends + * + * @access public + * @param string Indicate whether to return a timestamp, or an Interval + * @param int The interval one might want to use, if Interval return was selected + * @return string SQL statement + */ + public function getInterval($val, $ival = 900) + { + switch ($this->db_type) { + case 'sqlite': + case 'sqlite3': + case 'sqlite3oo': + case 'pdo-sqlite': + $interval = $ival; + $ts = time(); + break; + + case 'pdo-postgres': + case 'postgres': + $interval = "interval '$ival'"; + $ts = 'NOW()'; + break; + + case 'mysql': + case 'mysqli': + default: + $interval = $ival; + $ts = 'NOW()'; + break; + } + + switch ($val) { + case 'interval': + return $interval; + + default: + case 'ts': + return $ts; + } + } + + /** + * Operates on an array to prepare it for SQL usage. + * + * @access public + * @param string Concatenation character + * @param array Input array + * @param string How to convert (int: Only numbers, string: serendipity_db_escape_String) + * @return string Imploded string + */ + public function implode($string, &$array, $type = 'int') + { + $new_array = array(); + if (!is_array($array)) { + return ''; + } + + foreach ($array as $idx => $key) { + if ($type == 'int') { + $new_array[$idx] = (int)$key; + } else { + $new_array[$idx] = $this->escapeString($key); + } + } + + $string = implode($string, $new_array); + return $string; + } + + /** + * Returns the SQL code used for concatenating strings + * + * @access public + * @param string Input string/column to concatenate + * @return string SQL parameter + */ + public function concat($string) + { + } + + /** + * Try to connect to the configured Database (during installation) + * + * @access public + * @param array input configuration array, holding the connection info + * @param array referenced array which holds the errors that might be encountered + * @return boolean return true on success, false on error + */ + public function probe($hash, &$errs) + { + } +} diff --git a/lib/Serendipity/Database/DbFactory.php b/lib/Serendipity/Database/DbFactory.php new file mode 100644 index 00000000..f96566d1 --- /dev/null +++ b/lib/Serendipity/Database/DbFactory.php @@ -0,0 +1,43 @@ + 'Mysqli', + 'mysqli' => 'Mysqli', + 'pdo-postgres' => 'PdoPostgres', + 'pdo-sqlite' => 'PdoSqlite', + 'postgres' => 'Postgres', + 'sqlite' => 'Sqlite', + 'sqlite3' => 'Sqlite3', + 'sqlite3oo' => 'Sqlite3oo', + 'sqlrelay' => 'SqlRelay', + ]; + + if (!array_key_exists($serendipity['dbType'], $config2class)) { + throw new Exception('Database type "' . $serendipity['dbType'] . '" not supported!'); + } + + // Name of database class + $dbClass = '\\Serendipity\\Database\\' . $config2class[$serendipity['dbType']] . 'Database'; + + self::$db_instance = new $dbClass($serendipity); + return self::$db_instance; + } +} diff --git a/lib/Serendipity/Database/MysqliDatabase.php b/lib/Serendipity/Database/MysqliDatabase.php new file mode 100644 index 00000000..e7a55eea --- /dev/null +++ b/lib/Serendipity/Database/MysqliDatabase.php @@ -0,0 +1,499 @@ +query('start transaction'); + } + + /** + * Tells the DB Layer to end a DB transaction. + * + * @access public + * @param boolean If true, perform the query. If false, rollback. + */ + public function endTransaction($commit) + { + if ($commit) { + $this->query('commit'); + }else{ + $this->query('rollback'); + } + } + + /** + * Assemble and return SQL condition for a "IN (...)" clause + * + * @access public + * @param string table column name + * @param array referenced array of values to search for in the "IN (...)" clause + * @param string condition of how to associate the different input values of the $search_ids parameter + * @return string resulting SQL string + */ + public function inSql($col, &$search_ids, $type = ' OR ') + { + return $col . " IN (" . implode(', ', $search_ids) . ")"; + } + + /** + * Perform a DB Layer SQL query. + * + * This function returns values dependin on the input parameters and the result of the query. + * It can return: + * false or a string if there was an error (depends on $expectError), + * true if the query succeeded but did not generate any rows + * array of field values if it returned a single row and $single is true + * array of array of field values if it returned row(s) [stacked array] + * + * @access public + * @param string SQL query to execute + * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! + * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) + * @param boolean If true, errors will be reported. If false, errors will be ignored. + * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column + * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. + * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignored on DUPLICATE INDEX queries and the likes) + * @return mixed Returns the result of the SQL query, depending on the input parameters + */ + public function &query($sql, $single = false, $result_type = "both", $reportErr = false, $assocKey = false, $assocVal = false, $expectError = false) + { + $type_map = array( + 'assoc' => MYSQLI_ASSOC, + 'num' => MYSQLI_NUM, + 'both' => MYSQLI_BOTH, + 'true' => true, + 'false' => false + ); + + if ($expectError) { + $c = @mysqli_query($this->db_conn, $sql); + } else { + $c = mysqli_query($this->db_conn, $sql); + } + + if (!$expectError && mysqli_error($this->db_conn) != '') { + $msg = mysqli_error($this->db_conn); + return $msg; + } + + if (!$c) { + if (!$expectError && !$this->serendipity['production']) { + print mysqli_error($this->db_conn); + if (function_exists('debug_backtrace') && $reportErr == true) { + highlight_string(var_export(debug_backtrace(), 1)); + } + } + + return $type_map['false']; + } + + if ($c === true) { + return $type_map['true']; + } + + $result_type = $type_map[$result_type]; + + switch(mysqli_num_rows($c)) { + case 0: + if ($single) { + return $type_map['false']; + } + return $type_map['true']; + case 1: + if ($single) { + return mysqli_fetch_array($c, $result_type); + } + default: + if ($single) { + return mysqli_fetch_array($c, $result_type); + } + + $rows = array(); + while ($row = mysqli_fetch_array($c, $result_type)) { + if (!empty($assocKey)) { + // You can fetch a key-associated array via the two function parameters assocKey and assocVal + if (empty($assocVal)) { + $rows[$row[$assocKey]] = $row; + } else { + $rows[$row[$assocKey]] = $row[$assocVal]; + } + } else { + $rows[] = $row; + } + } + return $rows; + } + } + + /** + * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns + * + * @access public + * @return int Value of the auto-increment column + */ + public function insertId() + { + return mysqli_insert_id($this->db_conn); + } + + /** + * Returns the number of affected rows of a SQL query + * + * @access public + * @return int Number of affected rows + */ + public function affectedRows() + { + return mysqli_affected_rows($this->db_conn); + } + + /** + * Returns the number of updated rows in a SQL query + * + * @access public + * @return int Number of updated rows + */ + public function updatedRows() + { + preg_match( + "/^[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)/", + mysqli_info($this->db_conn), + $arr + ); + // mysqli_affected_rows returns 0 if rows were matched but not changed. + // mysqli_info returns rows matched + return $arr[1]; + } + + /** + * Returns the number of matched rows in a SQL query + * + * @access public + * @return int Number of matched rows + */ + public function matchedRows() + { + preg_match( + "/^[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)/", + mysqli_info($this->db_conn), + $arr + ); + // mysqli_affected_rows returns 0 if rows were matched but not changed. + // mysqli_info returns rows matched + return $arr[0]; + } + + /** + * Returns a escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. + * + * @access public + * @param string input string + * @return string output string + */ + public function escapeString($string) + { + return mysqli_escape_string($this->db_conn, $string); + } + + /** + * Returns the option to a LIMIT SQL statement, because it varies across DB systems + * + * @access public + * @param int Number of the first row to return data from + * @param int Number of rows to return + * @return string SQL string to pass to a LIMIT statement + */ + public function limit($start, $offset) + { + return $start . ', ' . $offset; + } + + /** + * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement + * + * @access public + * @param SQL string of a LIMIT option + * @return SQL string containing a full LIMIT statement + */ + public function limitSql($limitstring) + { + return ' LIMIT ' . $limitstring; + } + + /** + * Connect to the configured Database + * + * @access public + * @return resource connection handle + */ + public function connect() + { + if (isset($this->db_conn)) { + return $this->db_conn; + } + + $function = 'mysqli_connect'; + + $connparts = explode(':', $this->db_hostname); + if (!empty($connparts[1])) { + // A "hostname:port" connection was specified + $this->db_conn = $function($connparts[0], $this->db_username, $this->db_password, $this->db_name, $connparts[1]); + } else { + // Connect with default ports + $this->db_conn = $function($connparts[0], $this->db_username, $this->db_password); + } + mysqli_select_db($this->db_conn, $this->db_name); + $this->reconnect(); + + return $this->db_conn; + } + + public function reconnect() + { + $use_charset = ''; + if (isset($this->serendipity['dbCharset']) && !empty($this->serendipity['dbCharset'])) { + $use_charset = $this->serendipity['dbCharset']; + if (!defined('SQL_CHARSET_INIT')) { define('SQL_CHARSET_INIT', true); } + } elseif (defined('SQL_CHARSET') && $this->serendipity['dbNames'] && !defined('SQL_CHARSET_INIT')) { + $use_charset = SQL_CHARSET; + } + + if ($use_charset != '') { + mysqli_set_charset($this->db_conn, $use_charset); + } + } + + /** + * Prepares a Serendipity query input to fully valid SQL. Replaces certain "template" variables. + * + * @access public + * @param string SQL query with template variables to convert + * @return resource SQL resource handle of the executed query + */ + public function schemaImport($query) + { + static $search = array('{AUTOINCREMENT}', '{PRIMARY}', + '{UNSIGNED}', '{FULLTEXT}', '{FULLTEXT_MYSQL}', '{BOOLEAN}', '{TEXT}'); + static $replace = array('int(11) not null auto_increment', 'primary key', + 'unsigned' , 'FULLTEXT', 'FULLTEXT', 'enum (\'true\', \'false\') NOT NULL default \'true\'', 'LONGTEXT'); + + $search[] = '{UTF_8}'; + if ( $_POST['charset'] == 'UTF-8/' || + $this->serendipity['charset'] == 'UTF-8/' || + $this->serendipity['POST']['charset'] == 'UTF-8/' || + LANG_CHARSET == 'UTF-8' ) { + if ($this->isUtf8mb4Ready()) { + $replace[] = 'ROW_FORMAT=DYNAMIC /*!40100 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */'; + } else { + # in old versions we stick to the three byte pseudo utf8 to not trip + # over the max index restriction of 1000 bytes + $replace[] = '/*!40100 CHARACTER SET utf8 COLLATE utf8_unicode_ci */'; + } + } else { + $replace[] = ''; + } + + if ($this->isUtf8mb4Ready()) { + # InnoDB enables us to use utf8mb4 with the higher max index size + $this->query("SET storage_engine=INNODB"); + } else { + # Before 5.6.4/10.0.5 InnoDB did not support fulltext indexes, which we use, + # thus we stay with MyISAM here + $this->query("SET storage_engine=MYISAM"); + } + + $query = trim(str_replace($search, $replace, $query)); + if ($query[0] == '@') { + // Errors are expected to happen (like duplicate index creation) + return $this->query(substr($query, 1), false, 'both', false, false, false, true); + } else { + return $this->query($query); + } + } + + /** + * Check if we think that it is safe to ugprade to utf8mb4. This checks version numbers and applied settings. + * Depending on the version of mariadb/mysql we need to check either one or three settings. We check for + * innodb being available with fulltext index and large index support, so that our database scheme can work + * + * @return boolean Whether the database could support utf8mb4 + */ + protected function isUtf8mb4Ready() + { + $mysql_version = mysqli_get_server_info($this->db_conn); + $maria = false; + if (strpos($mysql_version, 'MariaDB') !== false) { + $maria = true; + } + if ($maria) { + # maria trips up our version detection scheme by prepending 5.5.5 to newer versions + $mysql_version = str_replace('5.5.5-', '', $mysql_version); + } + + if ($maria) { + # see https://mariadb.com/kb/en/innodb-file-format/ for when barracuda is available, and when it's the only option + # see https://docs.nextcloud.com/server/15/admin_manual/configuration_database/mysql_4byte_support.html for which + # variables we have to check to assume utf8mb4 it can work (with the large indexes we need) + + if (version_compare($mysql_version, '10.3.1', '>=')) { + # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_file_per_table + $rows = $this->query("SHOW VARIABLES LIKE 'innodb_file_per_table'"); + try { + return $rows[0][1] == 'ON'; + } catch (Exception $e) { + return false; + } + } + + # see https://mariadb.com/kb/en/full-text-index-overview/. We need 10.0.5 to have fulltext indexes with innodb + if (version_compare($mysql_version, '10.0.5', '>=')) { + # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_file_per_table + $rows = $this->query("SHOW VARIABLES LIKE 'innodb_file_per_table'"); + try { + if ($rows[0][1] != 'ON') { + return false; + } + } catch (Exception $e) { + return false; + } + + # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_file_format + $rows = $this->query("SHOW VARIABLES LIKE 'innodb_file_format'"); + try { + if ($rows[0][1] != 'barracuda') { + return false; + } + } catch (Exception $e) { + return false; + } + + # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_large_prefix + $rows = $this->query("SHOW VARIABLES LIKE 'innodb_large_prefix'"); + try { + if ($rows[0][1] != 'ON') { + return false; + } + } catch (Exception $e) { + return false; + } + } + return false; # version too old + } + + # now we know it is not mariadb, but "real" mysql + + # These versions might need to be changed based on testing feedback + if (version_compare($mysql_version, '8.0.0', '>=')) { + $rows = $this->query("SHOW VARIABLES LIKE 'innodb_file_per_table'"); + try { + return $rows[0][1] == 'ON'; + } catch (Exception $e) { + return false; + } + } + + # see https://dev.mysql.com/doc/refman/5.6/en/innodb-fulltext-index.html. We need 5.6 for fulltext indexes + if (version_compare($mysql_version, '5.6', '>=')) { + # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_file_per_table + $rows = $this->query("SHOW VARIABLES LIKE 'innodb_file_per_table'"); + try { + if ($rows[0][1] != 'ON') { + return false; + } + } catch (Exception $e) { + return false; + } + + # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_file_format + $rows = $this->query("SHOW VARIABLES LIKE 'innodb_file_format'"); + try { + if ($rows[0][1] != 'barracuda') { + return false; + } + } catch (Exception $e) { + return false; + } + + # see https://mariadb.com/kb/en/innodb-system-variables/#innodb_large_prefix + $rows = $this->query("SHOW VARIABLES LIKE 'innodb_large_prefix'"); + try { + if ($rows[0][1] != 'ON') { + return false; + } + } catch (Exception $e) { + return false; + } + return true; + } + + return false; # version too old + } + + /** + * Try to connect to the configured Database (during installation) + * + * @access public + * @param array input configuration array, holding the connection info + * @param array referenced array which holds the errors that might be encountered + * @return boolean return true on success, false on error + */ + public function probe($hash, &$errs) + { + if (!function_exists('mysqli_connect')) { + $errs[] = 'No mySQLi extension found. Please check your webserver installation or contact your systems administrator regarding this problem.'; + return false; + } + + $function = 'mysqli_connect'; + $connparts = explode(':', $hash['dbHost']); + if (!empty($connparts[1])) { + // A "hostname:port" connection was specified + $c = @$function($connparts[0], $hash['dbUser'], $hash['dbPass'], $hash['dbName'], $connparts[1]); + } else { + // Connect with default ports + $c = @$function($connparts[0], $hash['dbUser'], $hash['dbPass']); + } + + if (!$c) { + $errs[] = 'Could not connect to database; check your settings.'; + $errs[] = 'The mySQL error was: ' . serendipity_specialchars(mysqli_connect_error()); + return false; + } + + $this->db_conn = $c; + + if ( !@mysqli_select_db($c, $hash['dbName']) ) { + $errs[] = 'The database you specified does not exist.'; + $errs[] = 'The mySQL error was: ' . serendipity_specialchars(mysqli_error($c)); + return false; + } + + return true; + } + + /** + * Returns the SQL code used for concatenating strings + * + * @access public + * @param string Input string/column to concatenate + * @return string SQL parameter + */ + public function concat($string) + { + return 'concat(' . $string . ')'; + } +} diff --git a/lib/Serendipity/Database/PdoPostgresDatabase.php b/lib/Serendipity/Database/PdoPostgresDatabase.php new file mode 100644 index 00000000..d3ed9ca0 --- /dev/null +++ b/lib/Serendipity/Database/PdoPostgresDatabase.php @@ -0,0 +1,342 @@ +db_conn->beginTransaction(); + } + + /** + * Tells the DB Layer to end a DB transaction. + * + * @access public + * @param boolean If true, perform the query. If false, rollback. + */ + public function endTransaction($commit) + { + if ($commit) { + $this->db_conn->commit(); + } else { + $this->db_conn->rollback(); + } + } + + /** + * Assemble and return SQL condition for a "IN (...)" clause + * + * @access public + * @param string table column name + * @param array referenced array of values to search for in the "IN (...)" clause + * @param string condition of how to associate the different input values of the $search_ids parameter + * @return string resulting SQL string + */ + public function inSql($col, &$search_ids, $type = ' OR ') + { + return $col . " IN (" . implode(', ', $search_ids) . ")"; + } + + /** + * Connect to the configured Database + * + * @access public + * @return resource connection handle + */ + public function connect() + { + $host = $port = ''; + if (strlen($this->db_hostname)) { + if (false !== strstr($this->db_hostname, ':')) { + $tmp = explode(':', $this->db_hostname); + $host = "host={$tmp[0]};"; + $port = "port={$tmp[1]};"; + } else { + $host = "host={$this->db_hostname};"; + } + } + + $this->db_conn = new \PDO( + sprintf( + 'pgsql:%sdbname=%s', + "$host$port", + $this->db_name + ), + $this->db_username, + $this->db_password + ); + + return $this->db_conn; + } + + /** + * Returns a escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. + * + * @access public + * @param string input string + * @return string output string + */ + public function escapeString($string) + { + return substr($this->db_conn->quote($string), 1, -1); + } + + /** + * Returns the option to a LIMIT SQL statement, because it varies across DB systems + * + * @access public + * @param int Number of the first row to return data from + * @param int Number of rows to return + * @return string SQL string to pass to a LIMIT statement + */ + public function limit($start, $offset) + { + return $offset . ', ' . $start; + } + + /** + * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement + * + * @access public + * @param SQL string of a LIMIT option + * @return SQL string containing a full LIMIT statement + */ + public function limitSql($limitstring) + { + $limit_split = explode(',', $limitstring); + if (count($limit_split) > 1) { + $limit = ' LIMIT ' . $limit_split[0] . ' OFFSET ' . $limit_split[1]; + } else { + $limit = ' LIMIT ' . $limit_split[0]; + } + return $limit; + } + + /** + * Returns the number of affected rows of a SQL query + * + * @access public + * @return int Number of affected rows + */ + public function affectedRows() + { + return $this->serendipity['dbSth']->rowCount(); + } + + /** + * Returns the number of updated rows in a SQL query + * + * @access public + * @return int Number of updated rows + */ + public function updatedRows() + { + // it is unknown whether pg_affected_rows returns number of rows + // UPDATED or MATCHED on an UPDATE statement. + return $this->serendipity['dbSth']->rowCount(); + } + + /** + * Returns the number of matched rows in a SQL query + * + * @access public + * @return int Number of matched rows + */ + public function matchedRows() + { + // it is unknown whether pg_affected_rows returns number of rows + // UPDATED or MATCHED on an UPDATE statement. + return $this->serendipity['dbSth']->rowCount(); + } + + /** + * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns + * + * @access public + * @param string Name of the table to get a INSERT ID for + * @param string Name of the column to get a INSERT ID for + * @return int Value of the auto-increment column + */ + public function insertId($table = '', $id = '') + { + if (empty($table) || empty($id)) { + // BC - will/should never be called with empty parameters! + return $this->db_conn->lastInsertId(); + } else { + $query = "SELECT currval('{$this->db_prefix}{$table}_{$id}_seq'::text) AS {$id}"; + $res = $this->db_conn->prepare($query); + $res->execute(); + foreach ($res->fetchAll(PDO::FETCH_ASSOC) as $row) { + return $row[$id]; + } + return $this->db_conn->lastInsertId(); + } + } + + /** + * Perform a DB Layer SQL query. + * + * This function returns values dependin on the input parameters and the result of the query. + * It can return: + * false or a string if there was an error (depends on $expectError), + * true if the query succeeded but did not generate any rows + * array of field values if it returned a single row and $single is true + * array of array of field values if it returned row(s) [stacked array] + * + * @access public + * @param string SQL query to execute + * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! + * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) + * @param boolean If true, errors will be reported. If false, errors will be ignored. + * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column + * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. + * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) + * @return mixed Returns the result of the SQL query, depending on the input parameters + */ + public function &query($sql, $single = false, $result_type = "both", $reportErr = false, $assocKey = false, $assocVal = false, $expectError = false) + { + $type_map = array( + 'assoc' => PDO::FETCH_ASSOC, + 'num' => PDO::FETCH_NUM, + 'both' => PDO::FETCH_BOTH, + 'true' => true, + 'false' => false + ); + + if (!$expectError && ($reportErr || !$this->serendipity['production'])) { + $this->serendipity['dbSth'] = $this->db_conn->prepare($sql); + } else { + $this->serendipity['dbSth'] = $this->db_conn->prepare($sql); + } + + if (!$this->serendipity['dbSth']) { + if (!$expectError && !$this->serendipity['production']) { + print "Error in $sql"; + print $this->db_conn->errorInfo() . "
\n"; + if (function_exists('debug_backtrace')) { + highlight_string(var_export(debug_backtrace(), 1)); + } + print "
$sql
\n"; + } + return $type_map['false']; + } + + $this->serendipity['dbSth']->execute(); + + if ($this->serendipity['dbSth'] === true) { + return $type_map['true']; + } + + $result_type = $type_map[$result_type]; + + $n = 0; + + $rows = array(); + foreach ($this->serendipity['dbSth']->fetchAll($result_type) as $row) { + if (!empty($assocKey)) { + // You can fetch a key-associated array via the two function parameters assocKey and assocVal + if (empty($assocVal)) { + $rows[$row[$assocKey]] = $row; + } else { + $rows[$row[$assocKey]] = $row[$assocVal]; + } + } else { + $rows[] = $row; + } + } + if (count($rows) == 0) { + if ($single) { + return $type_map['false']; + } + return $type_map['true']; + } + if (count($rows) == 1 && $single) { + return $rows[0]; + } + return $rows; + } + + /** + * Prepares a Serendipity query input to fully valid SQL. Replaces certain "template" variables. + * + * @access public + * @param string SQL query with template variables to convert + * @return resource SQL resource handle of the executed query + */ + public function schemaImport($query) + { + static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', + '{FULLTEXT}', '{BOOLEAN}', 'int(1)', 'int(10)', 'int(11)', 'int(4)', '{UTF_8}', '{TEXT}'); + static $replace = array('SERIAL', 'primary key', '', '', 'BOOLEAN NOT NULL', 'int2', + 'int4', 'int4', 'int4', '', 'text'); + + if (stristr($query, '{FULLTEXT_MYSQL}')) { + return true; + } + + $query = trim(str_replace($search, $replace, $query)); + if ($query[0] == '@') { + // Errors are expected to happen (like duplicate index creation) + return $this->query(substr($query, 1), false, 'both', false, false, false, true); + } else { + return $this->query($query); + } + } + + /** + * Try to connect to the configured Database (during installation) + * + * @access public + * @param array input configuration array, holding the connection info + * @param array referenced array which holds the errors that might be encountered + * @return boolean return true on success, false on error + */ + public function probe($hash, &$errs) + { + if (!in_array('pgsql', PDO::getAvailableDrivers())) { + $errs[] = 'PDO_PGSQL driver not avialable'; + return false; + } + + $this->db_conn = new PDO( + sprintf( + 'pgsql:%sdbname=%s', + strlen($hash['dbHost']) ? ('host=' . $hash['dbHost'] . ';') : '', + $hash['dbName'] + ), + $hash['dbUser'], + $hash['dbPass'] + ); + + if (!$this->db_conn) { + $errs[] = 'Could not connect to database; check your settings.'; + return false; + } + + return true; + } + + /** + * Returns the SQL code used for concatenating strings + * + * @access public + * @param string Input string/column to concatenate + * @return string SQL parameter + */ + public function concat($string) + { + return '(' . str_replace(', ', '||', $string) . ')'; + } +} diff --git a/lib/Serendipity/Database/PdoSqliteDatabase.php b/lib/Serendipity/Database/PdoSqliteDatabase.php new file mode 100644 index 00000000..1558b374 --- /dev/null +++ b/lib/Serendipity/Database/PdoSqliteDatabase.php @@ -0,0 +1,368 @@ +db_conn = new PDO( + 'sqlite:' . (defined('S9Y_DATA_PATH') ? S9Y_DATA_PATH : $this->serendipity['serendipityPath']) . $this->db_name . '.db' + ); + + return $this->db_conn; + } + + // TODO: Replace with proper Logging object + public function logmsg($msgstr) + { + $fp = @fopen('sqlite.log', 'a'); + fwrite($fp, '[' . date('d.m.Y H:i') . '] ' . $msgstr . "\n\n"); + fclose($fp); + } + + /** + * Parse result arrays into expected format for further operations + * + * SQLite does not support to return "e.entryid" within a $row['entryid'] return. + * So this function manually iterate through all result rows and rewrites 'X.yyyy' to 'yyyy'. + * Yeah. This sucks. Don't tell me! + * + * @access private + * @param resource The row resource handle + * @param int Bitmask to tell whether to fetch numerical/associative arrays + * @return array Propper array containing the resource results + */ + protected function fetchArray($row, $type = PDO::FETCH_ASSOC) + { + static $search = array('%00', '%25'); + static $replace = array("\x00", '%'); + + if (!is_array($row)) { + return $row; + } + + /* strip any slashes, correct fieldname */ + $newrow = array(); + foreach ($row as $i => $v) { + // TODO: If a query of the format 'SELECT a.id, b.text FROM table' is used, + // the sqlite extension will give us key indizes 'a.id' and 'b.text' + // instead of just 'id' and 'text' like in mysql/postgresql extension. + // To fix that, we use a preg-regex; but that is quite performance costy. + // Either we always need to use 'SELECT a.id AS id, b.text AS text' in query, + // or the sqlite extension may get fixed. :-) + $newrow[preg_replace('@^.+\.(.*)@', '\1', $i)] = str_replace($search, $replace, $v); + } + + return $newrow; + } + + /** + * Tells the DB Layer to start a DB transaction. + * + * @access public + */ + public function beginTransaction(){ + $this->db_conn->beginTransaction(); + } + + /** + * Tells the DB Layer to end a DB transaction. + * + * @access public + * @param boolean If true, perform the query. If false, rollback. + */ + public function endTransaction(bool $commit) + { + if ($commit){ + $this->db_conn->commit(); + } else { + $this->db_conn->rollback(); + } + } + + /** + * Assemble and return SQL condition for a "IN (...)" clause + * + * @access public + * @param string table column name + * @param array referenced array of values to search for in the "IN (...)" clause + * @param string condition of how to associate the different input values of the $search_ids parameter + * @return string resulting SQL string + */ + public function inSql($col, &$searchIds, $type = ' OR ') + { + $sql = array(); + if (!is_array($searchIds)) { + return false; + } + + foreach ($searchIds as $id) { + $sql[] = $col . ' = ' . $id; + } + + $cond = '(' . implode($type, $sql) . ')'; + return $cond; + } + + /** + * Returns an escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. + * + * @access public + * @param string input string + * @return string output string + */ + public function escapeString($string) + { + return substr($this->db_conn->quote($string), 1, -1); + } + + /** + * Returns the option to a LIMIT SQL statement, because it varies across DB systems + * + * @access public + * @param int Number of the first row to return data from + * @param int Number of rows to return + * @return string SQL string to pass to a LIMIT statement + */ + public function limit($start, $offset) + { + return $start . ', ' . $offset; + } + + /** + * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement + * + * @access public + * @param SQL string of a LIMIT option + * @return SQL string containing a full LIMIT statement + */ + public function limitSql($limitstring) + { + return ' LIMIT ' . $limitstring; + } + + /** + * Returns the number of affected rows of a SQL query + * + * @access public + * @return int Number of affected rows + */ + public function affectedRows() + { + // FIXME: Cache statement handler in this class? + return $this->serendipity['dbSth']->rowCount(); + } + + /** + * Returns the number of updated rows in a SQL query + * + * @access public + * @return int Number of updated rows + */ + public function updatedRows() + { + // it is unknown whether pg_affected_rows returns number of rows + // UPDATED or MATCHED on an UPDATE statement. + // FIXME: Cache statement handler in this class? + return $this->serendipity['dbSth']->rowCount(); + } + + /** + * Returns the number of matched rows in a SQL query + * + * @access public + * @return int Number of matched rows + */ + public function matchedRows() + { + // it is unknown whether pg_affected_rows returns number of rows + // UPDATED or MATCHED on an UPDATE statement. + // FIXME: Cache statement handler in this class? + return $this->serendipity['dbSth']->rowCount(); + } + + /** + * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns + * + * @access public + * @param string Name of the table to get a INSERT ID for + * @param string Name of the column to get a INSERT ID for + * @return int Value of the auto-increment column + */ + public function insertId($table = '', $id = '') + { + return $this->db_conn->lastInsertId(); + } + + /** + * Perform a DB Layer SQL query. + * + * This function returns values dependin on the input parameters and the result of the query. + * It can return: + * false or a string if there was an error (depends on $expectError), + * true if the query succeeded but did not generate any rows + * array of field values if it returned a single row and $single is true + * array of array of field values if it returned row(s) [stacked array] + * + * @access public + * @param string SQL query to execute + * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! + * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) + * @param boolean If true, errors will be reported. If false, errors will be ignored. + * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column + * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. + * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) + * @return mixed Returns the result of the SQL query, depending on the input parameters + */ + public function &query($sql, $single = false, $result_type = "both", $reportErr = false, $assocKey = false, $assocVal = false, $expectError = false) + { + $type_map = array( + 'assoc' => PDO::FETCH_ASSOC, + 'num' => PDO::FETCH_NUM, + 'both' => PDO::FETCH_BOTH, + 'true' => true, + 'false' => false + ); + + //serendipity_db_logmsg('SQLQUERY: ' . $sql); + if (!$expectError && ($reportErr || !$this->serendipity['production'])) { + $this->serendipity['dbSth'] = $this->db_conn->prepare($sql); + } else { + $this->serendipity['dbSth'] = $this->db_conn->prepare($sql); + } + + if (!$this->serendipity['dbSth']) { + if (!$expectError && !$this->serendipity['production']) { + print "Error in $sql"; + print $this->db_conn->errorInfo() . "
\n"; + if (function_exists('debug_backtrace')) { + highlight_string(var_export(debug_backtrace(), 1)); + } + print "
$sql
"; + } + return $type_map['false']; + } + + $this->serendipity['dbSth']->execute(); + + if ($this->serendipity['dbSth'] === true) { + return $type_map['true']; + } + + $result_type = $type_map[$result_type]; + + $rows = array(); + + foreach ($this->serendipity['dbSth']->fetchAll($result_type) as $row) { + $row = $this->fetchArray($row, $result_type); + if (!empty($assocKey)) { + // You can fetch a key-associated array via the two function parameters assocKey and assocVal + if (empty($assocVal)) { + $rows[$row[$assocKey]] = $row; + } else { + $rows[$row[$assocKey]] = $row[$assocVal]; + } + } else { + $rows[] = $row; + } + } + //$this->logmsg('SQLRESULT: ' . print_r($rows,true)); + + if (count($rows) == 0) { + if ($single) { + return $type_map['false']; + } + return $type_map['true']; + } + if (count($rows) == 1 && $single) { + return $rows[0]; + } + return $rows; + } + + /** + * Prepares a Serendipity query input to fully valid SQL. Replaces certain "template" variables. + * + * @access public + * @param string SQL query with template variables to convert + * @return resource SQL resource handle of the executed query + */ + public function schemaImport($query) + { + static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', '{FULLTEXT}', '{BOOLEAN}', '{UTF_8}', '{TEXT}'); + static $replace = array('INTEGER AUTOINCREMENT', 'PRIMARY KEY', '', '', 'BOOLEAN NOT NULL', '', 'LONGTEXT'); + + if (stristr($query, '{FULLTEXT_MYSQL}')) { + return true; + } + + $query = trim(str_replace($search, $replace, $query)); + $query = str_replace('INTEGER AUTOINCREMENT PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT', $query); + if ($query[0] == '@') { + // Errors are expected to happen (like duplicate index creation) + return $this->query(substr($query, 1), false, 'both', false, false, false, true); + } else { + return $this->query($query); + } + } + + /** + * Try to connect to the configured Database (during installation) + * + * @access public + * @param array input configuration array, holding the connection info + * @param array referenced array which holds the errors that might be encountered + * @return boolean return true on success, false on error + */ + public function probe($hash, &$errs) + { + if (!in_array('sqlite', PDO::getAvailableDrivers())) { + $errs[] = 'PDO_SQLITE driver not available'; + return false; + } + + $dbName = (isset($hash['sqlitedbName']) ? $hash['sqlitedbName'] : $hash['dbName']); + if (defined('S9Y_DATA_PATH')) { + // Shared installations! + $dbfile = S9Y_DATA_PATH . $dbName . '.db'; + } else { + $dbfile = $this->serendipity['serendipityPath'] . $dbName . '.db'; + } + + $this->db_conn = new PDO( + 'sqlite:' . $dbfile + ); + + if (!$this->db_conn) { + $errs[] = "Unable to open \"$dbfile\" - check permissions (directory needs to be writeable for webserver)!"; + return false; + } + + return true; + } + + /** + * Returns the SQL code used for concatenating strings + * + * @access public + * @param string Input string/column to concatenate + * @return string SQL parameter + */ + public function concat($string) + { + return 'concat(' . $string . ')'; + } +} diff --git a/lib/Serendipity/Database/PostgresDatabase.php b/lib/Serendipity/Database/PostgresDatabase.php new file mode 100644 index 00000000..50e2720b --- /dev/null +++ b/lib/Serendipity/Database/PostgresDatabase.php @@ -0,0 +1,356 @@ +query('begin work'); + } + + /** + * Tells the DB Layer to end a DB transaction. + * + * @access public + * @param boolean If true, perform the query. If false, rollback. + */ + public function endTransaction($commit) + { + if ($commit) { + $this->query('commit'); + } else { + $this->query('rollback'); + } + } + + /** + * Assemble and return SQL condition for a "IN (...)" clause + * + * @access public + * @param string table column name + * @param array referenced array of values to search for in the "IN (...)" clause + * @param string condition of how to associate the different input values of the $search_ids parameter + * @return string resulting SQL string + */ + public function inSql($col, &$search_ids, $type = ' OR ') + { + return $col . " IN (" . implode(', ', $search_ids) . ")"; + } + + /** + * Connect to the configured Database + * + * @access public + * @return resource connection handle + */ + public function connect() + { + if (isset($this->serendipity['dbPersistent']) && $this->serendipity['dbPersistent']) { + $function = 'pg_pconnect'; + } else { + $function = 'pg_connect'; + } + + $host = $port = ''; + if (strlen($this->db_hostname)) { + if (false !== strstr($this->db_hostname, ':')) { + $tmp = explode(':', $this->db_hostname); + $host = "host={$tmp[0]} "; + $port = "port={$tmp[1]} "; + } else { + $host = "host={$this->db_hostname} "; + } + } + + $this->db_conn = $function( + sprintf( + '%sdbname=%s user=%s password=%s', + "$host$port", + $this->db_name, + $this->db_username, + $this->db_password + ) + ); + + return $this->db_conn; + } + + /** + * Returns an escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. + * + * @access public + * @param string input string + * @return string output string + */ + public function escapeString($string) + { + return pg_escape_string($string); + } + + /** + * Returns the option to a LIMIT SQL statement, because it varies across DB systems + * + * @access public + * @param int Number of the first row to return data from + * @param int Number of rows to return + * @return string SQL string to pass to a LIMIT statement + */ + public function limit($start, $offset) + { + return $offset . ', ' . $start; + } + + /** + * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement + * + * @access public + * @param SQL string of a LIMIT option + * @return SQL string containing a full LIMIT statement + */ + public function limitSql($limitstring) + { + $limit_split = explode(',', $limitstring); + if (count($limit_split) > 1) { + $limit = ' LIMIT ' . $limit_split[0] . ' OFFSET ' . $limit_split[1]; + } else { + $limit = ' LIMIT ' . $limit_split[0]; + } + return $limit; + } + + /** + * Returns the number of affected rows of a SQL query + * + * @access public + * @return int Number of affected rows + */ + public function affectedRows() + { + return pg_affected_rows($this->serendipity['dbLastResult']); + } + + /** + * Returns the number of updated rows in a SQL query + * + * @access public + * @return int Number of updated rows + */ + public function updatedRows() + { + // it is unknown whether pg_affected_rows returns number of rows + // UPDATED or MATCHED on an UPDATE statement. + return pg_affected_rows($this->serendipity['dbLastResult']); + } + + /** + * Returns the number of matched rows in a SQL query + * + * @access public + * @return int Number of matched rows + */ + public function matchedRows() + { + // it is unknown whether pg_affected_rows returns number of rows + // UPDATED or MATCHED on an UPDATE statement. + return pg_affected_rows($this->serendipity['dbLastResult']); + } + + /** + * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns + * + * @access public + * @param string Name of the table to get a INSERT ID for + * @param string Name of the column to get a INSERT ID for + * @return int Value of the auto-increment column + */ + public function insertId($table = '', $id = '') + { + if (empty($table) || empty($id)) { + // BC - will/should never be called with empty parameters! + return pg_last_oid($this->serendipity['dbLastResult']); + } else { + $query = "SELECT currval('{$this->db_prefix}{$table}_{$id}_seq'::text) AS {$id}"; + $res = pg_query($this->db_conn, $query); + if (pg_num_rows($res)) { + $insertId = pg_fetch_array($res, 0, PGSQL_ASSOC); + return $insertId[$id]; + } else { + return pg_last_oid($this->serendipity['dbLastResult']); // BC - should not happen! + } + } + } + + /** + * Perform a DB Layer SQL query. + * + * This function returns values dependin on the input parameters and the result of the query. + * It can return: + * false or a string if there was an error (depends on $expectError), + * true if the query succeeded but did not generate any rows + * array of field values if it returned a single row and $single is true + * array of array of field values if it returned row(s) [stacked array] + * + * @access public + * @param string SQL query to execute + * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! + * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) + * @param boolean If true, errors will be reported. If false, errors will be ignored. + * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column + * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. + * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) + * @return mixed Returns the result of the SQL query, depending on the input parameters + */ + public function &query($sql, $single = false, $result_type = "both", $reportErr = false, $assocKey = false, $assocVal = false, $expectError = false) + { + $type_map = array( + 'assoc' => PGSQL_ASSOC, + 'num' => PGSQL_NUM, + 'both' => PGSQL_BOTH, + 'true' => true, + 'false' => false + ); + + if (!isset($this->serendipity['dbPgsqlOIDS'])) { + $this->serendipity['dbPgsqlOIDS'] = true; + @$this->query('SET default_with_oids = true', true, 'both', false, false, false, true); + } + + if (!$expectError && ($reportErr || !$this->serendipity['production'])) { + $this->serendipity['dbLastResult'] = pg_query($this->db_conn, $sql); + } else { + $this->serendipity['dbLastResult'] = @pg_query($this->db_conn, $sql); + } + + if (!$this->serendipity['dbLastResult']) { + if (!$expectError && !$this->serendipity['production']) { + print "Error in $sql"; + print pg_last_error($this->db_conn) . "
\n"; + if (function_exists('debug_backtrace')) { + highlight_string(var_export(debug_backtrace(), 1)); + } + print "
$sql
"; + } + return $type_map['false']; + } + + if ($this->serendipity['dbLastResult'] === true) { + return $type_map['true']; + } + + $result_type = $type_map[$result_type]; + + $n = pg_num_rows($this->serendipity['dbLastResult']); + + switch ($n) { + case 0: + if ($single) { + return $type_map['false']; + } + return $type_map['true']; + + case 1: + if ($single) { + return pg_fetch_array($this->serendipity['dbLastResult'], 0, $result_type); + } + default: + $rows = array(); + for ($i = 0; $i < $n; $i++) { + if (!empty($assocKey)) { + // You can fetch a key-associated array via the two function parameters assocKey and assocVal + $row = pg_fetch_array($this->serendipity['dbLastResult'], $i, $result_type); + if (empty($assocVal)) { + $rows[$row[$assocKey]] = $row; + } else { + $rows[$row[$assocKey]] = $row[$assocVal]; + } + } else { + $rows[] = pg_fetch_array($this->serendipity['dbLastResult'], $i, $result_type); + } + } + return $rows; + } + } + + /** + * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. + * + * @access public + * @param string SQL query with template variables to convert + * @return resource SQL resource handle of the executed query + */ + public function schemaImport($query) + { + static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', + '{FULLTEXT}', '{BOOLEAN}', 'int(1)', 'int(10)', 'int(11)', 'int(4)', '{UTF_8}', '{TEXT}'); + static $replace = array('SERIAL', 'primary key', '', '', 'BOOLEAN NOT NULL', 'int2', + 'int4', 'int4', 'int4', '', 'text'); + + if (stristr($query, '{FULLTEXT_MYSQL}')) { + return true; + } + + $query = trim(str_replace($search, $replace, $query)); + if ($query[0] == '@') { + // Errors are expected to happen (like duplicate index creation) + return $this->query(substr($query, 1), false, 'both', false, false, false, true); + } else { + return $this->query($query); + } + } + + /** + * Try to connect to the configured Database (during installation) + * + * @access public + * @param array input configuration array, holding the connection info + * @param array referenced array which holds the errors that might be encountered + * @return boolean return true on success, false on error + */ + public function probe($hash, &$errs) + { + if (!function_exists('pg_connect')) { + $errs[] = 'No PostgreSQL extension found. Please check your webserver installation or contact your systems administrator regarding this problem.'; + return false; + } + + $this->db_conn = pg_connect( + sprintf( + '%sdbname=%s user=%s password=%s', + strlen($hash['dbHost']) ? ('host=' . $hash['dbHost'] . ' ') : '', + $hash['dbName'], + $hash['dbUser'], + $hash['dbPass'] + ) + ); + + if (!$this->db_conn) { + $errs[] = 'Could not connect to database; check your settings.'; + return false; + } + + return true; + } + + /** + * Returns the SQL code used for concatenating strings + * + * @access public + * @param string Input string/column to concatenate + * @return string SQL parameter + */ + public function concat($string) + { + return '(' . str_replace(', ', '||', $string) . ')'; + } +} diff --git a/lib/Serendipity/Database/SqlRelayDatabase.php b/lib/Serendipity/Database/SqlRelayDatabase.php new file mode 100644 index 00000000..cabe5a00 --- /dev/null +++ b/lib/Serendipity/Database/SqlRelayDatabase.php @@ -0,0 +1,552 @@ +db_conn); + switch ($databaseType) { + case 'mysql': + $sqlstr = 'start transaction'; + break; + + case 'postgresql': + $sqlstr = 'begin work'; + break; + + case 'sqlite': + $sqlstr = 'begin transaction'; + break; + + default: + $sqlstr = 'start transaction'; + break; + } + $this->query($sqlstr); + } + + /** + * Tells the DB Layer to end a DB transaction. + * + * @access public + * @param boolean If true, perform the query. If false, rollback. + */ + public function endTransaction($commit) + { + if ($commit) { + sqlrcon_commit($this->db_conn); + } else { + sqlrcon_rollback($this->db_conn); + } + } + + /** + * Assemble and return SQL condition for a "IN (...)" clause + * + * @access public + * @param string table column name + * @param array referenced array of values to search for in the "IN (...)" clause + * @param string condition of how to associate the different input values of the $search_ids parameter + * @return string resulting SQL string + */ + public function inSql($col, &$search_ids, $type = ' OR ') + { + if (sqlrcon_identify($this->db_conn) == "sqlite") { + $sql = array(); + if (!is_array($search_ids)) { + return false; + } + + foreach ($search_ids as $id) { + $sql[] = $col . ' = ' . $id; + } + + $cond = '(' . implode($type, $sql) . ')'; + return $cond; + } else { + return $col . " IN (" . implode(', ', $search_ids) . ")"; + } + } + + /** + * Perform a DB Layer SQL query. + * + * This function returns values dependin on the input parameters and the result of the query. + * It can return: + * false or a string if there was an error (depends on $expectError), + * true if the query succeeded but did not generate any rows + * array of field values if it returned a single row and $single is true + * array of array of field values if it returned row(s) [stacked array] + * + * @access public + * @param string SQL query to execute + * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! + * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) + * @param boolean If true, errors will be reported. If false, errors will be ignored. + * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column + * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. + * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) + * @return mixed Returns the result of the SQL query, depending on the input parameters + */ + protected function generateResultset($cursor, $result_type = 'sqlr_BOTH') + { + $return_row = array(); + + for ($r_row = 0, $max_r = sqlrcur_rowCount($cursor) ; $r_row < $max_r ; $r_row++) { + for ($r_col=0, $max_rc = sqlrcur_colCount($cursor); $r_col < $max_rc ; $r_col++) { + if ($result_type == 'sqlr_ASSOC') { + $return_row[sqlrcur_getColumnName($cursor, $r_col)] = sqlrcur_getField($cursor, $r_row, $r_col); + } else { + $return_row[$r_row][$r_col] = sqlrcur_getField($cursor, $r_row,$r_col); + $return_row[$r_row][sqlrcur_getColumnName($cursor, $r_col)] = $return_row[$r_row][$r_col]; + } + } + } + return $return_row; + } + + public function &query($sql, $single = false, $result_type = "both", $reportErr = false, $assocKey = false, $assocVal = false, $expectError = false) + { + $type_map = array( + 'assoc' => sqlr_ASSOC, + 'num' => sqlr_NUM, + 'both' => sqlr_BOTH, + 'true' => true, + 'false' => false + ); + static $benchmark = false; + + // highlight_string(var_export($sql, 1)); + + if (!is_resource($this->db_conn)) { + return false; + } + + $cur = sqlrcur_alloc($this->db_conn); + $this->db_cursor = $cur; + + if ($benchmark) { + $start = microtime_float(); + } + + if ($expectError) { + $c = sqlrcur_sendQuery($cur, $sql); + } else { + $c = sqlrcur_sendQuery($cur, $sql); + } + + if ($benchmark) { + $end = microtime_float(); + + $cur = sqlrcur_alloc($this->db_conn); + $sql_b = "INSERT INTO BLOGLOG (request, timestamp, sql, exec_time, ip) VALUES ('" . $this->escapeString($_SERVER['REQUEST_URI']) . "', NOW(), '" . $this->escapeString($sql) . "', '" . (number_format($end-$start, 10)) . "', '" . $this->escapeString($_SERVER['REMOTE_ADDR']) . "')"; + $c = sqlrcur_sendQuery($cur, $sql_b); + + $psql = $sql; + $psql = preg_replace('@[0-9]{10}@', 'TIMESTAMP', $psql); + $sql_U = "UPDATE BLOGLOG_TOTAL SET counter = counter + 1 WHERE sql = '" . $this->escapeString($psql) . "'"; + $c = sqlrcur_sendQuery($cur, $sql_U); + if (sqlrcur_affectedRows() < 1) { + $sql_i = "INSERT INTO BLOGLOG_TOTAL (sql, counter) VALUES ('" . $this->escapeString($psql) . "', 1)"; + $c = sqlrcur_sendQuery($cur,$sql_i); + } + } + + if (!$expectError && sqlrcur_errorMessage($cur) != '') { + $msg = '
' . serendipity_specialchars($sql) . '
/ ' . serendipity_specialchars(sqlrcur_errorMessage($cur)); + return $msg; + } + + if (!$c) { + if (!$expectError && !$this->serendipity['production']) { + print '
' . serendipity_specialchars($sql) . '
/ ' . serendipity_specialchars(sqlrcur_errorMessage($cur)); + if (function_exists('debug_backtrace') && $reportErr == true) { + highlight_string(var_export(debug_backtrace(), 1)); + } + } + + return $type_map['false']; + } + + if ($c === true) { + return $type_map['true']; + } + + $result_type = $type_map[$result_type]; + + switch (sqlrcur_rowCount($cur)) { + case 0: + if ($single) { + return $type_map['false']; + } + return $type_map['true']; + + case 1: + if ($single) { + $row = $this->generateResultset($cur, $result_type); + if ($result_type != 'sqlr_ASSOC') { + $row=$row[0]; + } + return $row; + } + + default: + if ($single) { + return $this->generateResultset($cur, $result_type); + } + + $row = $this->generateResultset($cur); + $rows = array(); + + for ($idx=0, $idxc = count($row); $idx < $idxc ; $idx++) { + if (!empty($assocKey)) { + // You can fetch a key-associated array via the two function parameters assocKey and assocVal + if (empty($assocVal)) { + $rows[$assocKey] = $row[$idx][$assocKey]; + } else { + $rows[$row[$idx][$assocKey]] = $row[$idx][$assocVal]; + } + } else { + $rows = $row; + } + } + return $rows; + } + } + + /** + * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns + * + * @access public + * @return int Value of the auto-increment column + * + * If you insert multiple rows using a single INSERT statement, + * LAST_INSERT_ID() returns the value generated for the first inserted row only. + */ + public function insertId($table = '', $id = '') + { + $databaseType = sqlrcon_identify($this->db_conn); + switch ($databaseType) { + case 'mysql': + $sqlstr='SELECT LAST_INSERT_ID()'; + break; + + case 'postgresql': + $sqlstr = "SELECT currval('{$this->db_prefix}{$table}_{$id}_seq'::text) AS {$id}"; + break; + + case 'sqlite': + $sqlstr = 'SELECT last_insert_rowid()'; + break; + + default: + $sqlstr = 'SELECT LAST_INSERT_ID()'; + } + + $row = $this->query($sqlstr, true); + + return $row[0]; + } + + /** + * Returns the number of affected rows of a SQL query + * + * @access public + * @return int Number of affected rows + */ + public function affectedRows() + { + /* int sqlrcur_affectedRows(int sqlrcurref) + * Returns the number of rows that were updated, inserted or deleted by the query. + * Not all databases support this call. + * Don't use it for applications which are designed to be portable across databases. + * -1 is returned by databases which don't support this option. + */ + return sqlrcur_affectedRows($this->db_cursor); + } + + /** + * Returns the number of updated rows in a SQL query + * + * @access public + * @return int Number of updated rows + */ + public function updatedRows() + { + /* + preg_match( + "/^[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)/", + mysql_info(), + $arr); + // mysql_affected_rows returns 0 if rows were matched but not changed. + // mysql_info returns rows matched AND rows changed + return $arr[2]; + */ + return $this->affectedRows(); + } + + /** + * Returns the number of matched rows in a SQL query + * + * @access public + * @return int Number of matched rows + */ + public function matchedRows() + { + /* + preg_match( + "/^[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)/", + mysql_info(), + $arr); + // mysql_affected_rows returns 0 if rows were matched but not changed. + // mysql_info returns rows matched AND rows changed + return $arr[1]; + */ + return $this->affectedRows(); + } + + /** + * Returns an escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. + * + * @access public + * @param string input string + * @return string output string + */ + public function escapeString($str) + { + static $search = array("\x00", '%', "'", '\"'); + static $replace = array('%00', '%25', "''", '\\\"'); + + $databaseType = sqlrcon_identify($this->db_conn); + switch ($databaseType) { + case 'mysql': + if (function_exists('mysql_real_escape_string')) { + $rtnstr = mysql_real_escape_string($str, $this->db_conn); + } else { + $rtnstr = addslashes($str); + } + break; + + case 'postgresql': + if (function_exists('pg_escape_string')) { + $rtnstr = pg_escape_string($str); + } else { + $rtnstr = addslashes($str); + } + break; + + case 'sqlite': + $rtnstr = str_replace($search, $replace, $str); + break; + + default: + $rtnstr = addslashes($str); + } + + return $rtnstr; + } + + /** + * Returns the option to a LIMIT SQL statement, because it varies across DB systems + * + * @access public + * @param int Number of the first row to return data from + * @param int Number of rows to return + * @return string SQL string to pass to a LIMIT statement + */ + public function limit($start, $offset) + { + return $start . ', ' . $offset; + } + + /** + * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement + * + * @access public + * @param SQL string of a LIMIT option + * @return SQL string containing a full LIMIT statement + */ + public function limitSql($limitstring) + { + $databaseType = sqlrcon_identify($this->db_conn); + switch ($databaseType) { + case "mysql": + return ' LIMIT ' . $limitstring; + case "postgresql": + $limit_split = explode(',', $limitstring); + if (count($limit_split) > 1) { + $limit = ' LIMIT ' . $limit_split[0] . ' OFFSET ' . $limit_split[1]; + } else { + $limit = ' LIMIT ' . $limit_split[0]; + } + return $limit; + default: + return ' LIMIT ' . $limitstring; + } + } + + /** + * Connect to the configured Database + * + * @access public + * @return resource connection handle + */ + public function connect() + { + if (isset($this->db_conn)) { + return $this->db_conn; + } + + if (isset($this->serendipity['dbPersistent']) && $this->serendipity['dbPersistent']) { + $function = 'sqlrcon_alloc'; + } else { + $function = 'sqlrcon_alloc'; + } + + #$this->db_hostname="localhost:9000" + $dbHostPort = explode(":", $this->db_hostname); + $this->db_conn = $function($dbHostPort[0], $dbHostPort[1], "", $this->db_username, $this->db_password, 0, 1); + sqlrcon_debugOff($this->db_conn); + + if( sqlrcon_identify($this->db_conn) == "mysql") { + $this->reconnect(); + } + + return $this->db_conn; + } + + public function reconnect() + { + if (isset($this->serendipity['dbCharset'])) { + $this->query("SET NAMES " . $this->serendipity['dbCharset']); + @define('SQL_CHARSET_INIT', true); + } elseif (defined('SQL_CHARSET') && $this->serendipity['dbNames'] && !defined('SQL_CHARSET_INIT')) { + $this->query("SET NAMES " . SQL_CHARSET); + } + } + + /** + * Prepares a Serendipity query input to fully valid SQL. Replaces certain "template" variables. + * + * @access public + * @param string SQL query with template variables to convert + * @return resource SQL resource handle of the executed query + */ + public function schemaImport($query) + { + $databaseType = sqlrcon_identify($this->db_conn); + switch ($databaseType) { + case "mysql": + static $search = array('{AUTOINCREMENT}', '{PRIMARY}', + '{UNSIGNED}', '{FULLTEXT}', '{FULLTEXT_MYSQL}', '{BOOLEAN}', '{TEXT}'); + static $replace = array('int(11) not null auto_increment', 'primary key', + 'unsigned' , 'FULLTEXT', 'FULLTEXT', 'enum (\'true\', \'false\') NOT NULL default \'true\'', 'LONGTEXT'); + static $is_utf8 = null; + + if ($is_utf8 === null) { + $search[] = '{UTF_8}'; + if ((isset($_POST['charset']) && $_POST['charset'] == 'UTF-8/') || + $this->serendipity['charset'] == 'UTF-8/' || + $this->serendipity['POST']['charset'] == 'UTF-8/' || + LANG_CHARSET == 'UTF-8' ) { + $replace[] = '/*!40100 CHARACTER SET utf8 COLLATE utf8_unicode_ci */'; + } else { + $replace[] = ''; + } + } + break; + + case "postgresql": + static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', + '{FULLTEXT}', '{FULLTEXT_MYSQL}', '{BOOLEAN}', 'int(1)', 'int(10)', 'int(11)', 'int(4)', '{UTF_8}', '{TEXT}'); + static $replace = array('SERIAL', 'primary key', '', + '', '', 'BOOLEAN NOT NULL', 'int2', 'int4', 'int4', 'int4', '', 'text'); + break; + + case "sqlite": + static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', '{FULLTEXT}', '{FULLTEXT_MYSQL}', '{BOOLEAN}', '{UTF_8}', '{TEXT}'); + static $replace = array('INTEGER', 'PRIMARY KEY', '', '', '', 'BOOLEAN NOT NULL', '', 'LONGTEXT'); + break; + + default: + return true; + } + + $query = trim(str_replace($search, $replace, $query)); + + if ($query{0} == '@') { + // Errors are expected to happen (like duplicate index creation) + return $this->query(substr($query, 1), false, 'both', false, false, false, true); + } else { + return $this->query($query); + } + } + + /** + * Try to connect to the configured Database (during installation) + * + * @access public + * @param array input configuration array, holding the connection info + * @param array referenced array which holds the errors that might be encountered + * @return boolean return true on success, false on error + */ + public function probe($hash, &$errs) + { + $c = null; + + if (!function_exists('sqlrcon_alloc')) { + $errs[] = 'No sql_relay extension found. Please check your webserver installation or contact your systems administrator regarding this problem.'; + return false; + } + + if (!($c=@sqlrcon_alloc($hash['dbHost'], '', '', $hash['dbUser'], $hash['dbPass'], 0, 5))) { + $errs[] = 'Could not connect to database; check your settings.'; + return false; + } + + $this->db_conn = $c; + + /* which db should be used is defined in sqlrelay.conf */ + + return true; + } + + /** + * Returns the SQL code used for concatenating strings + * + * @access public + * @param string Input string/column to concatenate + * @return string SQL parameter + */ + public function concat($string) + { + $databaseType = sqlrcon_identify($this->db_conn); + switch ($databaseType) { + case 'mysql': + return 'concat(' . $string . ')'; + + case 'postgresql': + return '(' . str_replace(', ', '||', $string) . ')'; + + case 'sqlite': + return 'concat(' . $string . ')'; + + default: + return 'concat(' . $string . ')'; + } + } +} diff --git a/lib/Serendipity/Database/Sqlite3Database.php b/lib/Serendipity/Database/Sqlite3Database.php new file mode 100644 index 00000000..f9542404 --- /dev/null +++ b/lib/Serendipity/Database/Sqlite3Database.php @@ -0,0 +1,390 @@ +query('begin transaction'); + } + + /** + * Tells the DB Layer to end a DB transaction. + * + * @access public + * @param boolean If true, perform the query. If false, rollback. + */ + public function endTransaction($commit) + { + if ($commit) { + $this->query('commit transaction'); + } else { + $this->query('rollback transaction'); + } + } + + /** + * Connect to the configured Database + * + * @access public + * @return resource connection handle + */ + public function connect() + { + if (isset($this->db_conn)) { + return $this->db_conn; + } + + // SQLite3 doesn't support persistent connections + $this->db_conn = sqlite3_open((defined('S9Y_DATA_PATH') ? S9Y_DATA_PATH : $this->serendipity['serendipityPath']) . $this->db_name . '.db'); + + return $this->db_conn; + } + + /** + * Returns a escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. + * + * @access public + * @param string input string + * @return string output string + */ + public function escapeString($string) + { + static $search = array("\x00", '%', "'", '\"'); + static $replace = array('%00', '%25', "''", '\\\"'); + + return str_replace($search, $replace, $string); + } + + /** + * Returns the number of affected rows of a SQL query + * + * @access public + * @return int Number of affected rows + */ + public function affectedRows() + { + return sqlite3_changes($this->db_conn); + } + + /** + * Returns the number of updated rows in a SQL query + * + * @access public + * @return int Number of updated rows + */ + public function updatedRows() + { + // It is unknown whether sqllite returns rows MATCHED or rows UPDATED + return sqlite3_changes($this->db_conn); + } + + /** + * Returns the number of matched rows in a SQL query + * + * @access public + * @return int Number of matched rows + */ + public function matchedRows() + { + // It is unknown whether sqllite returns rows MATCHED or rows UPDATED + return sqlite3_changes($this->db_conn); + } + + /** + * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns + * + * @access public + * @return int Value of the auto-increment column + */ + public function insertId() + { + return sqlite3_last_insert_rowid($this->db_conn); + } + + /** + * Parse result arrays into expected format for further operations + * + * SQLite does not support to return "e.entryid" within a $row['entryid'] return. + * So this function manually iteratse through all result rows and rewrites 'X.yyyy' to 'yyyy'. + * Yeah. This sucks. Don't tell me! + * + * @access private + * @param resource The row resource handle + * @param int Bitmask to tell whether to fetch numerical/associative arrays + * @return array Propper array containing the resource results + */ + protected function fetchArray($res, $type = self::SQLITE3_BOTH) + { + static $search = array('%00', '%25'); + static $replace = array("\x00", '%'); + + $row = sqlite3_fetch_array($res); + if (!is_array($row)) { + return $row; + } + + /* strip any slashes, correct fieldname */ + foreach ($row as $i => $v) { + // TODO: If a query of the format 'SELECT a.id, b.text FROM table' is used, + // the sqlite extension will give us key indizes 'a.id' and 'b.text' + // instead of just 'id' and 'text' like in mysql/postgresql extension. + // To fix that, we use a preg-regex; but that is quite performance costy. + // Either we always need to use 'SELECT a.id AS id, b.text AS text' in query, + // or the sqlite extension may get fixed. :-) + $row[preg_replace('@^.+\.(.*)@', '\1', $i)] = str_replace($search, $replace, $v); + } + + if ($type == self::SQLITE3_NUM) { + $frow = array(); + } else { + $frow = $row; + } + if ($type != self::SQLITE3_ASSOC) { + $i = 0; + foreach ($row as $k => $v) { + $frow[$i] = $v; + $i++; + } + } + + return $frow; + } + + /** + * Assemble and return SQL condition for a "IN (...)" clause + * + * @access public + * @param string table column name + * @param array referenced array of values to search for in the "IN (...)" clause + * @param string condition of how to associate the different input values of the $search_ids parameter + * @return string resulting SQL string + */ + public function inSql($col, &$search_ids, $type = ' OR ') + { + $sql = array(); + if (!is_array($search_ids)) { + return false; + } + + foreach ($search_ids as $id) { + $sql[] = $col . ' = ' . $id; + } + + $cond = '(' . implode($type, $sql) . ')'; + return $cond; + } + + /** + * Perform a DB Layer SQL query. + * + * This function returns values dependin on the input parameters and the result of the query. + * It can return: + * false or a string if there was an error (depends on $expectError), + * true if the query succeeded but did not generate any rows + * array of field values if it returned a single row and $single is true + * array of array of field values if it returned row(s) [stacked array] + * + * @access public + * @param string SQL query to execute + * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! + * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) + * @param boolean If true, errors will be reported. If false, errors will be ignored. + * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column + * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. + * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) + * @return mixed Returns the result of the SQL query, depending on the input parameters + */ + public function &query($sql, $single = false, $result_type = "both", $reportErr = true, $assocKey = false, $assocVal = false, $expectError = false) + { + $type_map = array( + 'assoc' => self::SQLITE3_ASSOC, + 'num' => self::SQLITE3_NUM, + 'both' => self::SQLITE3_BOTH, + 'true' => true, + 'false' => false + ); + + static $debug = false; + + if ($debug) { + // Open file and write directly. In case of crashes, the pointer needs to be killed. + $fp = @fopen('sqlite.log', 'a'); + fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE QUERY: ' . $sql . "\n\n"); + fclose($fp); + } + + if ($reportErr && !$expectError) { + $res = sqlite3_query($this->db_conn, $sql); + } else { + $res = @sqlite3_query($this->db_conn, $sql); + } + + if (!$res) { + if (!$expectError && !$this->serendipity['production']) { + var_dump($res); + var_dump($sql); + $msg = "problem with query"; + return $msg; + } + if ($debug) { + $fp = @fopen('sqlite.log', 'a'); + fwrite($fp, '[' . date('d.m.Y H:i') . '] [ERROR] ' . "\n\n"); + fclose($fp); + } + + return $type_map['false']; + } + + if ($res === true) { + return $type_map['true']; + } + + $rows = array(); + + while (($row = $this->fetchArray($res, $type_map[$result_type]))) { + if (!empty($assocKey)) { + // You can fetch a key-associated array via the two function parameters assocKey and assocVal + if (empty($assocVal)) { + $rows[$row[$assocKey]] = $row; + } else { + $rows[$row[$assocKey]] = $row[$assocVal]; + } + } else { + $rows[] = $row; + } + } + + if ($debug) { + $fp = @fopen('sqlite.log', 'a'); + fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE RESULT: ' . print_r($rows, true). "\n\n"); + fclose($fp); + } + + if ($single && count($rows) == 1) { + return $rows[0]; + } + + if (count($rows) == 0) { + if ($single) { + return $type_map['false']; + } + return $type_map['true']; + } + + return $rows; + } + + /** + * Try to connect to the configured Database (during installation) + * + * @access public + * @param array input configuration array, holding the connection info + * @param array referenced array which holds the errors that might be encountered + * @return boolean return true on success, false on error + */ + public function probe($hash, &$errs) + { + $dbName = (isset($hash['sqlitedbName']) ? $hash['sqlitedbName'] : $hash['dbName']); + + if (!function_exists('sqlite3_open')) { + $errs[] = 'SQLite extension not installed. Run "pear install sqlite" on your webserver or contact your systems administrator regarding this problem.'; + return false; + } + + if (defined('S9Y_DATA_PATH')) { + // Shared installations! + $dbfile = S9Y_DATA_PATH . $dbName . '.db'; + } else { + $dbfile = $this->serendipity['serendipityPath'] . $dbName . '.db'; + } + + $this->db_conn = sqlite3_open($dbfile); + + if ($this->db_conn) { + return true; + } + + $errs[] = "Unable to open \"$dbfile\" - check permissions (directory needs to be writeable for webserver)!"; + return false; + } + + /** + * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. + * + * @access public + * @param string SQL query with template variables to convert + * @return resource SQL resource handle of the executed query + */ + public function schemaImport($query) + { + static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', '{FULLTEXT}', '{BOOLEAN}', '{UTF_8}', '{TEXT}'); + static $replace = array('INTEGER AUTOINCREMENT', 'PRIMARY KEY', '', '', 'BOOLEAN NOT NULL', '', 'LONGTEXT'); + + if (stristr($query, '{FULLTEXT_MYSQL}')) { + return true; + } + + $query = trim(str_replace($search, $replace, $query)); + $query = str_replace('INTEGER AUTOINCREMENT PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT', $query); + if ($query[0] == '@') { + // Errors are expected to happen (like duplicate index creation) + return $this->query(substr($query, 1), false, 'both', false, false, false, true); + } else { + return $this->query($query); + } + } + + /** + * Returns the option to a LIMIT SQL statement, because it varies across DB systems + * + * @access public + * @param int Number of the first row to return data from + * @param int Number of rows to return + * @return string SQL string to pass to a LIMIT statement + */ + public function limit($start, $offset) + { + return $start . ', ' . $offset; + } + + /** + * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement + * + * @access public + * @param SQL string of a LIMIT option + * @return SQL string containing a full LIMIT statement + */ + public function limitSql($limitstring) + { + return ' LIMIT ' . $limitstring; + } + + /** + * Returns the SQL code used for concatenating strings + * + * @access public + * @param string Input string/column to concatenate + * @return string SQL parameter + */ + public function concat($string) + { + return 'concat(' . $string . ')'; + } +} diff --git a/lib/Serendipity/Database/Sqlite3ooDatabase.php b/lib/Serendipity/Database/Sqlite3ooDatabase.php new file mode 100644 index 00000000..a51762e9 --- /dev/null +++ b/lib/Serendipity/Database/Sqlite3ooDatabase.php @@ -0,0 +1,404 @@ +query('begin transaction'); + } + + /** + * Tells the DB Layer to end a DB transaction. + * + * @access public + * @param boolean If true, perform the query. If false, rollback. + */ + public function endTransaction($commit) + { + if ($commit) { + $this->query('commit transaction'); + } else { + $this->query('rollback transaction'); + } + } + + /** + * Connect to the configured Database + * + * @access public + * @return resource connection handle + */ + public function connect() + { + if (isset($this->db_conn)) { + return $this->db_conn; + } + + // SQLite3 doesn't support persistent connections + $this->db_conn = new SQLite3((defined('S9Y_DATA_PATH') ? S9Y_DATA_PATH : $this->serendipity['serendipityPath']) . $this->db_name . '.db'); + + return $this->db_conn; + } + + /** + * Returns an escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. + * + * @access public + * @param string input string + * @return string output string + */ + public function escapeString($string) + { + static $search = array("\x00", '%', "'", '\"'); + static $replace = array('%00', '%25', "''", '\\\"'); + + return str_replace($search, $replace, $string); + } + + /** + * Returns the number of affected rows of a SQL query + * + * @access public + * @return int Number of affected rows + */ + public function affectedRows() + { + return $this->db_conn->changes(); + } + + /** + * Returns the number of updated rows in a SQL query + * + * @access public + * @return int Number of updated rows + */ + public function updatedRows() + { + // It is unknown whether sqllite returns rows MATCHED or rows UPDATED + return $this->db_conn->changes(); + } + + /** + * Returns the number of matched rows in a SQL query + * + * @access public + * @return int Number of matched rows + */ + public function matchedRows() + { + // It is unknown whether sqllite returns rows MATCHED or rows UPDATED + return $this->db_conn->changes($this->db_conn); + } + + /** + * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns + * + * @access public + * @return int Value of the auto-increment column + */ + public function insertId() + { + return $this->db_conn->lastInsertRowID(); + } + + /** + * Parse result arrays into expected format for further operations + * + * SQLite does not support to return "e.entryid" within a $row['entryid'] return. + * So this function manually iteratse through all result rows and rewrites 'X.yyyy' to 'yyyy'. + * Yeah. This sucks. Don't tell me! + * + * @access private + * @param resource The row resource handle + * @param int Bitmask to tell whether to fetch numerical/associative arrays + * @return array Propper array containing the resource results + */ + public function sqlite_fetch_array($res, $type = self::SQLITE3_BOTH) + { + static $search = array('%00', '%25'); + static $replace = array("\x00", '%'); + + try { + $row = $res->fetchArray(); + } catch (Exception $e) { + $row = false; + echo "SQLITE-EXCEPTION: " . $e->getMessage() . "\n"; + } + + if (!is_array($row)) { + return $row; + } + + /* strip any slashes, correct fieldname */ + foreach ($row as $i => $v) { + // TODO: If a query of the format 'SELECT a.id, b.text FROM table' is used, + // the sqlite extension will give us key indizes 'a.id' and 'b.text' + // instead of just 'id' and 'text' like in mysql/postgresql extension. + // To fix that, we use a preg-regex; but that is quite performance costy. + // Either we always need to use 'SELECT a.id AS id, b.text AS text' in query, + // or the sqlite extension may get fixed. :-) + $row[preg_replace('@^.+\.(.*)@', '\1', $i)] = str_replace($search, $replace, $v); + } + + if ($type == self::SQLITE3_NUM) { + $frow = array(); + } else { + $frow = $row; + } + + if ($type != self::SQLITE3_ASSOC) { + $i = 0; + foreach ($row as $k => $v) { + $frow[$i] = $v; + $i++; + } + } + + return $frow; + } + + /** + * Assemble and return SQL condition for a "IN (...)" clause + * + * @access public + * @param string table column name + * @param array referenced array of values to search for in the "IN (...)" clause + * @param string condition of how to associate the different input values of the $search_ids parameter + * @return string resulting SQL string + */ + public function inSql($col, &$search_ids, $type = ' OR ') + { + $sql = array(); + if (!is_array($search_ids)) { + return false; + } + + foreach ($search_ids as $id) { + $sql[] = $col . ' = ' . $id; + } + + $cond = '(' . implode($type, $sql) . ')'; + return $cond; + } + + /** + * Perform a DB Layer SQL query. + * + * This function returns values dependin on the input parameters and the result of the query. + * It can return: + * false or a string if there was an error (depends on $expectError), + * true if the query succeeded but did not generate any rows + * array of field values if it returned a single row and $single is true + * array of array of field values if it returned row(s) [stacked array] + * + * @access public + * @param string SQL query to execute + * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! + * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) + * @param boolean If true, errors will be reported. If false, errors will be ignored. + * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column + * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. + * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignroed on DUPLICATE INDEX queries and the likes) + * @return mixed Returns the result of the SQL query, depending on the input parameters + */ + public function &query($sql, $single = false, $result_type = "both", $reportErr = true, $assocKey = false, $assocVal = false, $expectError = false) + { + $type_map = array( + 'assoc' => self::SQLITE3_ASSOC, + 'num' => self::SQLITE3_NUM, + 'both' => self::SQLITE3_BOTH, + 'true' => true, + 'false' => false + ); + + static $debug = false; + + if ($debug) { + // Open file and write directly. In case of crashes, the pointer needs to be killed. + $fp = @fopen('sqlite.log', 'a'); + fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE QUERY: ' . $sql . "\n\n"); + fclose($fp); + } + + if ($reportErr && !$expectError) { + $res = $this->db_conn->query($sql); + } else { + $res = @$this->db_conn->query($sql); + } + + if (!$res) { + if (!$expectError && !$this->serendipity['production']) { + var_dump($res); + var_dump($sql); + $msg = "problem with query"; + return $msg; + } + if ($debug) { + $fp = @fopen('sqlite.log', 'a'); + fwrite($fp, '[' . date('d.m.Y H:i') . '] [ERROR] ' . "\n\n"); + fclose($fp); + } + + return $type_map['false']; + } + + if (!preg_match('@^SELECT@imsU', trim($sql))) { + // Everything that is not SELECT will not return rows. + // SQLite3 OO will always return an object though. + if ($this->db_conn->lastErrorCode() > 0) { + echo "SQLITE-ERROR: " . $this->db_conn->lastErrorMsg() . "
\n"; + return $type_map['false']; + } else { + return $type_map['true']; + } + } + + $rows = array(); + + while (($row = $this->fetchArray($res, $type_map[$result_type]))) { + if (!empty($assocKey)) { + // You can fetch a key-associated array via the two function parameters assocKey and assocVal + if (empty($assocVal)) { + $rows[$row[$assocKey]] = $row; + } else { + $rows[$row[$assocKey]] = $row[$assocVal]; + } + } else { + $rows[] = $row; + } + } + + if ($debug) { + $fp = @fopen('sqlite.log', 'a'); + fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE RESULT: ' . print_r($rows, true). "\n\n"); + fclose($fp); + } + + if ($single && count($rows) == 1) { + return $rows[0]; + } + + if (count($rows) == 0) { + if ($single) { + return $type_map['false']; + } + return $type_map['true']; + } + + return $rows; + } + + /** + * Try to connect to the configured Database (during installation) + * + * @access public + * @param array input configuration array, holding the connection info + * @param array referenced array which holds the errors that might be encountered + * @return boolean return true on success, false on error + */ + public function probe($hash, &$errs) + { + $dbName = (isset($hash['sqlitedbName']) ? $hash['sqlitedbName'] : $hash['dbName']); + + if (!class_exists('SQLite3')) { + $errs[] = 'SQLite extension not installed. Available on PHP 5.4+.'; + return false; + } + + if (defined('S9Y_DATA_PATH')) { + // Shared installations! + $dbfile = S9Y_DATA_PATH . $dbName . '.db'; + } else { + $dbfile = $this->serendipity['serendipityPath'] . $dbName . '.db'; + } + + $this->db_conn = new SQLite3($dbfile); + + if ($this->db_conn) { + return true; + } + + $errs[] = "Unable to open \"$dbfile\" - check permissions (directory needs to be writeable for webserver)!"; + return false; + } + + /** + * Prepares a Serendipty query input to fully valid SQL. Replaces certain "template" variables. + * + * @access public + * @param string SQL query with template variables to convert + * @return resource SQL resource handle of the executed query + */ + public function schemaImport($query) + { + static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', '{FULLTEXT}', '{BOOLEAN}', '{UTF_8}', '{TEXT}'); + static $replace = array('INTEGER AUTOINCREMENT', 'PRIMARY KEY', '', '', 'BOOLEAN NOT NULL', '', 'LONGTEXT'); + + if (stristr($query, '{FULLTEXT_MYSQL}')) { + return true; + } + + $query = trim(str_replace($search, $replace, $query)); + $query = str_replace('INTEGER AUTOINCREMENT PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT', $query); + if ($query[0] == '@') { + // Errors are expected to happen (like duplicate index creation) + return $this->query(substr($query, 1), false, 'both', false, false, false, true); + } else { + return $this->query($query); + } + } + + /** + * Returns the option to a LIMIT SQL statement, because it varies across DB systems + * + * @access public + * @param int Number of the first row to return data from + * @param int Number of rows to return + * @return string SQL string to pass to a LIMIT statement + */ + public function limit($start, $offset) + { + return $start . ', ' . $offset; + } + + /** + * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement + * + * @access public + * @param SQL string of a LIMIT option + * @return SQL string containing a full LIMIT statement + */ + public function limitSql($limitstring) + { + return ' LIMIT ' . $limitstring; + } + + /** + * Returns the SQL code used for concatenating strings + * + * @access public + * @param string Input string/column to concatenate + * @return string SQL parameter + */ + public function concat($string) + { + return 'concat(' . $string . ')'; + } +} diff --git a/lib/Serendipity/Database/SqliteDatabase.php b/lib/Serendipity/Database/SqliteDatabase.php new file mode 100644 index 00000000..213495e7 --- /dev/null +++ b/lib/Serendipity/Database/SqliteDatabase.php @@ -0,0 +1,380 @@ +query('begin transaction'); + } + + /** + * Tells the DB Layer to end a DB transaction. + * + * @access public + * @param boolean If true, perform the query. If false, rollback. + */ + public function endTransaction($commit) + { + if ($commit) { + $this->query('commit transaction'); + } else { + $this->query('rollback transaction'); + } + } + + /** + * Connect to the configured Database + * + * @access public + * @return resource connection handle + */ + public function connect() + { + if (isset($this->db_conn)) { + return $this->db_conn; + } + + if (isset($this->serendipity['dbPersistent']) && $this->serendipity['dbPersistent']) { + $function = 'sqlite_popen'; + } else { + $function = 'sqlite_open'; + } + + if (defined('S9Y_DATA_PATH')) { + $this->db_conn = $function(S9Y_DATA_PATH . $this->db_name . '.db'); + } else { + $this->db_conn = $function($this->db_name . '.db'); + } + + return $this->db_conn; + } + + /** + * Returns an escaped string, so that it can be safely included in a SQL string encapsulated within quotes, without allowing SQL injection. + * + * @access public + * @param string input string + * @return string output string + */ + public function escapeString($string) + { + static $search = array("\x00", '%', "'", '\"'); + static $replace = array('%00', '%25', "''", '\\\"'); + + return str_replace($search, $replace, $string); + } + + /** + * Returns the number of affected rows of a SQL query + * + * @access public + * @return int Number of affected rows + */ + public function affectedRows() + { + return sqlite_changes($this->db_conn); + } + + /** + * Returns the number of updated rows in a SQL query + * + * @access public + * @return int Number of updated rows + */ + public function updatedRows() + { + // It is unknown whether sqllite returns rows MATCHED or rows UPDATED + return sqlite_changes($this->db_conn); + } + + /** + * Returns the number of matched rows in a SQL query + * + * @access public + * @return int Number of matched rows + */ + public function matchedRows() + { + // It is unknown whether sqllite returns rows MATCHED or rows UPDATED + return sqlite_changes($this->db_conn); + } + + /** + * Returns the latest INSERT_ID of an SQL INSERT INTO command, for auto-increment columns + * + * @access public + * @return int Value of the auto-increment column + */ + public function insertId() + { + return sqlite_last_insert_rowid($this->db_conn); + } + + /** + * Parse result arrays into expected format for further operations + * + * SQLite does not support to return "e.entryid" within a $row['entryid'] return. + * So this function manually iteratse through all result rows and rewrites 'X.yyyy' to 'yyyy'. + * Yeah. This sucks. Don't tell me! + * + * @access private + * @param resource The row resource handle + * @param int Bitmask to tell whether to fetch numerical/associative arrays + * @return array Propper array containing the resource results + */ + protected function fetchArray($res, $type = SQLITE_BOTH) + { + static $search = array('%00', '%25'); + static $replace = array("\x00", '%'); + + $row = sqlite_fetch_array($res, $type); + if (!is_array($row)) { + return $row; + } + + /* strip any slashes, correct fieldname */ + foreach ($row as $i => $v) { + // TODO: If a query of the format 'SELECT a.id, b.text FROM table' is used, + // the sqlite extension will give us key indizes 'a.id' and 'b.text' + // instead of just 'id' and 'text' like in mysql/postgresql extension. + // To fix that, we use a preg-regex; but that is quite performance costy. + // Either we always need to use 'SELECT a.id AS id, b.text AS text' in query, + // or the sqlite extension may get fixed. :-) + $row[preg_replace('@^.+\.(.*)@', '\1', $i)] = str_replace($search, $replace, $v); + } + + return $row; + } + + /** + * Assemble and return SQL condition for a "IN (...)" clause + * + * @access public + * @param string table column name + * @param array referenced array of values to search for in the "IN (...)" clause + * @param string condition of how to associate the different input values of the $search_ids parameter + * @return string resulting SQL string + */ + public function inSql($col, &$search_ids, $type = ' OR ') + { + $sql = array(); + if (!is_array($search_ids)) { + return false; + } + + foreach($search_ids AS $id) { + $sql[] = $col . ' = ' . $id; + } + + $cond = '(' . implode($type, $sql) . ')'; + return $cond; + } + + /** + * Perform a DB Layer SQL query. + * + * This function returns values dependin on the input parameters and the result of the query. + * It can return: + * false or a string if there was an error (depends on $expectError), + * true if the query succeeded but did not generate any rows + * array of field values if it returned a single row and $single is true + * array of array of field values if it returned row(s) [stacked array] + * + * @access public + * @param string SQL query to execute + * @param boolean Toggle whether the expected result is a single row (TRUE) or multiple rows (FALSE). This affects whether the returned array is 1 or 2 dimensional! + * @param string Result type of the array indexing. Can be one of "assoc" (associative), "num" (numerical), "both" (numerical and associative, default) + * @param boolean If true, errors will be reported. If false, errors will be ignored. + * @param string A possible array key name, so that you can control the multi-dimensional mapping of an array by the key column + * @param string A possible array field name, so that you can control the multi-dimensional mapping of an array by the key column and the field value. + * @param boolean If true, the executed SQL error is known to fail, and should be disregarded (errors can be ignored on DUPLICATE INDEX queries and the likes) + * @return mixed Returns the result of the SQL query, depending on the input parameters + */ + public function &query($sql, $single = false, $result_type = "both", $reportErr = true, $assocKey = false, $assocVal = false, $expectError = false) + { + $type_map = array( + 'assoc' => SQLITE_ASSOC, + 'num' => SQLITE_NUM, + 'both' => SQLITE_BOTH, + 'true' => true, + 'false' => false + ); + + static $debug = false; + + if ($debug) { + // Open file and write directly. In case of crashes, the pointer needs to be killed. + $fp = @fopen('sqlite.log', 'a'); + fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE QUERY: ' . $sql . "\n\n"); + fclose($fp); + } + + if ($reportErr && !$expectError) { + $res = sqlite_query($sql, $this->db_conn); + } else { + $res = @sqlite_query($sql, $this->db_conn); + } + + if (!$res) { + if (!$expectError && !$this->serendipity['production']) { + var_dump($res); + var_dump($sql); + $msg = "problem with query"; + return $msg; + } + if ($debug) { + $fp = @fopen('sqlite.log', 'a'); + fwrite($fp, '[' . date('d.m.Y H:i') . '] [ERROR] ' . "\n\n"); + fclose($fp); + } + + return $type_map['false']; + } + + if ($res === true) { + return $type_map['true']; + } + + if (sqlite_num_rows($res) == 0) { + if ($single) { + return $type_map['false']; + } + return $type_map['true']; + } else { + $rows = array(); + + while (($row = $this->fetchArray($res, $type_map[$result_type]))) { + if (!empty($assocKey)) { + // You can fetch a key-associated array via the two function parameters assocKey and assocVal + if (empty($assocVal)) { + $rows[$row[$assocKey]] = $row; + } else { + $rows[$row[$assocKey]] = $row[$assocVal]; + } + } else { + $rows[] = $row; + } + } + + if ($debug) { + $fp = @fopen('sqlite.log', 'a'); + fwrite($fp, '[' . date('d.m.Y H:i') . '] SQLITE RESULT: ' . print_r($rows, true). "\n\n"); + fclose($fp); + } + + if ($single && count($rows) == 1) { + return $rows[0]; + } + + return $rows; + } + } + + /** + * Try to connect to the configured Database (during installation) + * + * @access public + * @param array input configuration array, holding the connection info + * @param array referenced array which holds the errors that might be encountered + * @return boolean return true on success, false on error + */ + public function probe($hash, &$errs) + { + $dbName = (isset($hash['sqlitedbName']) ? $hash['sqlitedbName'] : $hash['dbName']); + + if (!function_exists('sqlite_open')) { + $errs[] = 'SQLite extension not installed. Run "pear install sqlite" on your webserver or contact your systems administrator regarding this problem.'; + return false; + } + + if (defined('S9Y_DATA_PATH')) { + // Shared installations! + $dbfile = S9Y_DATA_PATH . $dbName . '.db'; + } else { + $dbfile = $this->serendipity['serendipityPath'] . $dbName . '.db'; + } + + $this->db_conn = sqlite_open($dbfile); + + if ($this->db_conn) { + return true; + } + + $errs[] = "Unable to open \"$dbfile\" - check permissions (directory needs to be writeable for webserver)!"; + return false; + } + + /** + * Prepares a Serendipity query input to fully valid SQL. Replaces certain "template" variables. + * + * @access public + * @param string SQL query with template variables to convert + * @return resource SQL resource handle of the executed query + */ + public function schemaImport($query) + { + static $search = array('{AUTOINCREMENT}', '{PRIMARY}', '{UNSIGNED}', '{FULLTEXT}', '{BOOLEAN}', '{UTF_8}', '{TEXT}'); + static $replace = array('INTEGER', 'PRIMARY KEY', '', '', 'BOOLEAN NOT NULL', '', 'LONGTEXT'); + + if (stristr($query, '{FULLTEXT_MYSQL}')) { + return true; + } + + $query = trim(str_replace($search, $replace, $query)); + if ($query[0] == '@') { + // Errors are expected to happen (like duplicate index creation) + return $this->query(substr($query, 1), false, 'both', false, false, false, true); + } else { + return $this->query($query); + } + } + + /** + * Returns the option to a LIMIT SQL statement, because it varies across DB systems + * + * @access public + * @param int Number of the first row to return data from + * @param int Number of rows to return + * @return string SQL string to pass to a LIMIT statement + */ + public function limit($start, $offset) + { + return $start . ', ' . $offset; + } + + /** + * Return a LIMIT SQL option to the DB Layer as a full LIMIT statement + * + * @access public + * @param SQL string of a LIMIT option + * @return SQL string containing a full LIMIT statement + */ + public function limitSql($limitstring) + { + return ' LIMIT ' . $limitstring; + } + + /** + * Returns the SQL code used for concatenating strings + * + * @access public + * @param string Input string/column to concatenate + * @return string SQL parameter + */ + public function concat($string) + { + return 'concat(' . $string . ')'; + } +}