Passed
Pull Request — master (#3531)
by Martyn
05:56
created

Database_PostgreSQL::seekCounter()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 14
ccs 0
cts 0
cp 0
crap 12
rs 10
1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to the Postgre database.
5
 *
6
 * @name      ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
9
 *
10
 * This file contains code covered by:
11
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
12
 * license:  	BSD, See included LICENSE.TXT for terms and conditions.
13
 *
14
 * @version 1.1.7
15
 *
16
 */
17
18
// Let's define the name of the class so that we will be able to use it in the instantiations
19
if (!defined('DB_TYPE'))
20
	define('DB_TYPE', 'PostgreSQL');
21
22
/**
23
 * PostgreSQL database class, implements database class to control mysql functions
24
 */
25
class Database_PostgreSQL extends Database_Abstract
26
{
27
	/**
28
	 * Holds current instance of the class
29
	 * @var Database_PostgreSQL
30
	 */
31
	private static $_db = null;
32
33
	/**
34
	 * Holds last query result
35
	 * @var string
36
	 */
37
	private $_db_last_result = null;
38
39
	/**
40
	 * Since PostgreSQL doesn't support INSERT REPLACE we are using this to remember
41
	 * the rows affected by the delete
42
	 * @var int
43
	 */
44
	private $_db_replace_result = null;
45
46
	/**
47
	 * A variable to remember if a transaction was started already or not
48
	 * @var boolean
49
	 */
50
	private $_in_transaction = false;
51
52
	/**
53
	 * Initializes a database connection.
54
	 * It returns the connection, if successful.
55
	 *
56
	 * @param string $db_server
57
	 * @param string $db_name
58
	 * @param string $db_user
59
	 * @param string $db_passwd
60
	 * @param string $db_prefix
61
	 * @param mixed[] $db_options
62
	 *
63
	 * @return resource
64
	 * @throws Elk_Exception
65
	 */
66
	public static function initiate($db_server, $db_name, $db_user, $db_passwd, $db_prefix, $db_options = array())
67
	{
68
		// initialize the instance... if not done already!
69
		if (self::$_db === null)
70
			self::$_db = new self();
71
72
		if (!empty($db_options['port']))
73
			$db_port = ' port=' . (int) $db_options['port'];
74
		else
75
			$db_port = '';
76
77
		if (!empty($db_options['persist']))
78
			$connection = @pg_pconnect('host=' . $db_server . $db_port . ' dbname=' . $db_name . ' user=\'' . $db_user . '\' password=\'' . $db_passwd . '\'');
79
		else
80
			$connection = @pg_connect('host=' . $db_server . $db_port . ' dbname=' . $db_name . ' user=\'' . $db_user . '\' password=\'' . $db_passwd . '\'');
81
82
		// Something's wrong, show an error if its fatal (which we assume it is)
83
		if (!$connection)
0 ignored issues
show
introduced by
$connection is of type false|resource, thus it always evaluated to false.
Loading history...
84
		{
85
			if (!empty($db_options['non_fatal']))
86
				return null;
87
			else
88
				Errors::instance()->display_db_error();
89
		}
90
91
		self::$_db->_connection = $connection;
92
93
		return $connection;
94
	}
95
96
	/**
97
	 * Fix the database prefix if necessary.
98
	 * Do nothing on postgreSQL
99
	 *
100
	 * @param string $db_prefix
101
	 * @param string $db_name
102
	 *
103
	 * @return string
104
	 */
105
	public function fix_prefix($db_prefix, $db_name)
106
	{
107
		return $db_prefix;
108
	}
109
110
	/**
111
	 * Do a query.  Takes care of errors too.
112
	 * Special queries may need additional replacements to be appropriate
113
	 * for PostgreSQL.
114
	 *
115
	 * @param string $identifier
116
	 * @param string $db_string
117
	 * @param mixed[] $db_values
118
	 * @param resource|null $connection
119
	 *
120
	 * @return bool|resource|string
121
	 * @throws Elk_Exception
122
	 */
123
	public function query($identifier, $db_string, $db_values = array(), $connection = null)
124
	{
125
		global $db_show_debug, $time_start, $modSettings;
126
127
		// Decide which connection to use.
128
		$connection = $connection === null ? $this->_connection : $connection;
129
130
		// Special queries that need processing.
131
		$replacements = array(
132
			'alter_table' => array(
133
				'~(.+)~' => '',
134
			),
135
			'ban_suggest_error_ips' => array(
136
				'~RLIKE~' => '~',
137
				'~\\.~' => '\.',
138
			),
139
			'ban_suggest_message_ips' => array(
140
				'~RLIKE~' => '~',
141
				'~\\.~' => '\.',
142
			),
143
			'consolidate_spider_stats' => array(
144
				'~MONTH\(log_time\), DAYOFMONTH\(log_time\)~' => 'MONTH(CAST(CAST(log_time AS abstime) AS timestamp)), DAYOFMONTH(CAST(CAST(log_time AS abstime) AS timestamp))',
145
			),
146
			'display_get_post_poster' => array(
147
				'~GROUP BY id_msg\s+HAVING~' => 'AND',
148
			),
149
			'attach_download_increase' => array(
150
				'~LOW_PRIORITY~' => '',
151
			),
152
			'boardindex_fetch_boards' => array(
153
				'~COALESCE\(lb.id_msg, 0\) >= b.id_msg_updated~' => 'CASE WHEN COALESCE(lb.id_msg, 0) >= b.id_msg_updated THEN 1 ELSE 0 END',
154
			),
155
			'get_random_number' => array(
156
				'~RAND~' => 'RANDOM',
157
			),
158
			'insert_log_search_topics' => array(
159
				'~NOT RLIKE~' => '!~',
160
			),
161
			'insert_log_search_results_no_index' => array(
162
				'~NOT RLIKE~' => '!~',
163
			),
164
			'insert_log_search_results_subject' => array(
165
				'~NOT RLIKE~' => '!~',
166
			),
167
			'pm_conversation_list' => array(
168
				'~ORDER\\s+BY\\s+\\{raw:sort\\}~' => 'ORDER BY ' . (isset($db_values['sort']) ? ($db_values['sort'] === 'pm.id_pm' ? 'MAX(pm.id_pm)' : $db_values['sort']) : ''),
169
			),
170
			'top_topic_starters' => array(
171
				'~ORDER BY FIND_IN_SET\(id_member,(.+?)\)~' => 'ORDER BY STRPOS(\',\' || $1 || \',\', \',\' || id_member|| \',\')',
172
			),
173
			'unread_replies' => array(
174
				'~SELECT\\s+DISTINCT\\s+t.id_topic~' => 'SELECT t.id_topic, {raw:sort}',
175
			),
176
			'profile_board_stats' => array(
177
				'~COUNT\(\*\) \/ MAX\(b.num_posts\)~' => 'CAST(COUNT(*) AS DECIMAL) / CAST(b.num_posts AS DECIMAL)',
178
			),
179
		);
180
181
		if (isset($replacements[$identifier]))
182
			$db_string = preg_replace(array_keys($replacements[$identifier]), array_values($replacements[$identifier]), $db_string);
183
184
		// Limits need to be a little different.
185
		$db_string = preg_replace('~\sLIMIT\s(\d+|{int:.+}),\s*(\d+|{int:.+})(.*)~i', 'LIMIT $2 OFFSET $1 $3', $db_string);
186
187
		if (trim($db_string) == '')
188
			return false;
189
190
		// Comments that are allowed in a query are preg_removed.
191
		static $allowed_comments_from = array(
192
			'~\s+~s',
193
			'~/\*!40001 SQL_NO_CACHE \*/~',
194
			'~/\*!40000 USE INDEX \([A-Za-z\_]+?\) \*/~',
195
			'~/\*!40100 ON DUPLICATE KEY UPDATE id_msg = \d+ \*/~',
196
		);
197
		static $allowed_comments_to = array(
198
			' ',
199
			'',
200
			'',
201
			'',
202
		);
203
204
		// One more query....
205
		$this->_query_count++;
206
		$this->_db_replace_result = null;
207
208
		if (empty($modSettings['disableQueryCheck']) && strpos($db_string, '\'') !== false && empty($db_values['security_override']))
209
			$this->error_backtrace('Hacking attempt...', 'Illegal character (\') used in query...', true, __FILE__, __LINE__);
210
211
		if (empty($db_values['security_override']) && (!empty($db_values) || strpos($db_string, '{db_prefix}') !== false))
212
		{
213
			// Store these values for use in the callback function.
214
			$this->_db_callback_values = $db_values;
215
			$this->_db_callback_connection = $connection;
216
217
			// Inject the values passed to this function.
218
			$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', array($this, 'replacement__callback'), $db_string);
219
220
			// No need for them any longer.
221
			$this->_db_callback_values = array();
222
			$this->_db_callback_connection = null;
223
		}
224
225
		// Debugging.
226
		if ($db_show_debug === true)
227
		{
228
			$debug = Debug::instance();
229
230
			// Get the file and line number this function was called.
231
			list ($file, $line) = $this->error_backtrace('', '', 'return', __FILE__, __LINE__);
232
233
			if (!empty($_SESSION['debug_redirect']))
234
			{
235
				$debug->merge_db($_SESSION['debug_redirect']);
236
				// @todo this may be off by 1
237
				$this->_query_count += count($_SESSION['debug_redirect']);
238
				$_SESSION['debug_redirect'] = array();
239
			}
240
241
			// Don't overload it.
242
			$st = microtime(true);
243
			$db_cache = array();
244
			$db_cache['q'] = $this->_query_count < 50 ? $db_string : '...';
245
			$db_cache['f'] = $file;
246
			$db_cache['l'] = $line;
247
			$db_cache['s'] = $st - $time_start;
248
		}
249
250
		// First, we clean strings out of the query, reduce whitespace, lowercase, and trim - so we can check it over.
251
		if (empty($modSettings['disableQueryCheck']))
252
		{
253
			$clean = '';
254
			$old_pos = 0;
255
			$pos = -1;
256
			while (true)
257
			{
258
				$pos = strpos($db_string, '\'', $pos + 1);
259
				if ($pos === false)
260
					break;
261
				$clean .= substr($db_string, $old_pos, $pos - $old_pos);
262
263
				while (true)
264
				{
265
					$pos1 = strpos($db_string, '\'', $pos + 1);
266
					$pos2 = strpos($db_string, '\'\'', $pos + 1);
267
268
					if ($pos1 === false)
269
						break;
270
					elseif ($pos2 === false || $pos2 > $pos1)
271
					{
272
						$pos = $pos1;
273
						break;
274
					}
275
276
					$pos = $pos2 + 1;
277
				}
278
279
				$clean .= ' %s ';
280
				$old_pos = $pos + 1;
281
			}
282
283
			$clean .= substr($db_string, $old_pos);
284
			$clean = trim(strtolower(preg_replace($allowed_comments_from, $allowed_comments_to, $clean)));
285
286
			// Comments?  We don't use comments in our queries, we leave 'em outside!
287
			if (strpos($clean, '/*') > 2 || strpos($clean, '--') !== false || strpos($clean, ';') !== false)
288
				$fail = true;
289
			// Trying to change passwords, slow us down, or something?
290
			elseif (strpos($clean, 'sleep') !== false && preg_match('~(^|[^a-z])sleep($|[^[_a-z])~s', $clean) != 0)
291
				$fail = true;
292
			elseif (strpos($clean, 'benchmark') !== false && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)
293
				$fail = true;
294
295
			if (!empty($fail) && class_exists('Errors'))
296
				$this->error_backtrace('Hacking attempt...', 'Hacking attempt...' . "\n" . $db_string, E_USER_ERROR, __FILE__, __LINE__);
297
298
			// If we are updating something, better start a transaction so that indexes may be kept consistent
299
			if (!$this->_in_transaction && strpos($clean, 'update') !== false)
300
				$this->db_transaction('begin', $connection);
0 ignored issues
show
Bug introduced by
It seems like $connection can also be of type mysqli and postgre; however, parameter $connection of Database_PostgreSQL::db_transaction() does only seem to accept null|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

300
				$this->db_transaction('begin', /** @scrutinizer ignore-type */ $connection);
Loading history...
301
		}
302
303
		$this->_db_last_result = @pg_query($connection, $db_string);
0 ignored issues
show
Bug introduced by
It seems like $connection can also be of type mysqli and postgre; however, parameter $connection of pg_query() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

303
		$this->_db_last_result = @pg_query(/** @scrutinizer ignore-type */ $connection, $db_string);
Loading history...
Documentation Bug introduced by
It seems like @pg_query($connection, $db_string) of type false or resource is incompatible with the declared type string of property $_db_last_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
304
305
		// @deprecated since 1.1 - use skip_next_error method
306
		if (!empty($db_values['db_error_skip']))
307
		{
308
			$this->_skip_error = true;
309
		}
310
311
		if ($this->_db_last_result === false && !$this->_skip_error)
312
		{
313
			$this->error($db_string, $connection);
0 ignored issues
show
Bug introduced by
It seems like $connection can also be of type mysqli and postgre; however, parameter $connection of Database_PostgreSQL::error() does only seem to accept null|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

313
			$this->error($db_string, /** @scrutinizer ignore-type */ $connection);
Loading history...
314
		}
315
316
		// Revert not to skip errors
317
		if ($this->_skip_error === true)
318
		{
319
			$this->_skip_error = false;
320
		}
321
322
		if ($this->_in_transaction)
323
			$this->db_transaction('commit', $connection);
324
325
		// Debugging.
326
		if ($db_show_debug === true)
327
		{
328
			$db_cache['t'] = microtime(true) - $st;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $st does not seem to be defined for all execution paths leading up to this point.
Loading history...
329
			$debug->db_query($db_cache);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $debug does not seem to be defined for all execution paths leading up to this point.
Loading history...
330
		}
331
332
		return $this->_db_last_result;
333
	}
334
335
	/**
336
	 * Affected rows from previous operation.
337
	 *
338
	 * @param resource|null $result
339
	 */
340
	public function affected_rows($result = null)
341
	{
342
		if ($this->_db_replace_result !== null)
343
			return $this->_db_replace_result;
344
		elseif ($result === null && !$this->_db_last_result)
345
			return 0;
346
347
		return pg_affected_rows($result === null ? $this->_db_last_result : $result);
0 ignored issues
show
Bug introduced by
It seems like $result === null ? $this...b_last_result : $result can also be of type string; however, parameter $result of pg_affected_rows() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

347
		return pg_affected_rows(/** @scrutinizer ignore-type */ $result === null ? $this->_db_last_result : $result);
Loading history...
348
	}
349
350
	/**
351
	 * Last inserted id.
352
	 *
353
	 * @param string $table
354
	 * @param string|null $field = null
355
	 * @param resource|null $connection = null
356
	 * @throws Elk_Exception
357
	 */
358
	public function insert_id($table, $field = null, $connection = null)
359
	{
360
		global $db_prefix;
361
362
		$table = str_replace('{db_prefix}', $db_prefix, $table);
363
364
		$connection = $connection === null ? $this->_connection : $connection;
365
366
		// Try get the last ID for the auto increment field.
367
		$request = $this->query('', 'SELECT CURRVAL(\'' . $table . '_seq\') AS insertID',
368
			array(
369
			),
370
			$connection
0 ignored issues
show
Bug introduced by
It seems like $connection can also be of type mysqli and postgre; however, parameter $connection of Database_PostgreSQL::query() does only seem to accept null|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

370
			/** @scrutinizer ignore-type */ $connection
Loading history...
371
		);
372
373
		if (!$request)
0 ignored issues
show
introduced by
$request is of type false|resource, thus it always evaluated to false.
Loading history...
374
			return false;
375
376
		list ($lastID) = $this->fetch_row($request);
377
		$this->free_result($request);
378
379
		return $lastID;
380
	}
381
382
	/**
383
	 * Tracking the current row.
384
	 * Fetch a row from the resultset given as parameter.
385
	 *
386
	 * @param resource $result
387
	 * @param integer|bool $counter = false
388
	 */
389
	public function fetch_row($result, $counter = false)
390
	{
391
		global $db_row_count;
392
393
		if ($counter !== false)
394
			return pg_fetch_row($result, $counter);
0 ignored issues
show
Bug introduced by
It seems like $counter can also be of type true; however, parameter $row of pg_fetch_row() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

394
			return pg_fetch_row($result, /** @scrutinizer ignore-type */ $counter);
Loading history...
395
396
		// Reset the row counter...
397
		if (!isset($db_row_count[$this->seekCounter($result)]))
398
			$db_row_count[$this->seekCounter($result)] = 0;
399
400
		// Return the right row.
401
		return @pg_fetch_row($result, $db_row_count[$this->seekCounter($result)]++);
402
	}
403
404
	/**
405
	 * Free the resultset.
406
	 *
407
	 * @param resource $result
408
	 */
409
	public function free_result($result)
410
	{
411
		global $db_row_count;
412
413
		$id = $this->seekCounter($result);
414
		// Reset the row counter...
415
		if (isset($db_row_count[$id]))
416
			unset($db_row_count[$id]);
417
418
		// Just delegate to the native function
419
		pg_free_result($result);
420
	}
421
422
	/**
423
	 * Get the number of rows in the result.
424
	 *
425
	 * @param resource $result
426
	 */
427
	public function num_rows($result)
428
	{
429
		// simply delegate to the native function
430
		return pg_num_rows($result);
431
	}
432
433
	/**
434
	 * Get the number of fields in the resultset.
435
	 *
436
	 * @param resource $request
437
	 */
438
	public function num_fields($request)
439
	{
440
		return pg_num_fields($request);
441
	}
442
443
	/**
444
	 * Reset the internal result pointer.
445
	 *
446
	 * @param boolean $request
447
	 * @param integer $counter
448
	 */
449
	public function data_seek($request, $counter)
450
	{
451
		global $db_row_count;
452
453
		$db_row_count[$this->seekCounter($request)] = $counter;
454
455
		return true;
456
	}
457
458
	/**
459
	 * Do a transaction.
460
	 *
461
	 * @param string $type - the step to perform (i.e. 'begin', 'commit', 'rollback')
462
	 * @param resource|null $connection = null
463
	 */
464
	public function db_transaction($type = 'commit', $connection = null)
465
	{
466
		// Decide which connection to use
467
		$connection = $connection === null ? $this->_connection : $connection;
468
469
		if ($type == 'begin')
470
		{
471
			$this->_in_transaction = true;
472
			return @pg_query($connection, 'BEGIN');
0 ignored issues
show
Bug introduced by
It seems like $connection can also be of type mysqli and postgre; however, parameter $connection of pg_query() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

472
			return @pg_query(/** @scrutinizer ignore-type */ $connection, 'BEGIN');
Loading history...
473
		}
474
		elseif ($type == 'rollback')
475
			return @pg_query($connection, 'ROLLBACK');
476
		elseif ($type == 'commit')
477
		{
478
			$this->_in_transaction = false;
479
			return @pg_query($connection, 'COMMIT');
480
		}
481
482
		return false;
483
	}
484
485
	/**
486
	 * Return last error string from the database server
487
	 *
488
	 * @param resource|null $connection = null
489
	 */
490
	public function last_error($connection = null)
491
	{
492
		// Decide which connection to use
493
		$connection = $connection === null ? $this->_connection : $connection;
494
495
		if (is_resource($connection) || $connection instanceof \PgSql\Connection)
0 ignored issues
show
Bug introduced by
The type PgSql\Connection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
496
			return pg_last_error($connection);
497
	}
498
499
	/**
500
	 * Database error.
501
	 * Backtrace, log, try to fix.
502
	 *
503
	 * @param string        $db_string
504
	 * @param resource|null $connection = null
505
	 *
506
	 * @throws Elk_Exception
507
	 */
508
	public function error($db_string, $connection = null)
509
	{
510
		global $txt, $context, $modSettings, $db_show_debug;
511
512
		// We'll try recovering the file and line number the original db query was called from.
513
		list ($file, $line) = $this->error_backtrace('', '', 'return', __FILE__, __LINE__);
514
515
		// Decide which connection to use
516
		$connection = $connection === null ? $this->_connection : $connection;
517
518
		// This is the error message...
519
		$query_error = @pg_last_error($connection);
0 ignored issues
show
Bug introduced by
It seems like $connection can also be of type mysqli and postgre; however, parameter $connection of pg_last_error() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

519
		$query_error = @pg_last_error(/** @scrutinizer ignore-type */ $connection);
Loading history...
520
521
		// Log the error.
522
		if (class_exists('Errors'))
523
		{
524
			Errors::instance()->log_error($txt['database_error'] . ': ' . $query_error . (!empty($modSettings['enableErrorQueryLogging']) ? "\n\n" . $db_string : ''), 'database', $file, $line);
525
		}
526
527
		// Nothing's defined yet... just die with it.
528
		if (empty($context) || empty($txt))
529
			die($query_error);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
530
531
		// Show an error message, if possible.
532
		$context['error_title'] = $txt['database_error'];
533
		if (allowedTo('admin_forum'))
534
			$context['error_message'] = nl2br($query_error) . '<br />' . $txt['file'] . ': ' . $file . '<br />' . $txt['line'] . ': ' . $line;
535
		else
536
			$context['error_message'] = $txt['try_again'];
537
538
		// Add database version that we know of, for the admin to know. (and ask for support)
539
		if (allowedTo('admin_forum'))
540
			$context['error_message'] .= '<br /><br />' . sprintf($txt['database_error_versions'], $modSettings['elkVersion']);
541
542
		if (allowedTo('admin_forum') && $db_show_debug === true)
543
			$context['error_message'] .= '<br /><br />' . nl2br($db_string);
544
545
		// It's already been logged... don't log it again.
546
		throw new Elk_Exception($context['error_message'], false);
547
	}
548
549
	/**
550
	 * Insert data.
551
	 *
552
	 * @param string $method - options 'replace', 'ignore', 'insert'
553
	 * @param string $table
554
	 * @param mixed[] $columns
555
	 * @param mixed[] $data
556
	 * @param mixed[] $keys
557
	 * @param bool $disable_trans = false
558
	 * @param resource|null $connection = null
559
	 * @throws Elk_Exception
560
	 */
561
	public function insert($method, $table, $columns, $data, $keys, $disable_trans = false, $connection = null)
562
	{
563
		global $db_prefix;
564
565
		$connection = $connection === null ? $this->_connection : $connection;
566
567
		// With nothing to insert, simply return.
568
		if (empty($data))
569
			return;
570
571
		// Inserting data as a single row can be done as a single array.
572
		if (!is_array($data[array_rand($data)]))
573
			$data = array($data);
574
575
		// Replace the prefix holder with the actual prefix.
576
		$table = str_replace('{db_prefix}', $db_prefix, $table);
577
578
		$priv_trans = false;
579
		if ((count($data) > 1 || $method == 'replace') && !$this->_in_transaction && !$disable_trans)
580
		{
581
			$this->db_transaction('begin', $connection);
0 ignored issues
show
Bug introduced by
It seems like $connection can also be of type mysqli and postgre; however, parameter $connection of Database_PostgreSQL::db_transaction() does only seem to accept null|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

581
			$this->db_transaction('begin', /** @scrutinizer ignore-type */ $connection);
Loading history...
582
			$priv_trans = true;
583
		}
584
585
		// PostgreSQL doesn't support replace: we implement a MySQL-compatible behavior instead
586
		if ($method == 'replace')
587
		{
588
			$count = 0;
589
			$where = '';
590
			$db_replace_result = 0;
591
			foreach ($columns as $columnName => $type)
592
			{
593
				// Are we restricting the length?
594
				if (strpos($type, 'string-') !== false)
595
					$actualType = sprintf($columnName . ' = SUBSTRING({string:%1$s}, 1, ' . substr($type, 7) . '), ', $count);
596
				else
597
					$actualType = sprintf($columnName . ' = {%1$s:%2$s}, ', $type, $count);
598
599
				// A key? That's what we were looking for.
600
				if (in_array($columnName, $keys))
601
					$where .= (empty($where) ? '' : ' AND ') . substr($actualType, 0, -2);
602
				$count++;
603
			}
604
605
			// Make it so.
606
			if (!empty($where) && !empty($data))
607
			{
608
				foreach ($data as $k => $entry)
609
				{
610
					$this->query('', '
611
						DELETE FROM ' . $table .
612
						' WHERE ' . $where,
613
						$entry, $connection
0 ignored issues
show
Bug introduced by
It seems like $connection can also be of type mysqli and postgre; however, parameter $connection of Database_PostgreSQL::query() does only seem to accept null|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

613
						$entry, /** @scrutinizer ignore-type */ $connection
Loading history...
614
					);
615
					$db_replace_result += (!$this->_db_last_result ? 0 : pg_affected_rows($this->_db_last_result));
0 ignored issues
show
Bug introduced by
$this->_db_last_result of type string is incompatible with the type resource expected by parameter $result of pg_affected_rows(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

615
					$db_replace_result += (!$this->_db_last_result ? 0 : pg_affected_rows(/** @scrutinizer ignore-type */ $this->_db_last_result));
Loading history...
616
				}
617
			}
618
		}
619
620
		if (!empty($data))
621
		{
622
			// Create the mold for a single row insert.
623
			$insertData = '(';
624
			foreach ($columns as $columnName => $type)
625
			{
626
				// Are we restricting the length?
627
				if (strpos($type, 'string-') !== false)
628
					$insertData .= sprintf('SUBSTRING({string:%1$s}, 1, ' . substr($type, 7) . '), ', $columnName);
629
				else
630
					$insertData .= sprintf('{%1$s:%2$s}, ', $type, $columnName);
631
			}
632
			$insertData = substr($insertData, 0, -2) . ')';
633
634
			// Create an array consisting of only the columns.
635
			$indexed_columns = array_keys($columns);
636
637
			// Here's where the variables are injected to the query.
638
			$insertRows = array();
639
			foreach ($data as $dataRow)
640
				$insertRows[] = $this->quote($insertData, $this->_array_combine($indexed_columns, $dataRow), $connection);
0 ignored issues
show
Bug introduced by
It seems like $connection can also be of type resource; however, parameter $connection of Database_Abstract::quote() does only seem to accept mysqli|null|postgre, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

640
				$insertRows[] = $this->quote($insertData, $this->_array_combine($indexed_columns, $dataRow), /** @scrutinizer ignore-type */ $connection);
Loading history...
641
642
			$inserted_results = 0;
643
			$skip_error = $method == 'ignore' || $table === $db_prefix . 'log_errors';
644
			foreach ($insertRows as $entry)
645
			{
646
				$this->_skip_error = $skip_error;
647
648
				// Do the insert.
649
				$this->query('', '
650
					INSERT INTO ' . $table . '("' . implode('", "', $indexed_columns) . '")
651
					VALUES
652
						' . $entry,
653
					array(
654
						'security_override' => true,
655
					),
656
					$connection
657
				);
658
				$inserted_results += (!$this->_db_last_result ? 0 : pg_affected_rows($this->_db_last_result));
659
			}
660
			if (isset($db_replace_result))
661
				$this->_db_replace_result = $db_replace_result + $inserted_results;
662
		}
663
664
		if ($priv_trans)
665
			$this->db_transaction('commit', $connection);
666
	}
667
668
	/**
669
	 * Unescape an escaped string!
670
	 *
671
	 * @param string $string
672
	 */
673
	public function unescape_string($string)
674
	{
675
		return strtr($string, array('\'\'' => '\''));
676
	}
677
678
	/**
679
	 * Returns whether the database system supports ignore.
680
	 *
681
	 * @return false
682
	 */
683
	public function support_ignore()
684
	{
685
		return false;
686
	}
687
688
	/**
689
	 * Gets all the necessary INSERTs for the table named table_name.
690
	 * It goes in 250 row segments.
691
	 *
692
	 * @param string $tableName - the table to create the inserts for.
693
	 * @param bool $new_table
694
	 *
695
	 * @return string the query to insert the data back in, or an empty string if the table was empty.
696
	 * @throws Elk_Exception
697
	 */
698
	public function insert_sql($tableName, $new_table = false)
699
	{
700
		global $db_prefix;
701
702
		static $start = 0, $num_rows, $fields, $limit;
703
704
		if ($new_table)
705
		{
706
			$limit = strstr($tableName, 'log_') !== false ? 500 : 250;
707
			$start = 0;
708
		}
709
710
		$data = '';
711
		$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
712
713
		// This will be handy...
714
		$crlf = "\r\n";
715
716
		$result = $this->query('', '
717
			SELECT *
718
			FROM ' . $tableName . '
719
			LIMIT ' . $start . ', ' . $limit,
720
			array(
721
				'security_override' => true,
722
			)
723
		);
724
725
		// The number of rows, just for record keeping and breaking INSERTs up.
726
		$num_rows = $this->num_rows($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $result of Database_PostgreSQL::num_rows() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

726
		$num_rows = $this->num_rows(/** @scrutinizer ignore-type */ $result);
Loading history...
727
728
		if ($num_rows == 0)
729
			return '';
730
731
		if ($new_table)
732
		{
733
			$fields = array_keys($this->fetch_assoc($result));
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $request of Database_PostgreSQL::fetch_assoc() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

733
			$fields = array_keys($this->fetch_assoc(/** @scrutinizer ignore-type */ $result));
Loading history...
734
			$this->data_seek($result, 0);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type resource; however, parameter $request of Database_PostgreSQL::data_seek() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

734
			$this->data_seek(/** @scrutinizer ignore-type */ $result, 0);
Loading history...
735
		}
736
737
		// Start it off with the basic INSERT INTO.
738
		$insert_msg = 'INSERT INTO ' . $tableName . $crlf . "\t" . '(' . implode(', ', $fields) . ')' . $crlf . 'VALUES ' . $crlf . "\t";
739
740
		// Loop through each row.
741
		while ($row = $this->fetch_assoc($result))
742
		{
743
			// Get the fields in this row...
744
			$field_list = array();
745
746
			foreach ($row as $key => $item)
747
			{
748
				// Try to figure out the type of each field. (NULL, number, or 'string'.)
749
				if (!isset($item))
750
					$field_list[] = 'NULL';
751
				elseif (is_numeric($item) && (int) $item == $item)
752
					$field_list[] = $item;
753
				else
754
					$field_list[] = '\'' . $this->escape_string($item) . '\'';
755
			}
756
757
			// 'Insert' the data.
758
			$data .= $insert_msg . '(' . implode(', ', $field_list) . ');' . $crlf;
759
		}
760
		$this->free_result($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $result of Database_PostgreSQL::free_result() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

760
		$this->free_result(/** @scrutinizer ignore-type */ $result);
Loading history...
761
762
		$data .= $crlf;
763
764
		$start += $limit;
765
766
		return $data;
767
	}
768
769
	/**
770
	 * Dumps the schema (CREATE) for a table.
771
	 *
772
	 * @param string $tableName - the table
773
	 *
774
	 * @return string - the CREATE statement as string
775
	 * @throws Elk_Exception
776
	 */
777
	public function db_table_sql($tableName)
778
	{
779
		global $db_prefix;
780
781
		$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
782
783
		// This will be needed...
784
		$crlf = "\r\n";
785
786
		// Start the create table...
787
		$schema_create = 'CREATE TABLE ' . $tableName . ' (' . $crlf;
788
		$index_create = '';
789
		$seq_create = '';
790
791
		// Find all the fields.
792
		$result = $this->query('', '
793
			SELECT column_name, column_default, is_nullable, data_type, character_maximum_length
794
			FROM information_schema.columns
795
			WHERE table_name = {string:table}
796
			ORDER BY ordinal_position',
797
			array(
798
				'table' => $tableName,
799
			)
800
		);
801
		while ($row = $this->fetch_assoc($result))
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $request of Database_PostgreSQL::fetch_assoc() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

801
		while ($row = $this->fetch_assoc(/** @scrutinizer ignore-type */ $result))
Loading history...
802
		{
803
			if ($row['data_type'] == 'character varying')
804
				$row['data_type'] = 'varchar';
805
			elseif ($row['data_type'] == 'character')
806
				$row['data_type'] = 'char';
807
808
			if ($row['character_maximum_length'])
809
				$row['data_type'] .= '(' . $row['character_maximum_length'] . ')';
810
811
			// Make the CREATE for this column.
812
			$schema_create .= ' "' . $row['column_name'] . '" ' . $row['data_type'] . ($row['is_nullable'] != 'YES' ? ' NOT NULL' : '');
813
814
			// Add a default...?
815
			if (trim($row['column_default']) != '')
816
			{
817
				$schema_create .= ' default ' . $row['column_default'] . '';
818
819
				// Auto increment?
820
				if (preg_match('~nextval\(\'(.+?)\'(.+?)*\)~i', $row['column_default'], $matches) != 0)
821
				{
822
					// Get to find the next variable first!
823
					$count_req = $this->query('', '
824
						SELECT MAX("{raw:column}")
825
						FROM {raw:table}',
826
						array(
827
							'column' => $row['column_name'],
828
							'table' => $tableName,
829
						)
830
					);
831
					list ($max_ind) = $this->fetch_row($count_req);
0 ignored issues
show
Bug introduced by
It seems like $count_req can also be of type false; however, parameter $result of Database_PostgreSQL::fetch_row() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

831
					list ($max_ind) = $this->fetch_row(/** @scrutinizer ignore-type */ $count_req);
Loading history...
832
					$this->free_result($count_req);
0 ignored issues
show
Bug introduced by
It seems like $count_req can also be of type false; however, parameter $result of Database_PostgreSQL::free_result() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

832
					$this->free_result(/** @scrutinizer ignore-type */ $count_req);
Loading history...
833
834
					// Get the right bloody start!
835
					$seq_create .= 'CREATE SEQUENCE ' . $matches[1] . ' START WITH ' . ($max_ind + 1) . ';' . $crlf . $crlf;
836
				}
837
			}
838
839
			$schema_create .= ',' . $crlf;
840
		}
841
		$this->free_result($result);
842
843
		// Take off the last comma.
844
		$schema_create = substr($schema_create, 0, -strlen($crlf) - 1);
845
846
		$result = $this->query('', '
847
			SELECT CASE WHEN i.indisprimary THEN 1 ELSE 0 END AS is_primary, pg_get_indexdef(i.indexrelid) AS inddef
848
			FROM pg_class AS c
849
				INNER JOIN pg_index AS i ON (i.indrelid = c.oid)
850
				INNER JOIN pg_class AS c2 ON (c2.oid = i.indexrelid)
851
			WHERE c.relname = {string:table}',
852
			array(
853
				'table' => $tableName,
854
			)
855
		);
856
857
		while ($row = $this->fetch_assoc($result))
858
		{
859
			if ($row['is_primary'])
860
			{
861
				if (preg_match('~\(([^\)]+?)\)~i', $row['inddef'], $matches) == 0)
862
					continue;
863
864
				$index_create .= $crlf . 'ALTER TABLE ' . $tableName . ' ADD PRIMARY KEY ("' . $matches[1] . '");';
865
			}
866
			else
867
				$index_create .= $crlf . $row['inddef'] . ';';
868
		}
869
		$this->free_result($result);
870
871
		// Finish it off!
872
		$schema_create .= $crlf . ');';
873
874
		return $seq_create . $schema_create . $index_create;
875
	}
876
877
	/**
878
	 * {@inheritdoc}
879
	 */
880
	public function db_list_tables($db_name_str = false, $filter = false)
881
	{
882
		$request = $this->query('', '
883
			SELECT tablename
884
			FROM pg_tables
885
			WHERE schemaname = {string:schema_public}' . ($filter === false ? '' : '
886
				AND tablename LIKE {string:filter}') . '
887
			ORDER BY tablename',
888
			array(
889
				'schema_public' => 'public',
890
				'filter' => $filter,
891
			)
892
		);
893
		$tables = array();
894
		while ($row = $this->fetch_row($request))
0 ignored issues
show
Bug introduced by
It seems like $request can also be of type false; however, parameter $result of Database_PostgreSQL::fetch_row() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

894
		while ($row = $this->fetch_row(/** @scrutinizer ignore-type */ $request))
Loading history...
895
			$tables[] = $row[0];
896
		$this->free_result($request);
0 ignored issues
show
Bug introduced by
It seems like $request can also be of type false; however, parameter $result of Database_PostgreSQL::free_result() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

896
		$this->free_result(/** @scrutinizer ignore-type */ $request);
Loading history...
897
898
		return $tables;
899
	}
900
901
	/**
902
	 * Backup $table to $backup_table.
903
	 *
904
	 * @param string $table
905
	 * @param string $backup_table
906
	 * @throws Elk_Exception
907
	 */
908
	public function db_backup_table($table, $backup_table)
909
	{
910
		global $db_prefix;
911
912
		$table = str_replace('{db_prefix}', $db_prefix, $table);
913
914
		// Do we need to drop it first?
915
		$db_table = db_table();
916
		$db_table->db_drop_table($backup_table);
917
918
		// @todo Should we create backups of sequences as well?
919
		$this->query('', '
920
			CREATE TABLE {raw:backup_table}
921
			(
922
				LIKE {raw:table}
923
				INCLUDING DEFAULTS
924
			)',
925
			array(
926
				'backup_table' => $backup_table,
927
				'table' => $table,
928
			)
929
		);
930
931
		$this->query('', '
932
			INSERT INTO {raw:backup_table}
933
			SELECT * FROM {raw:table}',
934
			array(
935
				'backup_table' => $backup_table,
936
				'table' => $table,
937
			)
938
		);
939
	}
940
941
	/**
942
	 * Get the server version number.
943
	 *
944
	 * @return string - the version
945
	 */
946
	public function db_server_version()
947
	{
948
		$version = pg_version();
949
950
		return $version['server'];
951
	}
952
953
	/**
954
	 * Get the name (title) of the database system.
955
	 *
956
	 * @return string
957
	 */
958
	public function db_title()
959
	{
960
		return 'PostgreSQL';
961
	}
962
963
	/**
964
	 * Whether the database system is case sensitive.
965
	 *
966
	 * @return boolean
967
	 */
968
	public function db_case_sensitive()
969
	{
970
		return true;
971
	}
972
973
	/**
974
	 * Quotes identifiers for replacement__callback.
975
	 *
976
	 * @param mixed $replacement
977
	 * @return string
978
	 * @throws Elk_Exception
979
	 */
980
	protected function _replaceIdentifier($replacement)
981
	{
982
		if (preg_match('~[a-z_][0-9,a-z,A-Z$_]{0,60}~', $replacement) !== 1)
983
		{
984
			$this->error_backtrace('Wrong value type sent to the database. Invalid identifier used. (' . $replacement . ')', '', E_USER_ERROR, __FILE__, __LINE__);
985
		}
986
987
		return '"' . $replacement . '"';
988
	}
989
990
	/**
991
	 * Escape string for the database input
992
	 *
993
	 * @param string $string
994
	 */
995
	public function escape_string($string)
996
	{
997
		return pg_escape_string($this->_connection, $string);
0 ignored issues
show
Bug introduced by
It seems like $this->_connection can also be of type mysqli and postgre; however, parameter $connection of pg_escape_string() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

997
		return pg_escape_string(/** @scrutinizer ignore-type */ $this->_connection, $string);
Loading history...
998
	}
999
1000
	/**
1001
	 * Fetch next result as association.
1002
	 *
1003
	 * @param resource $request
1004
	 * @param int|bool $counter = false
1005
	 */
1006
	public function fetch_assoc($request, $counter = false)
1007
	{
1008
		global $db_row_count;
1009
1010
		if ($counter !== false)
1011
			return pg_fetch_assoc($request, $counter);
0 ignored issues
show
Bug introduced by
It seems like $counter can also be of type true; however, parameter $row of pg_fetch_assoc() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1011
			return pg_fetch_assoc($request, /** @scrutinizer ignore-type */ $counter);
Loading history...
1012
1013
		// Reset the row counter...
1014
		if (!isset($db_row_count[$this->seekCounter($request)]))
1015
			$db_row_count[$this->seekCounter($request)] = 0;
1016
1017
		// Return the right row.
1018
		return @pg_fetch_assoc($request, $db_row_count[$this->seekCounter($request)]++);
1019
	}
1020
1021
	/**
1022
	 * Return server info.
1023
	 *
1024
	 * @return string
1025
	 */
1026
	public function db_server_info()
1027
	{
1028
		// give info on client! we use it in install and upgrade and such things.
1029
		$version = pg_version();
1030
1031
		return $version['client'];
1032
	}
1033
1034
	/**
1035
	 * Return client version.
1036
	 *
1037
	 * @return string - the version
1038
	 */
1039
	public function db_client_version()
1040
	{
1041
		$version = pg_version();
1042
1043
		return $version['client'];
1044
	}
1045
1046
	/**
1047
	 * Dummy function really. Doesn't do anything on PostgreSQL.
1048
	 *
1049
	 * @param string|null $dbName = null
1050
	 * @param resource|null $connection = null
1051
	 *
1052
	 * @return boolean
1053
	 */
1054
	public function select_db($dbName = null, $connection = null)
1055
	{
1056
		return true;
1057
	}
1058
1059
	/**
1060
	 * Returns a reference to the existing instance
1061
	 */
1062
	public static function db()
1063
	{
1064
		return self::$_db;
1065
	}
1066
1067
	/**
1068
	 * Finds out if the connection is still valid.
1069
	 *
1070
	 * @param postgre|null $connection = null
0 ignored issues
show
Bug introduced by
The type postgre was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1071
	 */
1072
	public function validConnection($connection = null)
1073
	{
1074
		return (is_resource($connection) || $connection instanceof \PgSql\Connection);
1075
	}
1076
1077
	public function seekCounter($resource)
1078
	{
1079
		global $db_row_count;
1080
1081
		if(!is_resource($resource) && ($resource instanceof \PgSql\Result))
0 ignored issues
show
Bug introduced by
The type PgSql\Result was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1082
		{
1083
			$id = spl_object_id($resource);
1084
		}
1085
		else
1086
		{
1087
			$id = (int)$resource;
1088
		}
1089
1090
		return $id;
1091
	}
1092
}
1093