Completed
Push — master ( e72c23...de047a )
by Jeroen
25:41
created

Database   D

Complexity

Total Complexity 78

Size/Duplication

Total Lines 677
Duplicated Lines 8.27 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 83.98%

Importance

Changes 0
Metric Value
dl 56
loc 677
ccs 173
cts 206
cp 0.8398
rs 4.9824
c 0
b 0
f 0
wmc 78
lcom 1
cbo 10

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A resetConnections() 0 6 1
A getData() 0 3 1
A getDataRow() 0 3 1
A insertData() 13 13 2
A updateData() 0 15 3
A deleteData() 13 13 2
A getConnection() 0 10 3
A setupConnections() 0 8 2
B connect() 0 30 4
B fingerprintCallback() 16 16 5
C getResults() 14 56 12
B executeQuery() 0 31 6
C runSqlScript() 0 36 7
A registerDelayedQuery() 0 14 3
B executeDelayedQueries() 0 22 5
A enableQueryCache() 0 6 3
A disableQueryCache() 0 3 1
A invalidateQueryCache() 0 8 3
A getQueryCount() 0 3 1
A sanitizeInt() 0 11 3
A sanitizeString() 0 10 4
A getServerVersion() 0 8 2
A __get() 0 7 2
A __set() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Database often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Database, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Elgg;
3
4
use Doctrine\DBAL\DriverManager;
5
use Doctrine\DBAL\Connection;
6
use Doctrine\DBAL\Driver\Statement;
7
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
8
use Elgg\Database\DbConfig as DbConfig;
9
10
/**
11
 * The Elgg database
12
 *
13
 * @access private
14
 * @internal Use the public API functions in engine/lib/database.php
15
 *
16
 * @property-read string $prefix Elgg table prefix (read only)
17
 */
