diff options
Diffstat (limited to 'engine/lib/database.php')
| -rw-r--r-- | engine/lib/database.php | 1632 |
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'); |
