aboutsummaryrefslogtreecommitdiff
path: root/engine/lib/database.php
diff options
context:
space:
mode:
Diffstat (limited to 'engine/lib/database.php')
-rw-r--r--engine/lib/database.php1632
1 files changed, 735 insertions, 897 deletions
diff --git a/engine/lib/database.php b/engine/lib/database.php
index 73d22d28f..a7949788d 100644
--- a/engine/lib/database.php
+++ b/engine/lib/database.php
@@ -1,926 +1,764 @@
-<?php
-
- /**
- * Elgg database
- * Contains database connection and transfer functionality
- *
- * @package Elgg
- * @subpackage Core
- * @license http://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU Public License version 2
- * @author Curverider Ltd
- * @copyright Curverider Ltd 2008
- * @link http://elgg.org/
- */
-
- /**
- * @class QueryComponent Query component superclass.
- * Component of a query.
- * @author Marcus Povey
- */
- abstract class QueryComponent
- {
- /**
- * Associative array of fields and values
- */
- private $fields;
-
- function __construct()
- {
- $this->fields = array();
- }
-
- /**
- * Class member get overloading
- *
- * @param string $name
- * @return mixed
- */
- function __get($name) {
- return $this->fields[$name];
+<?php
+/**
+ * Elgg database procedural code.
+ *
+ * Includes functions for establishing and retrieving a database link,
+ * reading data, writing data, upgrading DB schemas, and sanitizing input.
+ *
+ * @package Elgg.Core
+ * @subpackage Database
+ */
+
+/**
+ * Query cache for all queries.
+ *
+ * Each query and its results are stored in this cache as:
+ * <code>
+ * $DB_QUERY_CACHE[query hash] => array(result1, result2, ... resultN)
+ * </code>
+ * @see elgg_query_runner() for details on the hash.
+ *
+ * @warning Elgg used to set this as an empty array to turn off the cache
+ *
+ * @global ElggLRUCache|null $DB_QUERY_CACHE
+ * @access private
+ */
+global $DB_QUERY_CACHE;
+$DB_QUERY_CACHE = null;
+
+/**
+ * Queries to be executed upon shutdown.
+ *
+ * These queries are saved to an array and executed using
+ * a function registered by register_shutdown_function().
+ *
+ * Queries are saved as an array in the format:
+ * <code>
+ * $DB_DELAYED_QUERIES[] = array(
+ * 'q' => str $query,
+ * 'l' => resource $dblink,
+ * 'h' => str $handler // a callback function
+ * );
+ * </code>
+ *
+ * @global array $DB_DELAYED_QUERIES
+ * @access private
+ */
+global $DB_DELAYED_QUERIES;
+$DB_DELAYED_QUERIES = array();
+
+/**
+ * Database connection resources.
+ *
+ * Each database link created with establish_db_link($name) is stored in
+ * $dblink as $dblink[$name] => resource. Use get_db_link($name) to retrieve it.
+ *
+ * @global resource[] $dblink
+ * @access private
+ */
+global $dblink;
+$dblink = array();
+
+/**
+ * Database call count
+ *
+ * Each call to the database increments this counter.
+ *
+ * @global integer $dbcalls
+ * @access private
+ */
+global $dbcalls;
+$dbcalls = 0;
+
+/**
+ * Establish a connection to the database servser
+ *
+ * Connect to the database server and use the Elgg database for a particular database link
+ *
+ * @param string $dblinkname The type of database connection. Used to identify the
+ * resource. eg "read", "write", or "readwrite".
+ *
+ * @return void
+ * @throws DatabaseException
+ * @access private
+ */
+function establish_db_link($dblinkname = "readwrite") {
+ // Get configuration, and globalise database link
+ global $CONFIG, $dblink, $DB_QUERY_CACHE;
+
+ if ($dblinkname != "readwrite" && isset($CONFIG->db[$dblinkname])) {
+ if (is_array($CONFIG->db[$dblinkname])) {
+ $index = rand(0, sizeof($CONFIG->db[$dblinkname]));
+ $dbhost = $CONFIG->db[$dblinkname][$index]->dbhost;
+ $dbuser = $CONFIG->db[$dblinkname][$index]->dbuser;
+ $dbpass = $CONFIG->db[$dblinkname][$index]->dbpass;
+ $dbname = $CONFIG->db[$dblinkname][$index]->dbname;
+ } else {
+ $dbhost = $CONFIG->db[$dblinkname]->dbhost;
+ $dbuser = $CONFIG->db[$dblinkname]->dbuser;
+ $dbpass = $CONFIG->db[$dblinkname]->dbpass;
+ $dbname = $CONFIG->db[$dblinkname]->dbname;
}
-
- /**
- * Class member set overloading
- *
- * @param string $name
- * @param mixed $value
- * @return void
- */
- function __set($name, $value) {
- $this->fields[$name] = $value;
-
- return true;
- }
+ } else {
+ $dbhost = $CONFIG->dbhost;
+ $dbuser = $CONFIG->dbuser;
+ $dbpass = $CONFIG->dbpass;
+ $dbname = $CONFIG->dbname;
}
-
- /**
- * @class SelectFieldQueryComponent Class representing a select field.
- * This class represents a select field component.
- * @author Marcus Povey
- */
- class SelectFieldQueryComponent extends QueryComponent
- {
- /**
- * Construct a select field component
- *
- * @param string $table The table containing the field.
- * @param string $field The field or "*"
- */
- function __construct($table, $field)
- {
- global $CONFIG;
-
- $this->table = $CONFIG->dbprefix . sanitise_string($table);
- $this->field = sanitise_string($field);
- }
-
- function __toString()
- {
- return "{$this->table}.{$this->field}";
- }
+
+ // Connect to database
+ if (!$dblink[$dblinkname] = mysql_connect($dbhost, $dbuser, $dbpass, true)) {
+ $msg = elgg_echo('DatabaseException:WrongCredentials',
+ array($dbuser, $dbhost, "****"));
+ throw new DatabaseException($msg);
}
-
- /**
- * @class LimitOffsetQueryComponent
- * Limit and offset clauses of a query.
- * @author Marcus Povey
- */
- class LimitOffsetQueryComponent extends QueryComponent
- {
- /**
- * Specify a limit and an offset.
- *
- * @param int $limit The limit.
- * @param int $offset The offset.
- */
- function __construct($limit = 25, $offset = 0)
- {
- $this->limit = (int)$limit;
- $this->offset = (int)$offset;
- }
-
- function __toString()
- {
- return "limit {$this->offset}, {$this->limit}";
- }
+
+ if (!mysql_select_db($dbname, $dblink[$dblinkname])) {
+ $msg = elgg_echo('DatabaseException:NoConnect', array($dbname));
+ throw new DatabaseException($msg);
}
-
- /**
- * @class OrderQueryComponent
- * Order the query results.
- * @author Marcus Povey
- */
- class OrderQueryComponent extends QueryComponent
- {
- function __construct($table, $field, $order = "asc")
- {
- global $CONFIG;
-
- $this->table = $CONFIG->dbprefix . sanitise_string($table);
- $this->field = sanitise_string($field);
- $this->order = sanitise_string($order);
- }
-
- function __toString()
- {
- return "order by {$this->table}.{$this->field} {$this->order}";
- }
+
+ // Set DB for UTF8
+ mysql_query("SET NAMES utf8");
+
+ $db_cache_off = FALSE;
+ if (isset($CONFIG->db_disable_query_cache)) {
+ $db_cache_off = $CONFIG->db_disable_query_cache;
}
-
- /**
- * @class TableQueryComponent
- * List of tables to select from or insert into.
- * @author Marcus Povey
- */
- class TableQueryComponent extends QueryComponent
- {
- function __construct($table)
- {
- global $CONFIG;
-
- $this->table = $CONFIG->dbprefix . sanitise_string($table);
- }
-
- function __toString()
- {
- return $this->table;
- }
+
+ // Set up cache if global not initialized and query cache not turned off
+ if ((!$DB_QUERY_CACHE) && (!$db_cache_off)) {
+ // @todo if we keep this cache in 1.9, expose the size as a config parameter
+ $DB_QUERY_CACHE = new ElggLRUCache(200);
}
-
- class AccessControlQueryComponent extends QueryComponent
- {
- /**
- * Construct the ACL.
- *
- * @param string $acl_table The table where the access control field is.
- * @param string $acl_field The field containing the access control.
- * @param string $object_owner_table The table containing the owner information for the stuff you're retrieving.
- * @param string $object_owner_id_field The field in $object_owner_table containing
- */
- function __construct($acl_table = "entities", $acl_field = "access_id", $object_owner_table = "entities", $object_owner_id_field = "owner_guid")
- {
- global $CONFIG;
+}
+
+/**
+ * Establish database connections
+ *
+ * If the configuration has been set up for multiple read/write databases, set those
+ * links up separately; otherwise just create the one database link.
+ *
+ * @return void
+ * @access private
+ */
+function setup_db_connections() {
+ global $CONFIG;
+
+ if (!empty($CONFIG->db->split)) {
+ establish_db_link('read');
+ establish_db_link('write');
+ } else {
+ establish_db_link('readwrite');
+ }
+}
+
+/**
+ * Display profiling information about db at NOTICE debug level upon shutdown.
+ *
+ * @return void
+ * @access private
+ */
+function db_profiling_shutdown_hook() {
+ global $dbcalls;
+
+ // demoted to NOTICE as it corrupts javasript at DEBUG
+ elgg_log("DB Queries for this page: $dbcalls", 'NOTICE');
+}
+
+/**
+ * Execute any delayed queries upon shutdown.
+ *
+ * @return void
+ * @access private
+ */
+function db_delayedexecution_shutdown_hook() {
+ global $DB_DELAYED_QUERIES;
+
+ foreach ($DB_DELAYED_QUERIES as $query_details) {
+ try {
+ $link = $query_details['l'];
+
+ if ($link == 'read' || $link == 'write') {
+ $link = get_db_link($link);
+ } elseif (!is_resource($link)) {
+ elgg_log("Link for delayed query not valid resource or db_link type. Query: {$query_details['q']}", 'WARNING');
+ }
- $this->acl_table = $CONFIG->dbprefix . sanitise_string($acl_table);
- $this->acl_field = sanitise_string($acl_field);
- $this->object_owner_table = $CONFIG->dbprefix . sanitise_string($object_owner_table);
- $this->object_owner_id_field = sanitise_string($object_owner_id_field);
- }
-
- function __toString()
- {
- $access = get_access_list();
+ $result = execute_query($query_details['q'], $link);
- return "and ({$this->acl_table}.{$this->acl_field} in {$access} or ({$this->acl_table}.{$this->acl_field} = 0 and {$this->object_owner_table}.{$this->object_owner_id_field} = {$_SESSION['id']}))";
+ if ((isset($query_details['h'])) && (is_callable($query_details['h']))) {
+ $query_details['h']($result);
+ }
+ } catch (Exception $e) {
+ // Suppress all errors since these can't be dealt with here
+ elgg_log($e, 'WARNING');
}
}
-
- /**
- * @class JoinQueryComponent Join query.
- * Represents a join query.
- * @author Marcus Povey
- */
- class JoinQueryComponent extends QueryComponent
- {
- /**
- * Construct a join query.
- * @param string $table Table one to join...
- * @param string $field Field 1 with...
- * @param string $table2 Table 2 ...
- * @param string $field2 Field...
- * @param string $operator Using this operator
- */
- function __construct($table1, $field1, $table2, $field2, $operator = "=")
- {
- global $CONFIG;
-
- $this->table1 = $CONFIG->dbprefix . sanitise_string($table1);
- $this->field1 = sanitise_string($field1);
- $this->table2 = $CONFIG->dbprefix . sanitise_string($table2);
- $this->field2 = sanitise_string($field2);
- $this->operator = sanitise_string($operator);
- }
-
- function __toString()
- {
- return "join {$this->table2} on {$this->$table}.{$this->$field} {$this->$operator} {$this->$table2}.{$this->$field2}";
- }
+}
+
+/**
+ * Returns (if required, also creates) a database link resource.
+ *
+ * Database link resources are stored in the {@link $dblink} global. These
+ * resources are created by {@link setup_db_connections()}, which is called if
+ * no links exist.
+ *
+ * @param string $dblinktype The type of link we want: "read", "write" or "readwrite".
+ *
+ * @return resource Database link
+ * @access private
+ */
+function get_db_link($dblinktype) {
+ global $dblink;
+
+ if (isset($dblink[$dblinktype])) {
+ return $dblink[$dblinktype];
+ } else if (isset($dblink['readwrite'])) {
+ return $dblink['readwrite'];
+ } else {
+ setup_db_connections();
+ return get_db_link($dblinktype);
}
-
- /**
- * @class WhereQueryComponent
- * A component of a where query.
- * @author Marcus Povey
- */
- class WhereQueryComponent extends QueryComponent
- {
- /**
- * A where query.
- *
- * @param string $left_table The table on the left of the operator
- * @param string $left_field The left field
- * @param string $operator The operator eg "=" or "<"
- * @param string $right_table The table on the right of the operator
- * @param string $right_field The right field
- * @param string $link_operator How this where clause links with the previous clause, eg. "and" "or"
- */
- function __construct($left_table, $left_field, $operator, $right_table, $right_field, $link_operator = "and")
- {
- global $CONFIG;
-
- $this->link_operator = sanitise_string($link_operator);
- $this->left_table = $CONFIG->dbprefix . sanitise_string($left_table);
- $this->left_field = sanitise_string($left_field);
- $this->operator = sanitise_string($operator);
- $this->right_table = $CONFIG->dbprefix . sanitise_string($right_table);
- $this->right_field = sanitise_string($right_field);
- }
-
- function __toString()
- {
- return "{$this->link_operator} {$this->left_table }.{$this->left_field} {$this->operator} {$this->right_table}.{$this->right_field}";
- }
+}
+
+/**
+ * Execute an EXPLAIN for $query.
+ *
+ * @param string $query The query to explain
+ * @param mixed $link The database link resource to user.
+ *
+ * @return mixed An object of the query's result, or FALSE
+ * @access private
+ */
+function explain_query($query, $link) {
+ if ($result = execute_query("explain " . $query, $link)) {
+ return mysql_fetch_object($result);
}
-
- /**
- * @class WhereStaticQueryComponent
- * A component of a where query where there is no right hand table, rather a static value.
- * @author Marcus Povey
- */
- class WhereStaticQueryComponent extends WhereQueryComponent
- {
- /**
- * A where query.
- *
- * @param string $left_table The table on the left of the operator
- * @param string $left_field The left field
- * @param string $operator The operator eg "=" or "<"
- * @param string $value The value
- * @param string $link_operator How this where clause links with the previous clause, eg. "and" "or"
- */
- function __construct($left_table, $left_field, $operator, $value, $link_operator = "and")
- {
- global $CONFIG;
-
- $this->link_operator = sanitise_string($link_operator);
- $this->left_table = $CONFIG->dbprefix . sanitise_string($left_table);
- $this->left_field = sanitise_string($left_field);
- $this->operator = sanitise_string($operator);
- if (is_numeric($value))
- $this->value = (int)$value;
- else
- $this->value = sanitise_string($value);
- }
-
- function __toString()
- {
- return "{$this->link_operator} {$this->left_table }.{$this->left_field} {$this->operator} {$this->right_table}.{$this->right_field}";
- }
+
+ return FALSE;
+}
+
+/**
+ * Execute a query.
+ *
+ * $query is executed via {@link mysql_query()}. If there is an SQL error,
+ * a {@link DatabaseException} is thrown.
+ *
+ * @internal
+ * {@link $dbcalls} is incremented and the query is saved into the {@link $DB_QUERY_CACHE}.
+ *
+ * @param string $query The query
+ * @param resource $dblink The DB link
+ *
+ * @return resource result of mysql_query()
+ * @throws DatabaseException
+ * @access private
+ */
+function execute_query($query, $dblink) {
+ global $dbcalls;
+
+ if ($query == NULL) {
+ throw new DatabaseException(elgg_echo('DatabaseException:InvalidQuery'));
}
-
- class WhereSetQueryComponent extends WhereQueryComponent
- {
- /**
- * Construct a subset of wheres.
- *
- * @param array $wheres An array of WhereQueryComponent
- * @param string $link_operator How this where clause links with the previous clause, eg. "and" "or"
- */
- function __construct(array $wheres, $link_operator = "and")
- {
- $this->link_operator = sanitise_string($link_operator);
- $this->wheres = $wheres;
+
+ if (!is_resource($dblink)) {
+ throw new DatabaseException(elgg_echo('DatabaseException:InvalidDBLink'));
+ }
+
+ $dbcalls++;
+
+ $result = mysql_query($query, $dblink);
+
+ if (mysql_errno($dblink)) {
+ throw new DatabaseException(mysql_error($dblink) . "\n\n QUERY: " . $query);
+ }
+
+ return $result;
+}
+
+/**
+ * Queue a query for execution upon shutdown.
+ *
+ * You can specify a handler function if you care about the result. This function will accept
+ * the raw result from {@link mysql_query()}.
+ *
+ * @param string $query The query to execute
+ * @param resource|string $dblink The database link to use or the link type (read | write)
+ * @param string $handler A callback function to pass the results array to
+ *
+ * @return true
+ * @access private
+ */
+function execute_delayed_query($query, $dblink, $handler = "") {
+ global $DB_DELAYED_QUERIES;
+
+ if (!isset($DB_DELAYED_QUERIES)) {
+ $DB_DELAYED_QUERIES = array();
+ }
+
+ if (!is_resource($dblink) && $dblink != 'read' && $dblink != 'write') {
+ return false;
+ }
+
+ // Construct delayed query
+ $delayed_query = array();
+ $delayed_query['q'] = $query;
+ $delayed_query['l'] = $dblink;
+ $delayed_query['h'] = $handler;
+
+ $DB_DELAYED_QUERIES[] = $delayed_query;
+
+ return TRUE;
+}
+
+/**
+ * Write wrapper for execute_delayed_query()
+ *
+ * @param string $query The query to execute
+ * @param string $handler The handler if you care about the result.
+ *
+ * @return true
+ * @uses execute_delayed_query()
+ * @uses get_db_link()
+ * @access private
+ */
+function execute_delayed_write_query($query, $handler = "") {
+ return execute_delayed_query($query, 'write', $handler);
+}
+
+/**
+ * Read wrapper for execute_delayed_query()
+ *
+ * @param string $query The query to execute
+ * @param string $handler The handler if you care about the result.
+ *
+ * @return true
+ * @uses execute_delayed_query()
+ * @uses get_db_link()
+ * @access private
+ */
+function execute_delayed_read_query($query, $handler = "") {
+ return execute_delayed_query($query, 'read', $handler);
+}
+
+/**
+ * Retrieve rows from the database.
+ *
+ * Queries are executed with {@link execute_query()} and results
+ * are retrieved with {@link mysql_fetch_object()}. If a callback
+ * function $callback is defined, each row will be passed as the single
+ * argument to $callback. If no callback function is defined, the
+ * entire result set is returned as an array.
+ *
+ * @param mixed $query The query being passed.
+ * @param string $callback Optionally, the name of a function to call back to on each row
+ *
+ * @return array An array of database result objects or callback function results. If the query
+ * returned nothing, an empty array.
+ * @access private
+ */
+function get_data($query, $callback = "") {
+ return elgg_query_runner($query, $callback, false);
+}
+
+/**
+ * Retrieve a single row from the database.
+ *
+ * Similar to {@link get_data()} but returns only the first row
+ * matched. If a callback function $callback is specified, the row will be passed
+ * as the only argument to $callback.
+ *
+ * @param mixed $query The query to execute.
+ * @param string $callback A callback function
+ *
+ * @return mixed A single database result object or the result of the callback function.
+ * @access private
+ */
+function get_data_row($query, $callback = "") {
+ return elgg_query_runner($query, $callback, true);
+}
+
+/**
+ * Handles returning data from a query, running it through a callback function,
+ * and caching the results. This is for R queries (from CRUD).
+ *
+ * @access private
+ *
+ * @param string $query The query to execute
+ * @param string $callback An optional callback function to run on each row
+ * @param bool $single Return only a single result?
+ *
+ * @return array An array of database result objects or callback function results. If the query
+ * returned nothing, an empty array.
+ * @since 1.8.0
+ * @access private
+ */
+function elgg_query_runner($query, $callback = null, $single = false) {
+ global $DB_QUERY_CACHE;
+
+ // Since we want to cache results of running the callback, we need to
+ // need to namespace the query with the callback and single result request.
+ // https://github.com/elgg/elgg/issues/4049
+ $hash = (string)$callback . (int)$single . $query;
+
+ // Is cached?
+ if ($DB_QUERY_CACHE) {
+ if (isset($DB_QUERY_CACHE[$hash])) {
+ elgg_log("DB query $query results returned from cache (hash: $hash)", 'NOTICE');
+ return $DB_QUERY_CACHE[$hash];
}
-
- function __toString()
- {
- $cnt = 0;
- $string = "{$this->link_operator} (";
- foreach ($this->wheres as $where) {
- if (!($where instanceof WhereQueryComponent))
- throw new DatabaseException("Where set contains non WhereQueryComponent");
-
- if (!$cnt)
- $string .= "{$where->link_operator} ";
-
- $string .= " {$where->left_table }.{$where->left_field} {$where->operator} {$where->right_table}.{$where->right_field}";
- $cnt ++;
+ }
+
+ $dblink = get_db_link('read');
+ $return = array();
+
+ if ($result = execute_query("$query", $dblink)) {
+
+ // test for callback once instead of on each iteration.
+ // @todo check profiling to see if this needs to be broken out into
+ // explicit cases instead of checking in the iteration.
+ $is_callable = is_callable($callback);
+ while ($row = mysql_fetch_object($result)) {
+ if ($is_callable) {
+ $row = $callback($row);
+ }
+
+ if ($single) {
+ $return = $row;
+ break;
+ } else {
+ $return[] = $row;
}
- $string .= ")";
-
- return $string;
}
}
-
- /**
- * @class QueryTypeQueryComponent
- * What type of query is this?
- * @author Marcus Povey
- */
- abstract class QueryTypeQueryComponent extends QueryComponent
- {
- function __toString()
- {
- return $this->query_type;
- }
+
+ if (empty($return)) {
+ elgg_log("DB query $query returned no results.", 'NOTICE');
}
-
- /**
- * @class SelectQueryTypeQueryComponent
- * A select query.
- * @author Marcus Povey
- */
- class SelectQueryTypeQueryComponent extends QueryTypeQueryComponent
- {
- function __construct()
- {
- $this->query_type = "select";
- }
+
+ // Cache result
+ if ($DB_QUERY_CACHE) {
+ $DB_QUERY_CACHE[$hash] = $return;
+ elgg_log("DB query $query results cached (hash: $hash)", 'NOTICE');
}
+
+ return $return;
+}
+
+/**
+ * Insert a row into the database.
+ *
+ * @note Altering the DB invalidates all queries in {@link $DB_QUERY_CACHE}.
+ *
+ * @param mixed $query The query to execute.
+ *
+ * @return int|false The database id of the inserted row if a AUTO_INCREMENT field is
+ * defined, 0 if not, and false on failure.
+ * @access private
+ */
+function insert_data($query) {
+
+ elgg_log("DB query $query", 'NOTICE');
- /**
- * @class InsertQueryTypeQueryComponent
- * An insert query.
- * @author Marcus Povey
- */
- class InsertQueryTypeQueryComponent extends QueryTypeQueryComponent
- {
- function __construct()
- {
- $this->query_type = "insert into";
- }
+ $dblink = get_db_link('write');
+
+ _elgg_invalidate_query_cache();
+
+ if (execute_query("$query", $dblink)) {
+ return mysql_insert_id($dblink);
}
-
- /**
- * @class DeleteQueryTypeQueryComponent
- * A delete query.
- * @author Marcus Povey
- */
- class DeleteQueryTypeQueryComponent extends QueryTypeQueryComponent
- {
- function __construct()
- {
- $this->query_type = "delete from";
- }
+
+ return FALSE;
+}
+
+/**
+ * Update the database.
+ *
+ * @note Altering the DB invalidates all queries in {@link $DB_QUERY_CACHE}.
+ *
+ * @param string $query The query to run.
+ *
+ * @return bool
+ * @access private
+ */
+function update_data($query) {
+
+ elgg_log("DB query $query", 'NOTICE');
+
+ $dblink = get_db_link('write');
+
+ _elgg_invalidate_query_cache();
+
+ if (execute_query("$query", $dblink)) {
+ return TRUE;
}
-
- /**
- * @class UpdateQueryTypeQueryComponent
- * An update query.
- * @author Marcus Povey
- */
- class UpdateQueryTypeQueryComponent extends QueryTypeQueryComponent
- {
- function __construct()
- {
- $this->query_type = "update";
- }
+
+ return FALSE;
+}
+
+/**
+ * Remove data from the database.
+ *
+ * @note Altering the DB invalidates all queries in {@link $DB_QUERY_CACHE}.
+ *
+ * @param string $query The SQL query to run
+ *
+ * @return int|false The number of affected rows or false on failure
+ * @access private
+ */
+function delete_data($query) {
+
+ elgg_log("DB query $query", 'NOTICE');
+
+ $dblink = get_db_link('write');
+
+ _elgg_invalidate_query_cache();
+
+ if (execute_query("$query", $dblink)) {
+ return mysql_affected_rows($dblink);
}
-
- /**
- * @class Query
- * This class provides a framework to construct complex queries in a safe environment.
- *
- * @author Marcus Povey
- */
- class Query
- {
-
- /// The limit of the query
- private $limit_and_offset;
-
- /// Fields to return on a query
- private $fields;
-
- /// Tables to use in a from query
- private $tables;
-
- /// Join tables
- private $joins;
-
- /// Where query
- private $where;
-
- /// Order by
- private $order;
-
- /// The query type
- private $query_type;
-
- /// ACL
- private $access_control;
-
- /**
- * Construct query & initialise variables
- */
- function __construct()
- {
- $this->fields = array();
- $this->tables = array();
- $this->joins = array();
- $this->where = array();
-
- $this->setQueryType(new SelectQueryTypeQueryComponent());
- }
-
- public function setLimitAndOffset(LimitOffsetQueryComponent $component) { $this->limit_and_offset = $component; }
-
- public function setSelectField(SelectFieldQueryComponent $component)
- {
- $this->fields = array();
- return $this->addSelectField($component);
- }
-
- public function addSelectField(SelectFieldQueryComponent $component) { $this->fields[] = $component; }
-
- public function addJoin(JoinQueryComponent $component) { $this->joins[] = $component; }
-
- public function setQueryType(QueryTypeQueryComponent $component) { $this->query_type = $component; }
-
- public function setOrder(OrderQueryComponent $component) { $this->order = $component; }
-
- public function addTable(TableQueryComponent $component) { $this->tables[] = $component; }
-
- public function addWhere(WhereQueryComponent $component) { $this->where[] = $component; }
-
- public function setAccessControl(AccessControlQueryComponent $component) { $this->access_control = $component; }
-
- public function __toString()
- {
- global $CONFIG;
-
- $sql = "";
-
- try
- {
- // Query prefix & fields
- if (!empty($this->query_type))
- {
- $sql .= "{$this->query_type} ";
-
- if (!empty($this->fields))
- {
- $fields = "";
-
- foreach ($this->fields as $field)
- $fields .= "$field";
-
- $sql .= " $fields from ";
- }
- else
- throw new DatabaseException("Fields missing on a select style query");
- }
- else
- throw new DatabaseException("Unrecognised or unspecified query type.");
-
- // Tables
- if (!empty($this->tables))
- {
- foreach($this->tables as $table)
- $sql .= "$table, ";
-
- $sql = trim($sql, ", ");
- }
- else
- throw new DatabaseException("No tables specified for query.");
-
- // Joins on select queries
- if ($this->query_type->query_type == 'select')
- {
- if (!empty($this->joins))
- {
- foreach($this->joins as $join)
- $sql .= "$join ";
- }
+
+ return FALSE;
+}
+
+/**
+ * Invalidate the query cache
+ *
+ * @access private
+ */
+function _elgg_invalidate_query_cache() {
+ global $DB_QUERY_CACHE;
+ if ($DB_QUERY_CACHE instanceof ElggLRUCache) {
+ $DB_QUERY_CACHE->clear();
+ elgg_log("Query cache invalidated", 'NOTICE');
+ } elseif ($DB_QUERY_CACHE) {
+ // In case someone sets the cache to an array and primes it with data
+ $DB_QUERY_CACHE = array();
+ elgg_log("Query cache invalidated", 'NOTICE');
+ }
+}
+
+/**
+ * Return tables matching the database prefix {@link $CONFIG->dbprefix}% in the currently
+ * selected database.
+ *
+ * @return array|false List of tables or false on failure
+ * @static array $tables Tables found matching the database prefix
+ * @access private
+ */
+function get_db_tables() {
+ global $CONFIG;
+ static $tables;
+
+ if (isset($tables)) {
+ return $tables;
+ }
+
+ try{
+ $result = get_data("show tables like '" . $CONFIG->dbprefix . "%'");
+ } catch (DatabaseException $d) {
+ // Likely we can't handle an exception here, so just return false.
+ return FALSE;
+ }
+
+ $tables = array();
+
+ if (is_array($result) && !empty($result)) {
+ foreach ($result as $row) {
+ $row = (array) $row;
+ if (is_array($row) && !empty($row)) {
+ foreach ($row as $element) {
+ $tables[] = $element;
}
-
- // Where
- if (!empty($this->where))
- {
- $sql .= " where 1 ";
-
- foreach ($this->where as $where)
- $sql .= "$where ";
+ }
+ }
+ } else {
+ return FALSE;
+ }
+
+ return $tables;
+}
+
+/**
+ * Optimise a table.
+ *
+ * Executes an OPTIMIZE TABLE query on $table. Useful after large DB changes.
+ *
+ * @param string $table The name of the table to optimise
+ *
+ * @return bool
+ * @access private
+ */
+function optimize_table($table) {
+ $table = sanitise_string($table);
+ return update_data("optimize table $table");
+}
+
+/**
+ * Get the last database error for a particular database link
+ *
+ * @param resource $dblink The DB link
+ *
+ * @return string Database error message
+ * @access private
+ */
+function get_db_error($dblink) {
+ return mysql_error($dblink);
+}
+
+/**
+ * Runs a full database script from disk.
+ *
+ * The file specified should be a standard SQL file as created by
+ * mysqldump or similar. Statements must be terminated with ;
+ * and a newline character (\n or \r\n) with only one statement per line.
+ *
+ * The special string 'prefix_' is replaced with the database prefix
+ * as defined in {@link $CONFIG->dbprefix}.
+ *
+ * @warning Errors do not halt execution of the script. If a line
+ * generates an error, the error message is saved and the
+ * next line is executed. After the file is run, any errors
+ * are displayed as a {@link DatabaseException}
+ *
+ * @param string $scriptlocation The full path to the script
+ *
+ * @return void
+ * @throws DatabaseException
+ * @access private
+ */
+function run_sql_script($scriptlocation) {
+ if ($script = file_get_contents($scriptlocation)) {
+ global $CONFIG;
+
+ $errors = array();
+
+ // Remove MySQL -- style comments
+ $script = preg_replace('/\-\-.*\n/', '', $script);
+
+ // Statements must end with ; and a newline
+ $sql_statements = preg_split('/;[\n\r]+/', $script);
+
+ foreach ($sql_statements as $statement) {
+ $statement = trim($statement);
+ $statement = str_replace("prefix_", $CONFIG->dbprefix, $statement);
+ if (!empty($statement)) {
+ try {
+ update_data($statement);
+ } catch (DatabaseException $e) {
+ $errors[] = $e->getMessage();
}
-
- // Access control
- if (!empty($this->access_control))
- {
-
- // Catch missing Where
- if (empty($this->where))
- $sql .= " where 1 ";
-
- $sql .= "{$this->access_control} ";
- }
- else
- throw new DatabaseException("No access control was provided on query");
-
- // Limits
- if (!empty($this->limit_and_offset))
- $sql .= "{$this->limit_and_offset} ";
-
- // Order by
- if (!empty($this->order))
- $sql .= $this->order;
-
- } catch (Exception $e) {
- trigger_error($e, E_USER_WARNING);
}
-
-
- return $sql;
}
-
+ if (!empty($errors)) {
+ $errortxt = "";
+ foreach ($errors as $error) {
+ $errortxt .= " {$error};";
+ }
+
+ $msg = elgg_echo('DatabaseException:DBSetupIssues') . $errortxt;
+ throw new DatabaseException($msg);
+ }
+ } else {
+ $msg = elgg_echo('DatabaseException:ScriptNotFound', array($scriptlocation));
+ throw new DatabaseException($msg);
+ }
+}
+
+/**
+ * Format a query string for logging
+ *
+ * @param string $query Query string
+ * @return string
+ * @access private
+ */
+function elgg_format_query($query) {
+ // remove newlines and extra spaces so logs are easier to read
+ return preg_replace('/\s\s+/', ' ', $query);
+}
+
+/**
+ * Sanitise a string for database use, but with the option of escaping extra characters.
+ *
+ * @param string $string The string to sanitise
+ * @param string $extra_escapeable Extra characters to escape with '\\'
+ *
+ * @return string The escaped string
+ */
+function sanitise_string_special($string, $extra_escapeable = '') {
+ $string = sanitise_string($string);
+
+ for ($n = 0; $n < strlen($extra_escapeable); $n++) {
+ $string = str_replace($extra_escapeable[$n], "\\" . $extra_escapeable[$n], $string);
}
-
- /**
- * Connect to the database server and use the Elgg database for a particular database link
- *
- * @param string $dblinkname Default "readwrite"; you can change this to set up additional global database links, eg "read" and "write"
- */
- function establish_db_link($dblinkname = "readwrite") {
-
- // Get configuration, and globalise database link
- global $CONFIG, $dblink;
-
- if (!isset($dblink)) {
- $dblink = array();
- }
-
- if ($dblinkname != "readwrite" && isset($CONFIG->db[$dblinkname])) {
- if (is_array($CONFIG->db[$dblinkname])) {
- $index = rand(0,sizeof($CONFIG->db[$dblinkname]));
- $dbhost = $CONFIG->db[$dblinkname][$index]->dbhost;
- $dbuser = $CONFIG->db[$dblinkname][$index]->dbuser;
- $dbpass = $CONFIG->db[$dblinkname][$index]->dbpass;
- $dbname = $CONFIG->db[$dblinkname][$index]->dbname;
- } else {
- $dbhost = $CONFIG->db[$dblinkname]->dbhost;
- $dbuser = $CONFIG->db[$dblinkname]->dbuser;
- $dbpass = $CONFIG->db[$dblinkname]->dbpass;
- $dbname = $CONFIG->db[$dblinkname]->dbname;
- }
- } else {
- $dbhost = $CONFIG->dbhost;
- $dbuser = $CONFIG->dbuser;
- $dbpass = $CONFIG->dbpass;
- $dbname = $CONFIG->dbname;
- }
-
- // Connect to database
- if (!$dblink[$dblinkname] = mysql_connect($CONFIG->dbhost, $CONFIG->dbuser, $CONFIG->dbpass, true))
- throw new DatabaseException("Elgg couldn't connect to the database using the given credentials.");
- if (!mysql_select_db($CONFIG->dbname, $dblink[$dblinkname]))
- throw new DatabaseException("Elgg couldn't select the database {$CONFIG->dbname}.");
-
- }
-
- /**
- * Establish all database connections
- *
- * If the configuration has been set up for multiple read/write databases, set those
- * links up separately; otherwise just create the one database link
- *
- */
-
- function setup_db_connections() {
-
- // Get configuration and globalise database link
- global $CONFIG, $dblink;
-
- if (!empty($CONFIG->db->split)) {
- establish_db_link('read');
- establish_db_link('write');
- } else {
- establish_db_link('readwrite');
- }
-
- }
-
- /**
- * Alias to setup_db_connections, for use in the event handler
- *
- * @param string $event The event type
- * @param string $object_type The object type
- * @param mixed $object Used for nothing in this context
- */
- function init_db($event, $object_type, $object = null) {
- setup_db_connections();
- return true;
- }
-
- /**
- * Gets the appropriate db link for the operation mode requested
- *
- * @param string $dblinktype The type of link we want - "read", "write" or "readwrite" (the default)
- * @return object Database link
- */
- function get_db_link($dblinktype) {
-
- global $dblink;
-
- if (isset($dblink[$dblinktype])) {
- return $dblink[$dblinktype];
- } else {
- return $dblink['readwrite'];
- }
-
+
+ return $string;
+}
+
+/**
+ * Sanitise a string for database use.
+ *
+ * @param string $string The string to sanitise
+ *
+ * @return string Sanitised string
+ */
+function sanitise_string($string) {
+ // @todo does this really need the trim?
+ // there are times when you might want trailing / preceeding white space.
+ return mysql_real_escape_string(trim($string));
+}
+
+/**
+ * Wrapper function for alternate English spelling
+ *
+ * @param string $string The string to sanitise
+ *
+ * @return string Sanitised string
+ */
+function sanitize_string($string) {
+ return sanitise_string($string);
+}
+
+/**
+ * Sanitises an integer for database use.
+ *
+ * @param int $int Value to be sanitized
+ * @param bool $signed Whether negative values should be allowed (true)
+ * @return int
+ */
+function sanitise_int($int, $signed = true) {
+ $int = (int) $int;
+
+ if ($signed === false) {
+ if ($int < 0) {
+ $int = 0;
}
-
- /**
- * Explain a given query, useful for debug.
- */
- function explain_query($query, $link)
- {
- if ($result = mysql_query("explain " . $query, $link)) {
- return mysql_fetch_object($result);
- }
-
- return false;
- }
-
- /**
- * Use this function to get data from the database
- * @param mixed $query The query being passed.
- * @param string $call Optionally, the name of a function to call back to on each row (which takes $row as a single parameter)
- * @return array An array of database result objects
- */
-
- function get_data($query, $callback = "") {
-
- global $CONFIG, $dbcalls;
-
- $dblink = get_db_link('read');
-
- $resultarray = array();
- $dbcalls++;
-
- if ((isset($CONFIG->debug)) && ($CONFIG->debug==true))
- {
- error_log("--- DB QUERY --- $query");
- error_log("--- EXPLAINATION --- " . print_r(explain_query($query,$dblink), true));
- }
-
- if ($result = mysql_query("$query", $dblink)) {
- while ($row = mysql_fetch_object($result)) {
- if (!empty($callback) && is_callable($callback)) {
- $row = $callback($row);
- }
- $resultarray[] = $row;
- }
- }
-
- if (mysql_errno($dblink))
- throw new DatabaseException(mysql_error($dblink) . " QUERY: " . $query);
-
- if (empty($resultarray)) {
- if ((isset($CONFIG->debug)) && ($CONFIG->debug==true))
- error_log("WARNING: DB query \"$query\" returned no results.");
-
- return false;
- }
- return $resultarray;
- }
-
- /**
- * Use this function to get a single data row from the database
- * @param mixed $query The query to run.
- * @return object A single database result object
- */
-
- function get_data_row($query) {
-
- global $CONFIG, $dbcalls;
-
- $dblink = get_db_link('read');
-
- $dbcalls++;
-
- if ((isset($CONFIG->debug)) && ($CONFIG->debug==true))
- {
- error_log("--- DB QUERY --- $query");
- error_log("--- EXPLAINATION --- " . print_r(explain_query($query,$dblink), true));
- }
-
- if ($result = mysql_query("$query", $dblink)) {
- while ($row = mysql_fetch_object($result)) {
- return $row;
- }
- }
-
- if (mysql_errno($dblink))
- throw new DatabaseException(mysql_error($dblink) . " QUERY: " . $query);
-
- if ((isset($CONFIG->debug)) && ($CONFIG->debug==true))
- error_log("WARNING: DB query \"$query\" returned no results.");
-
- return false;
- }
-
- /**
- * Use this function to insert database data; returns id or false
- *
- * @param mixed $query The query to run.
- * @return int $id the database id of the inserted row.
- */
-
- function insert_data($query) {
-
- global $dbcalls;
-
- $dblink = get_db_link('write');
-
- $dbcalls++;
-
- if (mysql_query("$query", $dblink))
- return mysql_insert_id($dblink);
-
- if (mysql_errno($dblink))
- throw new DatabaseException(mysql_error($dblink) . " QUERY: " . $query);
-
- return false;
- }
-
- /**
- * Update database data
- *
- * @param mixed $query The query to run.
- * @return int|false Either the number of affected rows, or false on failure
- */
-
- function update_data($query) {
-
- global $dbcalls;
-
- $dblink = get_db_link('write');
-
- $dbcalls++;
-
- if (mysql_query("$query", $dblink))
- return mysql_affected_rows();
-
- if (mysql_errno($dblink))
- throw new DatabaseException(mysql_error($dblink) . " QUERY: " . $query);
-
- return false;
-
- }
-
- /**
- * Use this function to delete data
- *
- * @param mixed $query The SQL query to run
- * @return int|false Either the number of affected rows, or false on failure
- */
-
- function delete_data($query) {
-
- global $dbcalls;
-
- $dblink = get_db_link('write');
-
- $dbcalls++;
-
- if (mysql_query("$query", $dblink))
- return mysql_affected_rows();
-
- if (mysql_errno($dblink))
- throw new DatabaseException(mysql_error($dblink) . " QUERY: " . $query);
-
- return false;
- }
-
- /**
- * Returns the number of rows returned by the last select statement, without the need to re-execute the query.
- *
- * CANDIDATE FOR DELETION?
- *
- * @return int The number of rows returned by the last statement
- */
- function count_last_select() {
- $row = get_data_row("SELECT found_rows() as count");
- if ($row)
- return $row->count;
- return 0;
- }
-
- /**
- * Get the tables currently installed in the Elgg database
- *
- * @return array List of tables
- */
- function get_db_tables() {
- global $CONFIG;
- $result = get_data("show tables");
-
- $tables = array();
-
- if (is_array($result) && !empty($result)) {
- foreach($result as $row) {
- $row = (array) $row;
- if (is_array($row) && !empty($row))
- foreach($row as $element) {
- $tables[] = $element;
- }
- }
- }
- else
- return false;
-
- return $tables;
- }
-
- /**
- * Get the last database error for a particular database link
- *
- * @param database link $dblink
- * @return string Database error message
- */
- function get_db_error($dblink) {
- return mysql_error($dblink);
- }
-
- /**
- * Runs a full database script from disk
- *
- * @uses $CONFIG
- * @param string $scriptlocation The full path to the script
- */
- function run_sql_script($scriptlocation) {
-
- if ($script = file_get_contents($scriptlocation)) {
-
- global $CONFIG;
-
- $errors = array();
-
- $script = preg_replace('/\-\-.*\n/', '', $script);
- $sql_statements = preg_split('/;[\n\r]+/', $script);
- foreach($sql_statements as $statement) {
- $statement = trim($statement);
- $statement = str_replace("prefix_",$CONFIG->dbprefix,$statement);
- if (!empty($statement)) {
- $result = update_data($statement);
- }
- }
- if (!empty($errors)) {
- $errortxt = "";
- foreach($errors as $error)
- $errortxt .= " {$error};";
- throw new DatabaseException("There were a number of issues: " . $errortxt);
- }
-
- } else {
- throw new DatabaseException("Elgg couldn't find the requested database script at {$scriptlocation}.");
- }
-
- }
-
- /**
- * Sanitise a string for database use
- *
- * @param string $string The string to sanitise
- * @return string Sanitised string
- */
- function sanitise_string($string) {
- return mysql_real_escape_string(trim($string));
- }
-
- /**
- * Wrapper function for Americans
- *
- * @param string $string The string to sanitise
- * @return string Sanitised string
- * @uses sanitise_string
- */
- function sanitize_string($string) {
- return sanitise_string($string);
- }
-
- /**
- * Sanitises an integer for database use
- *
- * @param int $int
- * @return int Sanitised integer
- */
- function sanitise_int($int) {
- return (int) $int;
- }
-
- /**
- * Wrapper function for Americans
- *
- * @param int $int
- * @return int Sanitised integer
- * @uses sanitise_string
- */
- function sanitize_int($int) {
- return (int) $int;
- }
-
- // Stuff for initialisation
-
- register_event_handler('boot','system','init_db',0);
-
-?> \ No newline at end of file
+ }
+
+ return (int) $int;
+}
+
+/**
+ * Sanitizes an integer for database use.
+ * Wrapper function for alternate English spelling (@see sanitise_int)
+ *
+ * @param int $int Value to be sanitized
+ * @param bool $signed Whether negative values should be allowed (true)
+ * @return int
+ */
+function sanitize_int($int, $signed = true) {
+ return sanitise_int($int, $signed);
+}
+
+/**
+ * Registers shutdown functions for database profiling and delayed queries.
+ *
+ * @access private
+ */
+function init_db() {
+ register_shutdown_function('db_delayedexecution_shutdown_hook');
+ register_shutdown_function('db_profiling_shutdown_hook');
+}
+
+elgg_register_event_handler('init', 'system', 'init_db');