18
class Database {
19
	use Profilable;
20
	use Loggable;
21
22
	const DELAYED_QUERY = 'q';
23
	const DELAYED_TYPE = 't';
24
	const DELAYED_HANDLER = 'h';
25
	const DELAYED_PARAMS = 'p';
26
27
	/**
28
	 * @var string $table_prefix Prefix for database tables
29
	 */
30
	private $table_prefix;
31
32
	/**
33
	 * @var Connection[]
34
	 */
35
	private $connections = [];
36
37
	/**
38
	 * @var int $query_count The number of queries made
39
	 */
40
	private $query_count = 0;
41
42
	/**
43
	 * Query cache for select queries.
44
	 *
45
	 * Queries and their results are stored in this cache as:
46
	 * <code>
47
	 * $DB_QUERY_CACHE[query hash] => array(result1, result2, ... resultN)
48
	 * </code>
49
	 * @see \Elgg\Database::getResults() for details on the hash.
50
	 *
51
	 * @var \Elgg\Cache\LRUCache $query_cache The cache
52
	 */
53
	private $query_cache = null;
54
55
	/**
56
	 * @var int $query_cache_size The number of queries to cache
57
	 */
58
	private $query_cache_size = 50;
59
60
	/**
61
	 * Queries are saved as an array with the DELAYED_* constants as keys.
62
	 *
63
	 * @see registerDelayedQuery
64
	 *
65
	 * @var array $delayed_queries Queries to be run during shutdown
66
	 */
67
	private $delayed_queries = [];
68
69
	/**
70
	 * @var \Elgg\Database\DbConfig $config Database configuration
71
	 */
72
	private $config;
73
74
	/**
75
	 * Constructor
76
	 *
77
	 * @param DbConfig $config DB configuration
78
	 */
79 3711
	public function __construct(DbConfig $config) {
80 3711
		$this->resetConnections($config);
81 3711
	}
82
83
	/**
84
	 * Reset the connections with new credentials
85
	 *
86
	 * @param DbConfig $config DB config
87
	 */
88 3711
	public function resetConnections(DbConfig $config) {
89 3711
		$this->connections = [];
90 3711
		$this->config = $config;
91 3711
		$this->table_prefix = $config->getTablePrefix();
92 3711
		$this->enableQueryCache();
93 3711
	}
94
95
	/**
96
	 * Gets (if required, also creates) a DB connection.
97
	 *
98
	 * @param string $type The type of link we want: "read", "write" or "readwrite".
99
	 *
100
	 * @return Connection
101
	 * @throws \DatabaseException
102
	 * @access private
103
	 */
104 293
	public function getConnection($type) {
105 293
		if (isset($this->connections[$type])) {
106 293
			return $this->connections[$type];
107 293
		} else if (isset($this->connections['readwrite'])) {
108 293
			return $this->connections['readwrite'];
109
		} else {
110 293
			$this->setupConnections();
111 293
			return $this->getConnection($type);
112
		}
113
	}
114
115
	/**
116
	 * Establish database connections
117
	 *
118
	 * If the configuration has been set up for multiple read/write databases, set those
119
	 * links up separately; otherwise just create the one database link.
120
	 *
121
	 * @return void
122
	 * @throws \DatabaseException
123
	 * @access private
124
	 */
125 293
	public function setupConnections() {
126 293
		if ($this->config->isDatabaseSplit()) {
127
			$this->connect('read');
128
			$this->connect('write');
129
		} else {
130 293
			$this->connect('readwrite');
131
		}
132 293
	}
133
134
	/**
135
	 * Establish a connection to the database server
136
	 *
137
	 * Connect to the database server and use the Elgg database for a particular database link
138
	 *
139
	 * @param string $type The type of database connection. "read", "write", or "readwrite".
140
	 *
141
	 * @return void
142
	 * @throws \DatabaseException
143
	 * @access private
144
	 */
145 293
	public function connect($type = "readwrite") {
146 293
		$conf = $this->config->getConnectionConfig($type);
147
148
		$params = [
149 293
			'dbname' => $conf['database'],
150 293
			'user' => $conf['user'],
151 293
			'password' => $conf['password'],
152 293
			'host' => $conf['host'],
153 293
			'charset' => $conf['encoding'],
154 293
			'driver' => 'pdo_mysql',
155
		];
156
157
		try {
158 293
			$this->connections[$type] = DriverManager::getConnection($params);
159 293
			$this->connections[$type]->setFetchMode(\PDO::FETCH_OBJ);
160
161
			// https://github.com/Elgg/Elgg/issues/8121
162 293
			$sub_query = "SELECT REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '')";
163 293
			$this->connections[$type]->exec("SET SESSION sql_mode=($sub_query);");
164
		} catch (\Exception $e) {
165
			// http://dev.mysql.com/doc/refman/5.1/en/error-messages-server.html
166
			if ($e->getCode() == 1102 || $e->getCode() == 1049) {
167
				$msg = "Elgg couldn't select the database '{$conf['database']}'. "
168
					. "Please check that the database is created and you have access to it.";
169
			} else {
170
				$msg = "Elgg couldn't connect to the database using the given credentials. Check the settings file.";
171
			}
172
			throw new \DatabaseException($msg);
173
		}
174 293
	}
175
176
	/**
177
	 * Retrieve rows from the database.
178
	 *
179
	 * Queries are executed with {@link \Elgg\Database::executeQuery()} and results
180
	 * are retrieved with {@link \PDO::fetchObject()}.  If a callback
181
	 * function $callback is defined, each row will be passed as a single
182
	 * argument to $callback.  If no callback function is defined, the
183
	 * entire result set is returned as an array.
184
	 *
185
	 * @param string   $query    The query being passed.
186
	 * @param callable $callback Optionally, the name of a function to call back to on each row
187
	 * @param array    $params   Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
188
	 *
189
	 * @return array An array of database result objects or callback function results. If the query
190
	 *               returned nothing, an empty array.
191
	 * @throws \DatabaseException
192
	 */
193 3711
	public function getData($query, $callback = null, array $params = []) {
194 3711
		return $this->getResults($query, $callback, false, $params);
1 ignored issue
show
Bug introduced by
It seems like $callback defined by parameter $callback on line 193 can also be of type callable; however, Elgg\Database::getResults() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
195
	}
196
197
	/**
198
	 * Retrieve a single row from the database.
199
	 *
200
	 * Similar to {@link \Elgg\Database::getData()} but returns only the first row
201
	 * matched.  If a callback function $callback is specified, the row will be passed
202
	 * as the only argument to $callback.
203
	 *
204
	 * @param string   $query    The query to execute.
205
	 * @param callable $callback A callback function to apply to the row
206
	 * @param array    $params   Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
207
	 *
208
	 * @return mixed A single database result object or the result of the callback function.
209
	 * @throws \DatabaseException
210
	 */
211 3711
	public function getDataRow($query, $callback = null, array $params = []) {
212 3711
		return $this->getResults($query, $callback, true, $params);
1 ignored issue
show
Bug introduced by
It seems like $callback defined by parameter $callback on line 211 can also be of type callable; however, Elgg\Database::getResults() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
213
	}
214
215
	/**
216
	 * Insert a row into the database.
217
	 *
218
	 * @note Altering the DB invalidates all queries in the query cache.
219
	 *
220
	 * @param string $query  The query to execute.
221
	 * @param array  $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
222
	 *
223
	 * @return int|false The database id of the inserted row if a AUTO_INCREMENT field is
224
	 *                   defined, 0 if not, and false on failure.
225
	 * @throws \DatabaseException
226
	 */
227 3625 View Code Duplication
	public function insertData($query, array $params = []) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
228
229 3625
		if ($this->logger) {
230 3625
			$this->logger->info("DB query $query");
231
		}
232
233 3625
		$connection = $this->getConnection('write');
234
235 3625
		$this->invalidateQueryCache();
236
237 3625
		$this->executeQuery($query, $connection, $params);
238 3625
		return (int) $connection->lastInsertId();
239
	}
