Commit 69e6f411 authored by Dries's avatar Dries

- Patch #225450 by Crell, chx, bjaspan, catch, swentel, recidive et al: next...

- Patch #225450 by Crell, chx, bjaspan, catch, swentel, recidive et al: next generation database layer for Drupal 7.
parent 0e795978
......@@ -2,6 +2,11 @@
Drupal 7.0, xxxx-xx-xx (development version)
----------------------
- Database:
* Fully rewritten database layer utilizing PHP 5's PDO abstraction layer.
* Added query builders for INSERT, UPDATE, DELETE, MERGE, and SELECT queries.
* Support for Master/slave replication, transactions, multi-insert queries,
delayed inserts, and other features.
- Security:
* Protected cron.php -- cron will only run if the proper key is provided.
* Changed to much stronger password hashes that are also compatible with the
......
......@@ -323,7 +323,7 @@ function conf_init() {
global $base_url, $base_path, $base_root;
// Export the following settings.php variables to the global namespace
global $db_url, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access;
global $databases, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access;
$conf = array();
if (file_exists('./' . conf_path() . '/settings.php')) {
......@@ -520,11 +520,7 @@ function variable_get($name, $default) {
function variable_set($name, $value) {
global $conf;
$serialized_value = serialize($value);
db_query("UPDATE {variable} SET value = '%s' WHERE name = '%s'", $serialized_value, $name);
if (!db_affected_rows()) {
@db_query("INSERT INTO {variable} (name, value) VALUES ('%s', '%s')", $name, $serialized_value);
}
db_merge('variable')->key(array('name' => $name))->fields(array('value' => serialize($value)))->execute();
cache_clear_all('variables', 'cache');
......@@ -813,24 +809,33 @@ function request_uri() {
function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE, $link = NULL) {
global $user, $base_root;
// Prepare the fields to be logged
$log_message = array(
'type' => $type,
'message' => $message,
'variables' => $variables,
'severity' => $severity,
'link' => $link,
'user' => $user,
'request_uri' => $base_root . request_uri(),
'referer' => referer_uri(),
'ip' => ip_address(),
'timestamp' => time(),
static $in_error_state = FALSE;
// It is possible that the error handling will itself trigger an error. In that case, we could
// end up in an infinite loop. To avoid that, we implement a simple static semaphore.
if (!$in_error_state) {
$in_error_state = TRUE;
// Prepare the fields to be logged
$log_message = array(
'type' => $type,
'message' => $message,
'variables' => $variables,
'severity' => $severity,
'link' => $link,
'user' => $user,
'request_uri' => $base_root . request_uri(),
'referer' => referer_uri(),
'ip' => ip_address(),
'timestamp' => time(),
);
// Call the logging hooks to log/process the message
foreach (module_implements('watchdog', TRUE) as $module) {
module_invoke($module, 'watchdog', $log_message);
// Call the logging hooks to log/process the message
foreach (module_implements('watchdog', TRUE) as $module) {
module_invoke($module, 'watchdog', $log_message);
}
}
$in_error_state = FALSE;
}
/**
......@@ -973,9 +978,24 @@ function drupal_bootstrap($phase) {
$current_phase = $phases[$phase_index];
unset($phases[$phase_index++]);
_drupal_bootstrap($current_phase);
global $_drupal_current_bootstrap_phase;
$_drupal_current_bootstrap_phase = $current_phase;
}
}
/**
* Return the current bootstrap phase for this Drupal process. The
* current phase is the one most recently completed by
* drupal_bootstrap().
*
* @see drupal_bootstrap
*/
function drupal_get_bootstrap_phase() {
global $_drupal_current_bootstrap_phase;
return $_drupal_current_bootstrap_phase;
}
function _drupal_bootstrap($phase) {
global $conf;
......@@ -1003,9 +1023,9 @@ function _drupal_bootstrap($phase) {
break;
case DRUPAL_BOOTSTRAP_DATABASE:
// Initialize the default database.
require_once './includes/database.inc';
db_set_active();
// Initialize the database system. Note that the connection
// won't be initialized until it is actually requested.
require_once './includes/database/database.inc';
// Register autoload functions so that we can access classes and interfaces.
spl_autoload_register('drupal_autoload_class');
spl_autoload_register('drupal_autoload_interface');
......@@ -1209,6 +1229,87 @@ function ip_address($reset = false) {
return $ip_address;
}
/**
* @ingroup schemaapi
* @{
*/
/**
* Get the schema definition of a table, or the whole database schema.
*
* The returned schema will include any modifications made by any
* module that implements hook_schema_alter().
*
* @param $table
* The name of the table. If not given, the schema of all tables is returned.
* @param $rebuild
* If true, the schema will be rebuilt instead of retrieved from the cache.
*/
function drupal_get_schema($table = NULL, $rebuild = FALSE) {
static $schema = array();
if (empty($schema) || $rebuild) {
// Try to load the schema from cache.
if (!$rebuild && $cached = cache_get('schema')) {
$schema = $cached->data;
}
// Otherwise, rebuild the schema cache.
else {
$schema = array();
// Load the .install files to get hook_schema.
// On some databases this function may be called before bootstrap has
// been completed, so we force the functions we need to load just in case.
if (drupal_function_exists('module_load_all_includes')) {
// There is currently a bug in module_list() where it caches what it
// was last called with, which is not always what you want.
// module_load_all_includes() calls module_list(), but if this function
// is called very early in the bootstrap process then it will be
// uninitialized and therefore return no modules. Instead, we have to
// "prime" module_list() here to to values we want, specifically
// "yes rebuild the list and don't limit to bootstrap".
// TODO: Remove this call after http://drupal.org/node/222109 is fixed.
module_list(TRUE, FALSE);
module_load_all_includes('install');
}
// Invoke hook_schema for all modules.
foreach (module_implements('schema') as $module) {
$current = module_invoke($module, 'schema');
require_once('./includes/common.inc');
if (drupal_function_exists('_drupal_initialize_schema')) {
_drupal_initialize_schema($module, $current);
}
$schema = array_merge($schema, $current);
}
if (drupal_function_exists('drupal_alter')) {
drupal_alter('schema', $schema);
}
if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) {
cache_set('schema', $schema);
}
}
}
if (!isset($table)) {
return $schema;
}
elseif (isset($schema[$table])) {
return $schema[$table];
}
else {
return FALSE;
}
}
/**
* @} End of "ingroup schemaapi".
*/
/**
* @ingroup registry
* @{
......@@ -1245,7 +1346,7 @@ function drupal_function_exists($function) {
return TRUE;
}
$file = db_result(db_query("SELECT filename FROM {registry} WHERE name = '%s' AND type = '%s'", $function, 'function'));
$file = db_result(db_query("SELECT filename FROM {registry} WHERE name = :name AND type = :type", array(':name' => $function, ':type' => 'function')));
if ($file) {
require_once($file);
$checked[$function] = function_exists($function);
......@@ -1293,7 +1394,7 @@ function drupal_autoload_class($class) {
* Helper for registry_check_{interface, class}.
*/
function _registry_check_code($type, $name) {
$file = db_result(db_query("SELECT filename FROM {registry} WHERE name = '%s' AND type = '%s'", $name, $type));
$file = db_result(db_query("SELECT filename FROM {registry} WHERE name = :name AND type = :type", array(':name' => $name, ':type' => $type)));
if ($file) {
require_once($file);
registry_mark_code($type, $name);
......@@ -1358,7 +1459,7 @@ function registry_cache_hook_implementations($hook, $write_to_persistent_cache =
if ($write_to_persistent_cache === TRUE) {
// Only write this to cache if the implementations data we are going to cache
// is different to what we loaded earlier in the request.
if ($implementations != registry_get_hook_implementations_cache()) {
if ($implementations != module_implements()) {
cache_set('hooks', $implementations, 'cache_registry');
}
}
......@@ -1412,23 +1513,6 @@ function registry_load_path_files($return = FALSE) {
}
}
/**
* registry_get_hook_implementations_cache
*/
function registry_get_hook_implementations_cache() {
static $implementations;
if ($implementations === NULL) {
if ($cache = cache_get('hooks', 'cache_registry')) {
$implementations = $cache->data;
}
else {
$implementations = array();
}
}
return $implementations;
}
/**
* @} End of "ingroup registry".
*/
......@@ -29,7 +29,6 @@ function cache_get($cid, $table = 'cache') {
// If the data is permanent or we're not enforcing a minimum cache lifetime
// always return the cached data.
if ($cache->expire == CACHE_PERMANENT || !variable_get('cache_lifetime', 0)) {
$cache->data = db_decode_blob($cache->data);
if ($cache->serialized) {
$cache->data = unserialize($cache->data);
}
......@@ -45,7 +44,6 @@ function cache_get($cid, $table = 'cache') {
return FALSE;
}
else {
$cache->data = db_decode_blob($cache->data);
if ($cache->serialized) {
$cache->data = unserialize($cache->data);
}
......@@ -101,16 +99,22 @@ function cache_get($cid, $table = 'cache') {
* A string containing HTTP header information for cached pages.
*/
function cache_set($cid, $data, $table = 'cache', $expire = CACHE_PERMANENT, $headers = NULL) {
$serialized = 0;
$fields = array(
'serialized' => 0,
'created' => time(),
'expire' => $expire,
'headers' => $headers,
);
if (!is_string($data)) {
$data = serialize($data);
$serialized = 1;
$fields['data'] = serialize($data);
$fields['serialized'] = 1;
}
$created = time();
db_query("UPDATE {" . $table . "} SET data = %b, created = %d, expire = %d, headers = '%s', serialized = %d WHERE cid = '%s'", $data, $created, $expire, $headers, $serialized, $cid);
if (!db_affected_rows()) {
@db_query("INSERT INTO {" . $table . "} (cid, data, created, expire, headers, serialized) VALUES ('%s', %b, %d, %d, '%s', %d)", $cid, $data, $created, $expire, $headers, $serialized);
else {
$fields['data'] = $data;
$fields['serialized'] = 0;
}
db_merge($table)->key(array('cid' => $cid))->fields($fields)->execute();
}
/**
......@@ -170,14 +174,14 @@ function cache_clear_all($cid = NULL, $table = NULL, $wildcard = FALSE) {
else {
if ($wildcard) {
if ($cid == '*') {
db_query("DELETE FROM {" . $table . "}");
db_delete($table)->execute();
}
else {
db_query("DELETE FROM {" . $table . "} WHERE cid LIKE '%s%%'", $cid);
db_delete($table)->condition('cid', $cid .'%', 'LIKE')->execute();
}
}
else {
db_query("DELETE FROM {" . $table . "} WHERE cid = '%s'", $cid);
db_delete($table)->condition('cid', $cid)->execute();
}
}
}
......
......@@ -2083,7 +2083,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL) {
// with HTML 4, we need to comment out the CDATA-tag.
$embed_prefix = "\n<!--//--><![CDATA[//><!--\n";
$embed_suffix = "\n//--><!]]>\n";
foreach ($javascript as $type => $data) {
if (!$data) continue;
......@@ -2487,6 +2487,9 @@ function _drupal_bootstrap_full() {
fix_gpc_magic();
// Load all enabled modules
module_load_all();
// Rebuild the module hook cache
module_implements('', NULL, TRUE);
// Let all modules take action before menu system handles the request
// We do not want this while running update.php.
if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
......@@ -2663,7 +2666,6 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1)
return $files;
}
/**
* This dispatch function hands off structured Drupal arrays to type-specific
* *_alter implementations. It ensures a consistent interface for all altering
......@@ -2712,7 +2714,6 @@ function drupal_alter($type, &$data) {
}
}
/**
* Renders HTML given a structured array tree.
*
......@@ -3060,54 +3061,6 @@ function drupal_common_theme() {
* @{
*/
/**
* Get the schema definition of a table, or the whole database schema.
*
* The returned schema will include any modifications made by any
* module that implements hook_schema_alter().
*
* @param $table
* The name of the table. If not given, the schema of all tables is returned.
* @param $rebuild
* If true, the schema will be rebuilt instead of retrieved from the cache.
*/
function drupal_get_schema($table = NULL, $rebuild = FALSE) {
static $schema = array();
if (empty($schema) || $rebuild) {
// Try to load the schema from cache.
if (!$rebuild && $cached = cache_get('schema')) {
$schema = $cached->data;
}
// Otherwise, rebuild the schema cache.
else {
$schema = array();
// Load the .install files to get hook_schema.
module_load_all_includes('install');
// Invoke hook_schema for all modules.
foreach (module_implements('schema') as $module) {
$current = module_invoke($module, 'schema');
_drupal_initialize_schema($module, $current);
$schema = array_merge($schema, $current);
}
drupal_alter('schema', $schema);
cache_set('schema', $schema);
}
}
if (!isset($table)) {
return $schema;
}
elseif (isset($schema[$table])) {
return $schema[$table];
}
else {
return FALSE;
}
}
/**
* Create all tables that a module defines in its hook_schema().
*
......@@ -3153,7 +3106,9 @@ function drupal_uninstall_schema($module) {
$ret = array();
foreach ($schema as $table) {
db_drop_table($ret, $table['name']);
if (db_table_exists($table['name'])) {
db_drop_table($ret, $table['name']);
}
}
return $ret;
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<?php
// $Id$
/**
* @file
* Database interface code for MySQL database servers.
*/
/**
* @ingroup database
* @{
*/
class DatabaseConnection_mysql extends DatabaseConnection {
protected $transactionSupport;
public function __construct(Array $connection_options = array()) {
$connection_options += array(
'transactions' => FALSE,
'port' => 3306,
);
$this->transactionSupport = $connection_options['transactions'];
$dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . $connection_options['port'] . ';dbname=' . $connection_options['database'];
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array(
// So we don't have to mess around with cursors and unbuffered queries by default.
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
// Because MySQL's prepared statements skip the query cache, because it's dumb.
PDO::ATTR_EMULATE_PREPARES => TRUE,
));
}
public function queryRange($query, Array $args, $from, $count, Array $options) {
// Backward compatibility hack, temporary.
$query = str_replace(array('%d' , '%f' , '%b' , "'%s'"), '?', $query);
return $this->query($query . ' LIMIT ' . $from . ', ' . $count, $args, $options);
}
public function queryTemporary($query, Array $args, $tablename) {
$query = preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' Engine=HEAP SELECT', $this->prefixTables($query));
return $this->query($query, $args, $options);
}
public function driver() {
return 'mysql';
}
public function databaseType() {
return 'mysql';
}
public function supportsTransactions() {
return $this->transactionSupport;
}
public function escapeTable($table) {
return preg_replace('/[^A-Za-z0-9_]+/', '', $table);
}
public function mapConditionOperator($operator) {
// We don't want to override any of the defaults.
return NULL;
}
/**
* @todo Remove this as soon as db_rewrite_sql() has been exterminated.
*/
public function distinctField($table, $field, $query) {
$field_to_select = 'DISTINCT(' . $table . '.' . $field . ')';
// (?<!text) is a negative look-behind (no need to rewrite queries that already use DISTINCT).
return preg_replace('/(SELECT.*)(?:' . $table . '\.|\s)(?<!DISTINCT\()(?<!DISTINCT\(' . $table . '\.)' . $field . '(.*FROM )/AUsi', '\1 ' . $field_to_select . '\2', $query);
}
}
/**
* @} End of "ingroup database".
*/
<?php
// $Id$
// MySQL specific install functions
class DatabaseInstaller_mysql extends DatabaseInstaller {
protected $pdoDriver = 'mysql';
public function name() {
return 'MySQL';
}
}
<?php
// $Id$
/**
* @ingroup database
* @{
*/
class InsertQuery_mysql extends InsertQuery {
public function execute() {
// Confirm that the user did not try to specify an identical
// field and default field.
if (array_intersect($this->insertFields, $this->defaultFields)) {
throw new PDOException('You may not specify the same field to have a value and a schema-default value.');
}
$last_insert_id = 0;
$max_placeholder = 0;
$values = array();
foreach ($this->insertValues as $insert_values) {
foreach ($insert_values as $value) {
$values[':db_insert_placeholder_' . $max_placeholder++] = $value;
}
}
$last_insert_id = $this->connection->query((string)$this, $values, $this->queryOptions);
// Re-initialize the values array so that we can re-use this query.
$this->insertValues = array();
return $last_insert_id;
}
public function __toString() {
$delay = $this->queryOptions['delay'] ? 'DELAYED' : '';
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
$query = "INSERT $delay INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
$max_placeholder = 0;
$values = array();
if (count($this->insertValues)) {
foreach ($this->insertValues as $insert_values) {
$placeholders = array();
// Default fields aren't really placeholders, but this is the most convenient
// way to handle them.
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
$new_placeholder = $max_placeholder + count($insert_values);
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
$placeholders[] = ':db_insert_placeholder_'. $i;
}
$max_placeholder = $new_placeholder;
$values[] = '('. implode(', ', $placeholders) .')';
}
}
else {
// If there are no values, then this is a default-only query. We still need to handle that.
$placeholders = array_fill(0, count($this->defaultFields), 'default');
$values[] = '(' . implode(', ', $placeholders) .')';
}
$query .= implode(', ', $values);
return $query;
}
}
class MergeQuery_mysql extends MergeQuery {
public function execute() {
// Set defaults.
if ($this->updateFields) {
$update_fields = $this->updateFields;
}
else {
$update_fields = $this->insertFields;
// If there are no exclude fields, this is a no-op.
foreach ($this->excludeFields as $exclude_field) {
unset($update_fields[$exclude_field]);
}
}
$insert_fields = $this->insertFields + $this->keyFields;
$max_placeholder = 0;
$values = array();
// We assume that the order here is the same as in __toString(). If that's
// not the case, then we have serious problems.
foreach ($insert_fields as $value) {
$values[':db_insert_placeholder_' . $max_placeholder++] = $value;
}
// Expressions take priority over literal fields, so we process those first
// and remove any literal fields that conflict.
foreach ($this->expressionFields as $field => $data) {
if (!empty($data['arguments'])) {
$values += $data['arguments'];
}
unset($update_fields[$field]);
}
// Because we filter $fields the same way here and in __toString(), the
// placeholders will all match up properly.
$max_placeholder = 0;
foreach ($update_fields as $field => $value) {
$values[':db_update_placeholder_' . ($max_placeholder++)] = $value;
}
$last_insert_id = $this->connection->query((string)$this, $values, $this->queryOptions);
return $last_insert_id;
}
public function __toString() {
// Set defaults.
$update_fields = array();
if ($this->updateFields) {
$update_fields = $this->updateFields;
}
else {
$update_fields = $this->insertFields;
// If there are no exclude fields, this is a no-op.
foreach ($this->excludeFields as $exclude_field) {
unset($update_fields[$exclude_field]);
}
}
$insert_fields = $this->insertFields + $this->keyFields;
$query = "INSERT INTO {" . $this->table . '} (' . implode(', ', array_keys($insert_fields)) . ') VALUES ';
$max_placeholder = 0;
$values = array();
// We don't need the $field, but this is a convenient way to count.
foreach ($insert_fields as $field) {
$values[] = ':db_insert_placeholder_' . $max_placeholder++;
}
$query .= '(' . implode(', ', $values) . ') ON DUPLICATE KEY UPDATE ';
// Expressions take priority over literal fields, so we process those first
// and remove any literal fields that conflict.
$max_placeholder = 0;
$update = array();
foreach ($this->expressionFields as $field => $data) {
$update[] = $field . '=' . $data['expression'];
unset($update_fields[$field]);
}
foreach ($update_fields as $field => $value) {
$update[] = ($field . '=:db_update_placeholder_' . $max_placeholder++);
}
$query .= implode(', ', $update);
return $query;
}
}
/**
* @} End of "ingroup database".
*/
<?php
// $Id$
/**
* @file
* Database schema code for MySQL database servers.
*/
/**
* @ingroup schemaapi
* @{
*/
class DatabaseSchema_mysql extends DatabaseSchema {
public function tableExists($table) {