240
241
	/**
242
	 * Update the database.
243
	 *
244
	 * @note Altering the DB invalidates all queries in the query cache.
245
	 *
246
	 * @note WARNING! update_data() has the 2nd and 3rd arguments reversed.
247
	 *
248
	 * @param string $query        The query to run.
249
	 * @param bool   $get_num_rows Return the number of rows affected (default: false).
250
	 * @param array  $params       Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
251
	 *
252
	 * @return bool|int
253
	 * @throws \DatabaseException
254
	 */
255 90
	public function updateData($query, $get_num_rows = false, array $params = []) {
256
257 90
		if ($this->logger) {
258 90
			$this->logger->info("DB query $query");
259
		}
260
261 90
		$this->invalidateQueryCache();
262
263 90
		$stmt = $this->executeQuery($query, $this->getConnection('write'), $params);
264 89
		if ($get_num_rows) {
265 15
			return $stmt->rowCount();
266
		} else {
267 84
			return true;
268
		}
269
	}
270
271
	/**
272
	 * Delete data from the database
273
	 *
274
	 * @note Altering the DB invalidates all queries in query cache.
275
	 *
276
	 * @param string $query  The SQL query to run
277
	 * @param array  $params Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
278
	 *
279
	 * @return int The number of affected rows
280
	 * @throws \DatabaseException
281
	 */
282 219 View Code Duplication
	public function deleteData($query, array $params = []) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
283
284 219
		if ($this->logger) {
285 219
			$this->logger->info("DB query $query");
286
		}
287
288 219
		$connection = $this->getConnection('write');
289
290 219
		$this->invalidateQueryCache();
291
292 219
		$stmt = $this->executeQuery("$query", $connection, $params);
293 218
		return (int) $stmt->rowCount();
294
	}
295
296
	/**
297
	 * Get a string that uniquely identifies a callback during the current request.
298
	 *
299
	 * This is used to cache queries whose results were transformed by the callback. If the callback involves
300
	 * object method calls of the same class, different instances will return different values.
301
	 *
302
	 * @param callable $callback The callable value to fingerprint
303
	 *
304
	 * @return string A string that is unique for each callable passed in
305
	 * @since 1.9.4
306
	 * @access private
307
	 */
308 3616 View Code Duplication
	protected function fingerprintCallback($callback) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
309 3616
		if (is_string($callback)) {
310 3615
			return $callback;
311
		}
312 195
		if (is_object($callback)) {
313 3
			return spl_object_hash($callback) . "::__invoke";
314
		}
315 194
		if (is_array($callback)) {
316 194
			if (is_string($callback[0])) {
317 1
				return "{$callback[0]}::{$callback[1]}";
318
			}
319 194
			return spl_object_hash($callback[0]) . "::{$callback[1]}";
320
		}
321
		// this should not happen
322
		return "";
323
	}
324
325
	/**
326
	 * Handles queries that return results, running the results through a
327
	 * an optional callback function. This is for R queries (from CRUD).
328
	 *
329
	 * @param string $query    The select query to execute
330
	 * @param string $callback An optional callback function to run on each row
331
	 * @param bool   $single   Return only a single result?
332
	 * @param array  $params   Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
333
	 *
334
	 * @return array An array of database result objects or callback function results. If the query
335
	 *               returned nothing, an empty array.
336
	 * @throws \DatabaseException
337
	 */
338 3711
	protected function getResults($query, $callback = null, $single = false, array $params = []) {
339
340
		// Since we want to cache results of running the callback, we need to
341
		// namespace the query with the callback and single result request.
342
		// https://github.com/elgg/elgg/issues/4049
343 3711
		$query_id = (int) $single . $query . '|';
344 3711
		if ($params) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $params of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
345 3711
			$query_id .= serialize($params) . '|';
346
		}
347
348 3711
		if ($callback) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $callback of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
349 3616
			if (!is_callable($callback)) {
350 1
				throw new \RuntimeException('$callback must be a callable function. Given '
351 1
											. _elgg_services()->handlers->describeCallable($callback));
352
			}
353 3616
			$query_id .= $this->fingerprintCallback($callback);
354
		}
355
		// MD5 yields smaller mem usage for cache and cleaner logs
356 3711
		$hash = md5($query_id);
357
358
		// Is cached?
359 3711 View Code Duplication
		if ($this->query_cache) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
360 3711
			if (isset($this->query_cache[$hash])) {
361 3475
				if ($this->logger) {
362 3475
					$this->logger->info("DB query $query results returned from cache (hash: $hash) (params: " . print_r($params, true) . ")");
363
				}
364 3475
				return $this->query_cache[$hash];
365
			}
366
		}
367
368 3711
		$return = [];
369
370 3711
		$stmt = $this->executeQuery($query, $this->getConnection('read'), $params);
371 3711
		while ($row = $stmt->fetch()) {
372 3705
			if ($callback) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $callback of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
373 3589
				$row = call_user_func($callback, $row);
374
			}
375
376 3705
			if ($single) {
377 396
				$return = $row;
378 396
				break;
379
			} else {
380 3705
				$return[] = $row;
381
			}
382
		}
383
384
		// Cache result
385 3711 View Code Duplication
		if ($this->query_cache) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
386 3711
			$this->query_cache[$hash] = $return;
387 3711
			if ($this->logger) {
388 3711
				$this->logger->info("DB query $query results cached (hash: $hash) (params: " . print_r($params, true) . ")");
389
			}
390
		}
391
392 3711
		return $return;
393
	}
394
395
	/**
396
	 * Execute a query.
397
	 *
398
	 * $query is executed via {@link Connection::query}. If there is an SQL error,
399
	 * a {@link DatabaseException} is thrown.
400
	 *
401
	 * @param string     $query      The query
402
	 * @param Connection $connection The DB connection
403
	 * @param array      $params     Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
404
	 *
405
	 * @return Statement The result of the query
406
	 * @throws \DatabaseException
407
	 */
408 3711
	protected function executeQuery($query, Connection $connection, array $params = []) {
409 3711
		if ($query == null) {
410
			throw new \DatabaseException("Query cannot be null");
411
		}
412
413 3711
		$this->query_count++;
414
415 3711
		if ($this->timer) {
416
			$timer_key = preg_replace('~\\s+~', ' ', trim($query . '|' . serialize($params)));
417
			$this->timer->begin(['SQL', $timer_key]);
418
		}
419
420
		try {
421 3711
			if ($params) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $params of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
422 3711
				$value = $connection->executeQuery($query, $params);
423
			} else {
424
				// faster
425 3711
				$value = $connection->query($query);
426
			}
427 3
		} catch (\Exception $e) {
428 3
			throw new \DatabaseException($e->getMessage() . "\n\n"
429 3
			. "QUERY: $query \n\n"
430 3
			. "PARAMS: " . print_r($params, true));
431
		}
432
433 3711
		if ($this->timer) {
434
			$this->timer->end(['SQL', $timer_key]);
0 ignored issues
show
Bug introduced by
The variable $timer_key does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
435
		}
436
437 3711
		return $value;
438
	}
439
440
	/**
441
	 * Runs a full database script from disk.
442
	 *
443
	 * The file specified should be a standard SQL file as created by
444
	 * mysqldump or similar.  Statements must be terminated with ;
445
	 * and a newline character (\n or \r\n).
446
	 *
447
	 * The special string 'prefix_' is replaced with the database prefix
448
	 * as defined in {@link $this->tablePrefix}.
449
	 *
450
	 * @warning Only single line comments are supported. A comment
451
	 * must start with '-- ' or '# ', where the comment sign is at the
452
	 * very beginning of each line.
453
	 *
454
	 * @warning Errors do not halt execution of the script.  If a line
455
	 * generates an error, the error message is saved and the
456
	 * next line is executed.  After the file is run, any errors
457
	 * are displayed as a {@link DatabaseException}
458
	 *
459
	 * @param string $scriptlocation The full path to the script
460
	 *
461
	 * @return void
462
	 * @throws \DatabaseException
463
	 * @access private
464
	 */
465 5
	public function runSqlScript($scriptlocation) {
466 5
		$script = file_get_contents($scriptlocation);
467 5
		if ($script) {
468 5
			$errors = [];
469
470
			// Remove MySQL '-- ' and '# ' style comments
471 5
			$script = preg_replace('/^(?:--|#) .*$/m', '', $script);
472
473
			// Statements must end with ; and a newline
474 5
			$sql_statements = preg_split('/;[\n\r]+/', "$script\n");
475
476 5
			foreach ($sql_statements as $statement) {
477 5
				$statement = trim($statement);
478 5
				$statement = str_replace("prefix_", $this->table_prefix, $statement);
479 5
				if (!empty($statement)) {
480
					try {
481 5
						$this->updateData($statement);
482
					} catch (\DatabaseException $e) {
483 5
						$errors[] = $e->getMessage();
484
					}
485
				}
486
			}
487 5
			if (!empty($errors)) {
488
				$errortxt = "";
489
				foreach ($errors as $error) {
490
					$errortxt .= " {$error};";
491
				}
492
493
				$msg = "There were a number of issues: " . $errortxt;
494 5
				throw new \DatabaseException($msg);
495
			}
496
		} else {
497
			$msg = "Elgg couldn't find the requested database script at " . $scriptlocation . ".";
498
			throw new \DatabaseException($msg);
499
		}
500 5
	}
501
502
	/**
503
	 * Queue a query for execution upon shutdown.
504
	 *
505
	 * You can specify a callback if you care about the result. This function will always
506
	 * be passed a \Doctrine\DBAL\Driver\Statement.
507
	 *
508
	 * @param string   $query    The query to execute
509
	 * @param string   $type     The query type ('read' or 'write')
510
	 * @param callable $callback A callback function to pass the results array to
511
	 * @param array    $params   Query params. E.g. [1, 'steve'] or [':id' => 1, ':name' => 'steve']
512
	 *
513
	 * @return boolean Whether registering was successful.
514
	 * @access private
515
	 */
516 166
	public function registerDelayedQuery($query, $type, $callback = null, array $params = []) {
517 166
		if ($type != 'read' && $type != 'write') {
518
			return false;
519
		}
520
521 166
		$this->delayed_queries[] = [
522 166
			self::DELAYED_QUERY => $query,
523 166
			self::DELAYED_TYPE => $type,
524 166
			self::DELAYED_HANDLER => $callback,
525 166
			self::DELAYED_PARAMS => $params,
526
		];
527
528 166
		return true;
529
	}
530
531
	/**
532
	 * Trigger all queries that were registered as "delayed" queries. This is
533
	 * called by the system automatically on shutdown.
534
	 *
535
	 * @return void
536
	 * @access private
537
	 * @todo make protected once this class is part of public API
538
	 */
539 1
	public function executeDelayedQueries() {
540
541 1
		foreach ($this->delayed_queries as $set) {
542 1
			$query = $set[self::DELAYED_QUERY];
543 1
			$type = $set[self::DELAYED_TYPE];
544 1
			$handler = $set[self::DELAYED_HANDLER];
545 1
			$params = $set[self::DELAYED_PARAMS];
546
547
			try {
548 1
				$stmt = $this->executeQuery($query, $this->getConnection($type), $params);
549
550 1
				if (is_callable($handler)) {
551 1
					call_user_func($handler, $stmt);
552
				}
553
			} catch (\Exception $e) {
554
				if ($this->logger) {
555
					// Suppress all exceptions since page already sent to requestor
556 1
					$this->logger->error($e);
557
				}
558
			}
559
		}
560 1
	}
561
562
	/**
563
	 * Enable the query cache
564
	 *
565
	 * This does not take precedence over the \Elgg\Database\Config setting.
566
	 *
567
	 * @return void
568
	 * @access private
569
	 */
570 3711
	public function enableQueryCache() {
571 3711
		if ($this->config->isQueryCacheEnabled() && $this->query_cache === null) {
572
			// @todo if we keep this cache, expose the size as a config parameter
573 3711
			$this->query_cache = new \Elgg\Cache\LRUCache($this->query_cache_size);
574
		}
575 3711
	}
576
577
	/**
578
	 * Disable the query cache
579
	 *
580
	 * This is useful for special scripts that pull large amounts of data back
581
	 * in single queries.
582
	 *
583
	 * @return void
584
	 * @access private
585
	 */
586 3615
	public function disableQueryCache() {
587 3615
		$this->query_cache = null;
588 3615
	}
589
590
	/**
591
	 * Invalidate the query cache
592
	 *
593
	 * @return void
594
	 */
595 3627
	protected function invalidateQueryCache() {
596 3627
		if ($this->query_cache) {
597 3627
			$this->query_cache->clear();
598 3627
			if ($this->logger) {
599 3627
				$this->logger->info("Query cache invalidated");
600
			}
601
		}
602 3627
	}
603
604
	/**
605
	 * Get the number of queries made to the database
606
	 *
607
	 * @return int
608
	 * @access private
609
	 */
610 1
	public function getQueryCount() {
611 1
		return $this->query_count;
612
	}
613
614
	/**
615
	 * Sanitizes an integer value for use in a query
616
	 *
617
	 * @param int  $value  Value to sanitize
618
	 * @param bool $signed Whether negative values are allowed (default: true)
619
	 * @return int
620
	 * @deprecated Use query parameters where possible
621
	 */
622 3711
	public function sanitizeInt($value, $signed = true) {
623 3711
		$value = (int) $value;
624
625 3711
		if ($signed === false) {
626 3711
			if ($value < 0) {
627
				$value = 0;
628
			}
629
		}
630
631 3711
		return $value;
632
	}
633
634
	/**
635
	 * Sanitizes a string for use in a query
636
	 *
637
	 * @param string $value Value to escape
638
	 * @return string
639
	 * @throws \DatabaseException
640
	 * @deprecated Use query parameters where possible
641
	 */
642 293
	public function sanitizeString($value) {
643 293
		if (is_array($value)) {
644 1
			throw new \DatabaseException(__METHOD__ . '() and serialize_string() cannot accept arrays.');
645
		}
646 293
		$quoted = $this->getConnection('read')->quote($value);
647 293
		if ($quoted[0] !== "'" || substr($quoted, -1) !== "'") {
648
			throw new \DatabaseException("PDO::quote did not return surrounding single quotes.");
649
		}
650 293
		return substr($quoted, 1, -1);
651
	}
652
653
	/**
654
	 * Get the server version number
655
	 *
656
	 * @param string $type Connection type (Config constants, e.g. Config::READ_WRITE)
657
	 *
658
	 * @return string Empty if version cannot be determined
659
	 * @access private
660
	 */
661
	public function getServerVersion($type) {
662
		$driver = $this->getConnection($type)->getWrappedConnection();
663
		if ($driver instanceof ServerInfoAwareConnection) {
664
			return $driver->getServerVersion();
665
		}
666
667
		return null;
668
	}
669
670
	/**
671
	 * Handle magic property reads
672
	 *
673
	 * @param string $name Property name
674
	 * @return mixed
675
	 */
676 3711
	public function __get($name) {
677 3711
		if ($name === 'prefix') {
678 3711
			return $this->table_prefix;
679
		}
680
681
		throw new \RuntimeException("Cannot read property '$name'");
682
	}
683
684
	/**
685
	 * Handle magic property writes
686
	 *
687
	 * @param string $name  Property name
688
	 * @param mixed  $value Value
689
	 * @return void
690
	 */
691
	public function __set($name, $value) {
692
		throw new \RuntimeException("Cannot write property '$name'");
693
	}
694
}
695