Completed
Pull Request — development (#2979)
by Stephen
08:55
created

Database_MySQL   D

Complexity

Total Complexity 229

Size/Duplication

Total Lines 1240
Duplicated Lines 13.47 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
dl 167
loc 1240
ccs 0
cts 710
cp 0
rs 4.4102
c 0
b 0
f 0
wmc 229
lcom 1
cbo 7

32 Methods

Rating   Name   Duplication   Size   Complexity  
C initiate() 11 54 10
A fix_prefix() 0 6 2
B _uniord() 4 17 15
A num_fields() 0 4 1
A data_seek() 0 5 1
B db_transaction() 0 14 5
A last_error() 0 8 3
F error() 22 202 58
A unescape_string() 0 4 1
A support_ignore() 0 4 1
C insert_sql() 23 68 10
F db_table_sql() 0 104 25
F db_backup_table() 0 123 20
A db_server_version() 12 12 1
A db_title() 0 4 1
A db_case_sensitive() 0 4 1
A db_server_info() 0 7 2
A db_client_version() 12 12 1
A select_db() 0 7 2
F query() 75 156 37
C _clean_4byte_chars() 0 40 7
A affected_rows() 0 4 2
A insert_id() 0 5 2
A fetch_row() 0 5 1
A free_result() 0 5 1
A num_rows() 0 5 1
B insert() 8 56 9
B db_list_tables() 0 26 5
A escape_string() 0 6 1
A fetch_assoc() 0 4 1
A db() 0 4 1
A validConnection() 0 4 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_MySQL 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_MySQL, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to the mysql 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
 * copyright:	2004-2011, GreyWyvern - All rights reserved.
15
 * license:  	BSD, See included LICENSE.TXT for terms and conditions.
16
 *
17
 * @version 1.1 Release Candidate 1
18
 *
19
 */
20
21
// Let's define the name of the class so that we will be able to use it in the instantiations
22
if (!defined('DB_TYPE'))
23
	define('DB_TYPE', 'MySQL');
24
25
/**
26
 * SQL database class, implements database class to control mysql functions
27
 */
28
class Database_MySQL extends Database_Abstract
29
{
30
	/**
31
	 * Holds current instance of the class
32
	 *
33
	 * @var Database_MySQL
34
	 */
35
	private static $_db = null;
36
37
	/**
38
	 * Initializes a database connection.
39
	 * It returns the connection, if successful.
40
	 *
41
	 * @param string $db_server
42
	 * @param string $db_name
43
	 * @param string $db_user
44
	 * @param string $db_passwd
45
	 * @param string $db_prefix
46
	 * @param mixed[] $db_options
47
	 *
48
	 * @return mysqli|null
49
	 * @throws Elk_Exception
50
	 */
51
	public static function initiate($db_server, $db_name, $db_user, $db_passwd, $db_prefix, $db_options = array())
0 ignored issues
show
Unused Code introduced by
The parameter $db_prefix is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
52
	{
53
		global $mysql_set_mode;
54
55
		// Initialize the instance... if not done already!
56
		if (self::$_db === null)
57
			self::$_db = new self();
58
59
		// Non-standard port
60 View Code Duplication
		if (!empty($db_options['port']))
61
			$db_port = (int) $db_options['port'];
62
		else
63
			$db_port = 0;
64
65
		// Select the database. Maybe.
66
		if (empty($db_options['dont_select_db']))
67
			$connection = @mysqli_connect((!empty($db_options['persist']) ? 'p:' : '') . $db_server, $db_user, $db_passwd, $db_name, $db_port);
68
		else
69
			$connection = @mysqli_connect((!empty($db_options['persist']) ? 'p:' : '') . $db_server, $db_user, $db_passwd, '', $db_port);
70
71
		// Something's wrong, show an error if its fatal (which we assume it is)
72 View Code Duplication
		if (!$connection)
73
		{
74
			if (!empty($db_options['non_fatal']))
75
				return null;
76
			else
77
				Errors::instance()->display_db_error();
78
		}
79
80
		// This makes it possible to automatically change the sql_mode and autocommit if needed.
81
		if (isset($mysql_set_mode) && $mysql_set_mode === true)
82
			self::$_db->query('', 'SET sql_mode = \'\', AUTOCOMMIT = 1',
83
			array(),
84
			false
85
		);
86
87
		self::$_db->_connection = $connection;
88
89
		// Few databases still have not set UTF-8 as their default input charset
90
		self::$_db->query('', '
91
			SET NAMES UTF8',
92
			array(
93
			)
94
		);
95
		// Sorry to change your config, but this may be a pain for the time being...
96
		self::$_db->query('', '
97
			SET sql_mode = {string:empty}',
98
			array(
99
				'empty' => ''
100
			)
101
		);
102
103
		return $connection;
104
	}
105
106
	/**
107
	 * Fix up the prefix so it doesn't require the database to be selected.
108
	 *
109
	 * @param string $db_prefix
110
	 * @param string $db_name
111
	 *
112
	 * @return string
113
	 */
114
	public function fix_prefix($db_prefix, $db_name)
115
	{
116
		$db_prefix = is_numeric(substr($db_prefix, 0, 1)) ? $db_name . '.' . $db_prefix : '`' . $db_name . '`.' . $db_prefix;
117
118
		return $db_prefix;
119
	}
120
121
	/**
122
	 * Do a query.  Takes care of errors too.
123
	 *
124
	 * @param string $identifier
125
	 * @param string $db_string
126
	 * @param mixed[]|false $db_values = array()
127
	 * @param mysqli_result|false|null $connection = null
128
	 * @throws Elk_Exception
129
	 */
130
	public function query($identifier, $db_string, $db_values = array(), $connection = null)
131
	{
132
		global $db_show_debug, $time_start, $modSettings;
133
134
		// Comments that are allowed in a query are preg_removed.
135
		static $allowed_comments_from = array(
136
			'~\s+~s',
137
			'~/\*!40001 SQL_NO_CACHE \*/~',
138
			'~/\*!40000 USE INDEX \([A-Za-z\_]+?\) \*/~',
139
			'~/\*!40100 ON DUPLICATE KEY UPDATE id_msg = \d+ \*/~',
140
		);
141
		static $allowed_comments_to = array(
142
			' ',
143
			'',
144
			'',
145
			'',
146
		);
147
148
		// Decide which connection to use.
149
		$connection = $connection === null ? $this->_connection : $connection;
150
151
		// One more query....
152
		$this->_query_count++;
153
154 View Code Duplication
		if (empty($modSettings['disableQueryCheck']) && strpos($db_string, '\'') !== false && empty($db_values['security_override']))
155
			$this->error_backtrace('Hacking attempt...', 'Illegal character (\') used in query...', true, __FILE__, __LINE__);
156
157
		// Use "ORDER BY null" to prevent Mysql doing filesorts for Group By clauses without an Order By
158
		if (strpos($db_string, 'GROUP BY') !== false && strpos($db_string, 'ORDER BY') === false && strpos($db_string, 'INSERT INTO') === false)
159
		{
160
			// Add before LIMIT
161
			if ($pos = strpos($db_string, 'LIMIT '))
162
				$db_string = substr($db_string, 0, $pos) . "\t\t\tORDER BY null\n" . substr($db_string, $pos, strlen($db_string));
163
			else
164
				// Append it.
165
				$db_string .= "\n\t\t\tORDER BY null";
166
		}
167
168 View Code Duplication
		if (empty($db_values['security_override']) && (!empty($db_values) || strpos($db_string, '{db_prefix}') !== false))
169
		{
170
			// Store these values for use in the callback function.
171
			$this->_db_callback_values = $db_values;
0 ignored issues
show
Documentation Bug introduced by
It seems like $db_values can also be of type false. However, the property $_db_callback_values is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
172
			$this->_db_callback_connection = $connection;
0 ignored issues
show
Documentation Bug introduced by
It seems like $connection can also be of type false. However, the property $_db_callback_connection is declared as type resource|object. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
173
174
			// Inject the values passed to this function.
175
			$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', array($this, 'replacement__callback'), $db_string);
176
177
			// No need for them any longer.
178
			$this->_db_callback_values = array();
179
			$this->_db_callback_connection = null;
180
		}
181
182
		// Debugging.
183 View Code Duplication
		if ($db_show_debug === true)
184
		{
185
			$debug = Debug::instance();
186
187
			// Get the file and line number this function was called.
188
			list ($file, $line) = $this->error_backtrace('', '', 'return', __FILE__, __LINE__);
189
190
			if (!empty($_SESSION['debug_redirect']))
191
			{
192
				$debug->merge_db($_SESSION['debug_redirect']);
193
				// @todo this may be off by 1
194
				$this->_query_count += count($_SESSION['debug_redirect']);
195
				$_SESSION['debug_redirect'] = array();
196
			}
197
198
			// Don't overload it.
199
			$st = microtime(true);
200
			$db_cache = array();
201
			$db_cache['q'] = $this->_query_count < 50 ? $db_string : '...';
202
			$db_cache['f'] = $file;
203
			$db_cache['l'] = $line;
204
			$db_cache['s'] = $st - $time_start;
205
		}
206
207
		// First, we clean strings out of the query, reduce whitespace, lowercase, and trim - so we can check it over.
208
		if (empty($modSettings['disableQueryCheck']))
209
		{
210
			$clean = '';
211
			$old_pos = 0;
212
			$pos = -1;
213 View Code Duplication
			while (true)
214
			{
215
				$pos = strpos($db_string, '\'', $pos + 1);
216
				if ($pos === false)
217
					break;
218
				$clean .= substr($db_string, $old_pos, $pos - $old_pos);
219
220
				while (true)
221
				{
222
					$pos1 = strpos($db_string, '\'', $pos + 1);
223
					$pos2 = strpos($db_string, '\\', $pos + 1);
224
					if ($pos1 === false)
225
						break;
226
					elseif ($pos2 === false || $pos2 > $pos1)
227
					{
228
						$pos = $pos1;
229
						break;
230
					}
231
232
					$pos = $pos2 + 1;
233
				}
234
235
				$clean .= ' %s ';
236
				$old_pos = $pos + 1;
237
			}
238
239
			$clean .= substr($db_string, $old_pos);
240
			$clean = trim(strtolower(preg_replace($allowed_comments_from, $allowed_comments_to, $clean)));
241
242
			// Comments?  We don't use comments in our queries, we leave 'em outside!
243 View Code Duplication
			if (strpos($clean, '/*') > 2 || strpos($clean, '--') !== false || strpos($clean, ';') !== false)
244
				$fail = true;
245
			// Trying to change passwords, slow us down, or something?
246
			elseif (strpos($clean, 'sleep') !== false && preg_match('~(^|[^a-z])sleep($|[^[_a-z])~s', $clean) != 0)
247
				$fail = true;
248
			elseif (strpos($clean, 'benchmark') !== false && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)
249
				$fail = true;
250
251
			if (!empty($fail) && function_exists('log_error'))
252
				$this->error_backtrace('Hacking attempt...', 'Hacking attempt...' . "\n" . $db_string, E_USER_ERROR, __FILE__, __LINE__);
253
		}
254
255
		if ($this->_unbuffered === false)
256
			$ret = @mysqli_query($connection, $db_string);
0 ignored issues
show
Security SQL Injection introduced by
$db_string can contain request data and is used in sql context(s) leading to a potential security vulnerability.

13 paths for user data to reach this point

  1. Path: Read from $_POST, and $user_info is assigned in sources/controllers/Post.controller.php on line 924
  1. Read from $_POST, and $user_info is assigned
    in sources/controllers/Post.controller.php on line 924
  2. $msgOptions is assigned
    in sources/controllers/Post.controller.php on line 1032
  3. $msgOptions is passed to modifyPost()
    in sources/controllers/Post.controller.php on line 1039
  4. $messages_columns is assigned
    in sources/subs/Post.subs.php on line 446
  5. $messages_columns is passed through implode()
    in sources/subs/Post.subs.php on line 486
  6. ``' UPDATE {db_prefix}messages SET ' . implode(', ', $messages_columns) . ' WHERE id_msg = {int:id_msg}'`` is passed to Database_MySQL::query()
    in sources/subs/Post.subs.php on line 484
  2. Path: Read from $_REQUEST, and $_REQUEST['params'] is passed to Search::searchParamsFromString() in sources/controllers/Search.controller.php on line 166
  1. Read from $_REQUEST, and $_REQUEST['params'] is passed to Search::searchParamsFromString()
    in sources/controllers/Search.controller.php on line 166
  2. $string is passed through str_replace(), and str_replace(array('-', '_', '.'), array('+', '/', '='), $string) is decoded by base64_decode(), and $temp_params is assigned
    in sources/subs/Search/Search.php on line 647
  3. !empty($temp_params2) ? $temp_params2 : $temp_params is passed through explode(), and $temp_params is assigned
    in sources/subs/Search/Search.php on line 651
  4. $data is assigned
    in sources/subs/Search/Search.php on line 653
  5. $data is passed through explode(), and explode('|\'|', $data) is passed through array_pad(), and $v is assigned
    in sources/subs/Search/Search.php on line 655
  6. Search::$_search_params is assigned
    in sources/subs/Search/Search.php on line 656
  7. Tainted property Search::$_search_params is read, and $this->_search_params['brd'] is passed through implode(), and Search::$_boardQuery is assigned
    in sources/subs/Search/Search.php on line 912
  8. Tainted property Search::$_boardQuery is read, and $subject_query is assigned
    in sources/subs/Search/Search.php on line 1676
  9. ``($this->_db->support_ignore() ? ' INSERT IGNORE INTO {db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics (' . ($this->_createTemporary ? '' : 'id_search, ') . 'id_topic)' : '') . ' SELECT ' . ($this->_createTemporary ? '' : $id_search . ', ') . 't.id_topic FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : ' INNER JOIN ' . implode(' INNER JOIN ', array_unique($subject_query['inner_join']))) . (empty($subject_query['left_join']) ? '' : ' LEFT JOIN ' . implode(' LEFT JOIN ', array_unique($subject_query['left_join']))) . ' WHERE ' . implode(' AND ', array_unique($subject_query['where'])) . (empty($modSettings['search_max_results']) ? '' : ' LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults))`` is passed to DbSearch_MySQL::search_query()
    in sources/subs/Search/Search.php on line 1699
  10. $db_string is passed to Database_MySQL::query()
    in sources/database/DbSearch-mysql.php on line 50
  3. Path: Read from $_REQUEST, and $_REQUEST['params'] is passed to Search::searchParamsFromString() in sources/controllers/Search.controller.php on line 302
  1. Read from $_REQUEST, and $_REQUEST['params'] is passed to Search::searchParamsFromString()
    in sources/controllers/Search.controller.php on line 302
  2. $string is passed through str_replace(), and str_replace(array('-', '_', '.'), array('+', '/', '='), $string) is decoded by base64_decode(), and $temp_params is assigned
    in sources/subs/Search/Search.php on line 647
  3. !empty($temp_params2) ? $temp_params2 : $temp_params is passed through explode(), and $temp_params is assigned
    in sources/subs/Search/Search.php on line 651
  4. $data is assigned
    in sources/subs/Search/Search.php on line 653
  5. $data is passed through explode(), and explode('|\'|', $data) is passed through array_pad(), and $v is assigned
    in sources/subs/Search/Search.php on line 655
  6. Search::$_search_params is assigned
    in sources/subs/Search/Search.php on line 656
  7. Tainted property Search::$_search_params is read, and $this->_search_params['brd'] is passed through implode(), and Search::$_boardQuery is assigned
    in sources/subs/Search/Search.php on line 912
  8. Tainted property Search::$_boardQuery is read, and $subject_query is assigned
    in sources/subs/Search/Search.php on line 1676
  9. ``($this->_db->support_ignore() ? ' INSERT IGNORE INTO {db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics (' . ($this->_createTemporary ? '' : 'id_search, ') . 'id_topic)' : '') . ' SELECT ' . ($this->_createTemporary ? '' : $id_search . ', ') . 't.id_topic FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : ' INNER JOIN ' . implode(' INNER JOIN ', array_unique($subject_query['inner_join']))) . (empty($subject_query['left_join']) ? '' : ' LEFT JOIN ' . implode(' LEFT JOIN ', array_unique($subject_query['left_join']))) . ' WHERE ' . implode(' AND ', array_unique($subject_query['where'])) . (empty($modSettings['search_max_results']) ? '' : ' LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults))`` is passed to DbSearch_MySQL::search_query()
    in sources/subs/Search/Search.php on line 1699
  10. $db_string is passed to Database_MySQL::query()
    in sources/database/DbSearch-mysql.php on line 50
  4. Path: Read from $_REQUEST, and $_REQUEST is passed to Search::mergeSearchParams() in sources/controllers/Search.controller.php on line 304
  1. Read from $_REQUEST, and $_REQUEST is passed to Search::mergeSearchParams()
    in sources/controllers/Search.controller.php on line 304
  2. Search::$_search_params is assigned
    in sources/subs/Search/Search.php on line 742
  3. Tainted property Search::$_search_params is read, and $this->_search_params['brd'] is passed through implode(), and Search::$_boardQuery is assigned
    in sources/subs/Search/Search.php on line 912
  4. Tainted property Search::$_boardQuery is read, and $subject_query is assigned
    in sources/subs/Search/Search.php on line 1676
  5. ``($this->_db->support_ignore() ? ' INSERT IGNORE INTO {db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics (' . ($this->_createTemporary ? '' : 'id_search, ') . 'id_topic)' : '') . ' SELECT ' . ($this->_createTemporary ? '' : $id_search . ', ') . 't.id_topic FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : ' INNER JOIN ' . implode(' INNER JOIN ', array_unique($subject_query['inner_join']))) . (empty($subject_query['left_join']) ? '' : ' LEFT JOIN ' . implode(' LEFT JOIN ', array_unique($subject_query['left_join']))) . ' WHERE ' . implode(' AND ', array_unique($subject_query['where'])) . (empty($modSettings['search_max_results']) ? '' : ' LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults))`` is passed to DbSearch_MySQL::search_query()
    in sources/subs/Search/Search.php on line 1699
  6. $db_string is passed to Database_MySQL::query()
    in sources/database/DbSearch-mysql.php on line 50
  5. Path: Read from $_GET in sources/subs/Search/Search.php on line 966
  1. Read from $_GET
    in sources/subs/Search/Search.php on line 966
  2. Data is passed through htmlspecialchars_decode()
    in vendor/sources/Subs.php on line 569
  3. Data is passed through str_replace()
    in vendor/sources/Subs.php on line 570
  4. Search::$_search_params is assigned
    in sources/subs/Search/Search.php on line 966
  5. Tainted property Search::$_search_params is read, and $subject_query is assigned
    in sources/subs/Search/Search.php on line 1176
  6. $subject_query is assigned
    in sources/subs/Search/Search.php on line 1207
  7. $subject_query is assigned
    in sources/subs/Search/Search.php on line 1215
  8. $subject_query is passed to Search::_build_search_results_log()
    in sources/subs/Search/Search.php on line 1226
  9. ``($this->_db->support_ignore() ? ' INSERT IGNORE INTO {db_prefix}log_search_results (' . implode(', ', array_keys($main_query['select'])) . ')' : '') . ' SELECT ' . implode(', ', $main_query['select']) . ' FROM ' . $main_query['from'] . (!empty($main_query['inner_join']) ? ' INNER JOIN ' . implode(' INNER JOIN ', array_unique($main_query['inner_join'])) : '') . (!empty($main_query['left_join']) ? ' LEFT JOIN ' . implode(' LEFT JOIN ', array_unique($main_query['left_join'])) : '') . (!empty($main_query['where']) ? ' WHERE ' : '') . implode(' AND ', array_unique($main_query['where'])) . (!empty($main_query['group_by']) ? ' GROUP BY ' . implode(', ', array_unique($main_query['group_by'])) : '') . (!empty($main_query['parameters']['limit']) ? ' LIMIT {int:limit}' : '')`` is passed to DbSearch_MySQL::search_query()
    in sources/subs/Search/Search.php on line 1923
  10. $db_string is passed to Database_MySQL::query()
    in sources/database/DbSearch-mysql.php on line 50
  6. Path: Read from $_POST, and Search::$_search_params is assigned in sources/subs/Search/Search.php on line 970
  1. Read from $_POST, and Search::$_search_params is assigned
    in sources/subs/Search/Search.php on line 970
  2. Tainted property Search::$_search_params is read, and $subject_query is assigned
    in sources/subs/Search/Search.php on line 1176
  3. $subject_query is assigned
    in sources/subs/Search/Search.php on line 1207
  4. $subject_query is assigned
    in sources/subs/Search/Search.php on line 1215
  5. $subject_query is passed to Search::_build_search_results_log()
    in sources/subs/Search/Search.php on line 1226
  6. ``($this->_db->support_ignore() ? ' INSERT IGNORE INTO {db_prefix}log_search_results (' . implode(', ', array_keys($main_query['select'])) . ')' : '') . ' SELECT ' . implode(', ', $main_query['select']) . ' FROM ' . $main_query['from'] . (!empty($main_query['inner_join']) ? ' INNER JOIN ' . implode(' INNER JOIN ', array_unique($main_query['inner_join'])) : '') . (!empty($main_query['left_join']) ? ' LEFT JOIN ' . implode(' LEFT JOIN ', array_unique($main_query['left_join'])) : '') . (!empty($main_query['where']) ? ' WHERE ' : '') . implode(' AND ', array_unique($main_query['where'])) . (!empty($main_query['group_by']) ? ' GROUP BY ' . implode(', ', array_unique($main_query['group_by'])) : '') . (!empty($main_query['parameters']['limit']) ? ' LIMIT {int:limit}' : '')`` is passed to DbSearch_MySQL::search_query()
    in sources/subs/Search/Search.php on line 1923
  7. $db_string is passed to Database_MySQL::query()
    in sources/database/DbSearch-mysql.php on line 50
  7. Path: Read from $_POST, and $_POST is passed to Data_Validator::is_valid() in sources/controllers/Post.controller.php on line 883
  1. Read from $_POST, and $_POST is passed to Data_Validator::is_valid()
    in sources/controllers/Post.controller.php on line 883
  2. $data is passed to Data_Validator::validate()
    in sources/subs/DataValidator.class.php on line 147
  3. Data_Validator::$_data is assigned
    in sources/subs/DataValidator.class.php on line 249
  4. Tainted property Data_Validator::$_data is read
    in sources/subs/DataValidator.class.php on line 281
  5. Data_Validator::validation_data() returns tainted data
    in sources/subs/HttpReq.class.php on line 359
  6. HttpReq::cleanValue() returns tainted data, and HttpReq::$_param is assigned
    in sources/subs/HttpReq.class.php on line 219
  7. Tainted property HttpReq::$_param is read
    in sources/subs/HttpReq.class.php on line 278
  8. HttpReq::getQuery() returns tainted data, and $sort is assigned
    in sources/controllers/Memberlist.controller.php on line 407
  9. $field is assigned
    in sources/controllers/Memberlist.controller.php on line 501
  10. $customJoin is assigned
    in sources/controllers/Memberlist.controller.php on line 505
  11. $customJoin is passed through array_unique(), and array_unique($customJoin) is passed to ml_searchMembers()
    in sources/controllers/Memberlist.controller.php on line 518
  12. $customJoin is passed through implode()
    in sources/subs/Memberlist.subs.php on line 235
  13. ``' SELECT COUNT(*) FROM {db_prefix}members AS mem LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = CASE WHEN mem.id_group = {int:regular_id_group} THEN mem.id_post_group ELSE mem.id_group END) ' . (empty($customJoin) ? '' : implode(' ', $customJoin)) . ' WHERE (' . $where . ') AND mem.is_activated = {int:is_activated}'`` is passed to Database_MySQL::query()
    in sources/subs/Memberlist.subs.php on line 230
  8. Path: Fetching key HTTP_CLIENT_IP from $_SERVER, and $_SERVER['HTTP_CLIENT_IP'] is passed through explode(), and explode('.', $_SERVER['HTTP_CLIENT_IP']) is passed through array_reverse(), and array_reverse(explode('.', $_SERVER['HTTP_CLIENT_IP'])) is passed through implode(), and Request::$_ban_ip is assigned in sources/Request.php on line 207
  1. Fetching key HTTP_CLIENT_IP from $_SERVER, and $_SERVER['HTTP_CLIENT_IP'] is passed through explode(), and explode('.', $_SERVER['HTTP_CLIENT_IP']) is passed through array_reverse(), and array_reverse(explode('.', $_SERVER['HTTP_CLIENT_IP'])) is passed through implode(), and Request::$_ban_ip is assigned
    in sources/Request.php on line 207
  2. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  3. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  4. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  5. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  6. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  8. $var is assigned
    in sources/subs/Members.subs.php on line 677
  9. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  10. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  11. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  12. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  13. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751
  9. Path: Fetching key HTTP_CLIENT_IP from $_SERVER, and Request::$_ban_ip is assigned in sources/Request.php on line 209
  1. Fetching key HTTP_CLIENT_IP from $_SERVER, and Request::$_ban_ip is assigned
    in sources/Request.php on line 209
  2. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  3. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  4. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  5. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  6. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  8. $var is assigned
    in sources/subs/Members.subs.php on line 677
  9. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  10. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  11. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  12. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  13. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751
  10. Path: Fetching key HTTP_CLIENT_IP from $_SERVER, and $_SERVER['HTTP_CLIENT_IP'] is passed through explode(), and explode('.', $_SERVER['HTTP_CLIENT_IP']) is passed through array_reverse(), and array_reverse(explode('.', $_SERVER['HTTP_CLIENT_IP'])) is passed through implode(), and Request::$_ban_ip is assigned in sources/Request.php on line 216
  1. Fetching key HTTP_CLIENT_IP from $_SERVER, and $_SERVER['HTTP_CLIENT_IP'] is passed through explode(), and explode('.', $_SERVER['HTTP_CLIENT_IP']) is passed through array_reverse(), and array_reverse(explode('.', $_SERVER['HTTP_CLIENT_IP'])) is passed through implode(), and Request::$_ban_ip is assigned
    in sources/Request.php on line 216
  2. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  3. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  4. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  5. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  6. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  8. $var is assigned
    in sources/subs/Members.subs.php on line 677
  9. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  10. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  11. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  12. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  13. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751
  11. Path: Fetching key HTTP_CLIENT_IP from $_SERVER, and Request::$_ban_ip is assigned in sources/Request.php on line 218
  1. Fetching key HTTP_CLIENT_IP from $_SERVER, and Request::$_ban_ip is assigned
    in sources/Request.php on line 218
  2. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  3. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  4. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  5. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  6. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  8. $var is assigned
    in sources/subs/Members.subs.php on line 677
  9. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  10. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  11. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  12. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  13. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751
  12. Path: Fetching key HTTP_X_FORWARDED_FOR from $_SERVER, and $_SERVER['HTTP_X_FORWARDED_FOR'] is passed through explode(), and explode(', ', $_SERVER['HTTP_X_FORWARDED_FOR']) is passed through array_reverse(), and $ips is assigned in sources/Request.php on line 225
  1. Fetching key HTTP_X_FORWARDED_FOR from $_SERVER, and $_SERVER['HTTP_X_FORWARDED_FOR'] is passed through explode(), and explode(', ', $_SERVER['HTTP_X_FORWARDED_FOR']) is passed through array_reverse(), and $ips is assigned
    in sources/Request.php on line 225
  2. $ip is assigned
    in sources/Request.php on line 228
  3. $ip is passed through trim(), and Request::$_ban_ip is assigned
    in sources/Request.php on line 235
  4. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  5. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  6. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  8. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  9. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  10. $var is assigned
    in sources/subs/Members.subs.php on line 677
  11. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  12. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  13. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  14. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  15. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751
  13. Path: Fetching key HTTP_X_FORWARDED_FOR from $_SERVER, and Request::$_ban_ip is assigned in sources/Request.php on line 241
  1. Fetching key HTTP_X_FORWARDED_FOR from $_SERVER, and Request::$_ban_ip is assigned
    in sources/Request.php on line 241
  2. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  3. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  4. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  5. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  6. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  8. $var is assigned
    in sources/subs/Members.subs.php on line 677
  9. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  10. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  11. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  12. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  13. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751

Preventing SQL Injection

There are two options to prevent SQL injection. Generally, it is recommended to use parameter binding:

$stmt = mysqli_prepare("SELECT * FROM users WHERE name = ?");
$stmt->bind_param("s", $taintedUserName);

An alternative – although generally not recommended – is to escape your data manually:

$mysqli = new mysqli('localhost', 'user', 'pass', 'dbname');

$escaped = $mysqli->real_escape_string($taintedUserName);
$mysqli->query("SELECT * FROM users WHERE name = '".$escaped."'");

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
257
		else
258
			$ret = @mysqli_query($connection, $db_string, MYSQLI_USE_RESULT);
0 ignored issues
show
Security SQL Injection introduced by
$db_string can contain request data and is used in sql context(s) leading to a potential security vulnerability.

13 paths for user data to reach this point

  1. Path: Read from $_POST, and $user_info is assigned in sources/controllers/Post.controller.php on line 924
  1. Read from $_POST, and $user_info is assigned
    in sources/controllers/Post.controller.php on line 924
  2. $msgOptions is assigned
    in sources/controllers/Post.controller.php on line 1032
  3. $msgOptions is passed to modifyPost()
    in sources/controllers/Post.controller.php on line 1039
  4. $messages_columns is assigned
    in sources/subs/Post.subs.php on line 446
  5. $messages_columns is passed through implode()
    in sources/subs/Post.subs.php on line 486
  6. ``' UPDATE {db_prefix}messages SET ' . implode(', ', $messages_columns) . ' WHERE id_msg = {int:id_msg}'`` is passed to Database_MySQL::query()
    in sources/subs/Post.subs.php on line 484
  2. Path: Read from $_REQUEST, and $_REQUEST['params'] is passed to Search::searchParamsFromString() in sources/controllers/Search.controller.php on line 166
  1. Read from $_REQUEST, and $_REQUEST['params'] is passed to Search::searchParamsFromString()
    in sources/controllers/Search.controller.php on line 166
  2. $string is passed through str_replace(), and str_replace(array('-', '_', '.'), array('+', '/', '='), $string) is decoded by base64_decode(), and $temp_params is assigned
    in sources/subs/Search/Search.php on line 647
  3. !empty($temp_params2) ? $temp_params2 : $temp_params is passed through explode(), and $temp_params is assigned
    in sources/subs/Search/Search.php on line 651
  4. $data is assigned
    in sources/subs/Search/Search.php on line 653
  5. $data is passed through explode(), and explode('|\'|', $data) is passed through array_pad(), and $v is assigned
    in sources/subs/Search/Search.php on line 655
  6. Search::$_search_params is assigned
    in sources/subs/Search/Search.php on line 656
  7. Tainted property Search::$_search_params is read, and $this->_search_params['brd'] is passed through implode(), and Search::$_boardQuery is assigned
    in sources/subs/Search/Search.php on line 912
  8. Tainted property Search::$_boardQuery is read, and $subject_query is assigned
    in sources/subs/Search/Search.php on line 1676
  9. ``($this->_db->support_ignore() ? ' INSERT IGNORE INTO {db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics (' . ($this->_createTemporary ? '' : 'id_search, ') . 'id_topic)' : '') . ' SELECT ' . ($this->_createTemporary ? '' : $id_search . ', ') . 't.id_topic FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : ' INNER JOIN ' . implode(' INNER JOIN ', array_unique($subject_query['inner_join']))) . (empty($subject_query['left_join']) ? '' : ' LEFT JOIN ' . implode(' LEFT JOIN ', array_unique($subject_query['left_join']))) . ' WHERE ' . implode(' AND ', array_unique($subject_query['where'])) . (empty($modSettings['search_max_results']) ? '' : ' LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults))`` is passed to DbSearch_MySQL::search_query()
    in sources/subs/Search/Search.php on line 1699
  10. $db_string is passed to Database_MySQL::query()
    in sources/database/DbSearch-mysql.php on line 50
  3. Path: Read from $_REQUEST, and $_REQUEST['params'] is passed to Search::searchParamsFromString() in sources/controllers/Search.controller.php on line 302
  1. Read from $_REQUEST, and $_REQUEST['params'] is passed to Search::searchParamsFromString()
    in sources/controllers/Search.controller.php on line 302
  2. $string is passed through str_replace(), and str_replace(array('-', '_', '.'), array('+', '/', '='), $string) is decoded by base64_decode(), and $temp_params is assigned
    in sources/subs/Search/Search.php on line 647
  3. !empty($temp_params2) ? $temp_params2 : $temp_params is passed through explode(), and $temp_params is assigned
    in sources/subs/Search/Search.php on line 651
  4. $data is assigned
    in sources/subs/Search/Search.php on line 653
  5. $data is passed through explode(), and explode('|\'|', $data) is passed through array_pad(), and $v is assigned
    in sources/subs/Search/Search.php on line 655
  6. Search::$_search_params is assigned
    in sources/subs/Search/Search.php on line 656
  7. Tainted property Search::$_search_params is read, and $this->_search_params['brd'] is passed through implode(), and Search::$_boardQuery is assigned
    in sources/subs/Search/Search.php on line 912
  8. Tainted property Search::$_boardQuery is read, and $subject_query is assigned
    in sources/subs/Search/Search.php on line 1676
  9. ``($this->_db->support_ignore() ? ' INSERT IGNORE INTO {db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics (' . ($this->_createTemporary ? '' : 'id_search, ') . 'id_topic)' : '') . ' SELECT ' . ($this->_createTemporary ? '' : $id_search . ', ') . 't.id_topic FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : ' INNER JOIN ' . implode(' INNER JOIN ', array_unique($subject_query['inner_join']))) . (empty($subject_query['left_join']) ? '' : ' LEFT JOIN ' . implode(' LEFT JOIN ', array_unique($subject_query['left_join']))) . ' WHERE ' . implode(' AND ', array_unique($subject_query['where'])) . (empty($modSettings['search_max_results']) ? '' : ' LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults))`` is passed to DbSearch_MySQL::search_query()
    in sources/subs/Search/Search.php on line 1699
  10. $db_string is passed to Database_MySQL::query()
    in sources/database/DbSearch-mysql.php on line 50
  4. Path: Read from $_REQUEST, and $_REQUEST is passed to Search::mergeSearchParams() in sources/controllers/Search.controller.php on line 304
  1. Read from $_REQUEST, and $_REQUEST is passed to Search::mergeSearchParams()
    in sources/controllers/Search.controller.php on line 304
  2. Search::$_search_params is assigned
    in sources/subs/Search/Search.php on line 742
  3. Tainted property Search::$_search_params is read, and $this->_search_params['brd'] is passed through implode(), and Search::$_boardQuery is assigned
    in sources/subs/Search/Search.php on line 912
  4. Tainted property Search::$_boardQuery is read, and $subject_query is assigned
    in sources/subs/Search/Search.php on line 1676
  5. ``($this->_db->support_ignore() ? ' INSERT IGNORE INTO {db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics (' . ($this->_createTemporary ? '' : 'id_search, ') . 'id_topic)' : '') . ' SELECT ' . ($this->_createTemporary ? '' : $id_search . ', ') . 't.id_topic FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : ' INNER JOIN ' . implode(' INNER JOIN ', array_unique($subject_query['inner_join']))) . (empty($subject_query['left_join']) ? '' : ' LEFT JOIN ' . implode(' LEFT JOIN ', array_unique($subject_query['left_join']))) . ' WHERE ' . implode(' AND ', array_unique($subject_query['where'])) . (empty($modSettings['search_max_results']) ? '' : ' LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults))`` is passed to DbSearch_MySQL::search_query()
    in sources/subs/Search/Search.php on line 1699
  6. $db_string is passed to Database_MySQL::query()
    in sources/database/DbSearch-mysql.php on line 50
  5. Path: Read from $_GET in sources/subs/Search/Search.php on line 966
  1. Read from $_GET
    in sources/subs/Search/Search.php on line 966
  2. Data is passed through htmlspecialchars_decode()
    in vendor/sources/Subs.php on line 569
  3. Data is passed through str_replace()
    in vendor/sources/Subs.php on line 570
  4. Search::$_search_params is assigned
    in sources/subs/Search/Search.php on line 966
  5. Tainted property Search::$_search_params is read, and $subject_query is assigned
    in sources/subs/Search/Search.php on line 1176
  6. $subject_query is assigned
    in sources/subs/Search/Search.php on line 1207
  7. $subject_query is assigned
    in sources/subs/Search/Search.php on line 1215
  8. $subject_query is passed to Search::_build_search_results_log()
    in sources/subs/Search/Search.php on line 1226
  9. ``($this->_db->support_ignore() ? ' INSERT IGNORE INTO {db_prefix}log_search_results (' . implode(', ', array_keys($main_query['select'])) . ')' : '') . ' SELECT ' . implode(', ', $main_query['select']) . ' FROM ' . $main_query['from'] . (!empty($main_query['inner_join']) ? ' INNER JOIN ' . implode(' INNER JOIN ', array_unique($main_query['inner_join'])) : '') . (!empty($main_query['left_join']) ? ' LEFT JOIN ' . implode(' LEFT JOIN ', array_unique($main_query['left_join'])) : '') . (!empty($main_query['where']) ? ' WHERE ' : '') . implode(' AND ', array_unique($main_query['where'])) . (!empty($main_query['group_by']) ? ' GROUP BY ' . implode(', ', array_unique($main_query['group_by'])) : '') . (!empty($main_query['parameters']['limit']) ? ' LIMIT {int:limit}' : '')`` is passed to DbSearch_MySQL::search_query()
    in sources/subs/Search/Search.php on line 1923
  10. $db_string is passed to Database_MySQL::query()
    in sources/database/DbSearch-mysql.php on line 50
  6. Path: Read from $_POST, and Search::$_search_params is assigned in sources/subs/Search/Search.php on line 970
  1. Read from $_POST, and Search::$_search_params is assigned
    in sources/subs/Search/Search.php on line 970
  2. Tainted property Search::$_search_params is read, and $subject_query is assigned
    in sources/subs/Search/Search.php on line 1176
  3. $subject_query is assigned
    in sources/subs/Search/Search.php on line 1207
  4. $subject_query is assigned
    in sources/subs/Search/Search.php on line 1215
  5. $subject_query is passed to Search::_build_search_results_log()
    in sources/subs/Search/Search.php on line 1226
  6. ``($this->_db->support_ignore() ? ' INSERT IGNORE INTO {db_prefix}log_search_results (' . implode(', ', array_keys($main_query['select'])) . ')' : '') . ' SELECT ' . implode(', ', $main_query['select']) . ' FROM ' . $main_query['from'] . (!empty($main_query['inner_join']) ? ' INNER JOIN ' . implode(' INNER JOIN ', array_unique($main_query['inner_join'])) : '') . (!empty($main_query['left_join']) ? ' LEFT JOIN ' . implode(' LEFT JOIN ', array_unique($main_query['left_join'])) : '') . (!empty($main_query['where']) ? ' WHERE ' : '') . implode(' AND ', array_unique($main_query['where'])) . (!empty($main_query['group_by']) ? ' GROUP BY ' . implode(', ', array_unique($main_query['group_by'])) : '') . (!empty($main_query['parameters']['limit']) ? ' LIMIT {int:limit}' : '')`` is passed to DbSearch_MySQL::search_query()
    in sources/subs/Search/Search.php on line 1923
  7. $db_string is passed to Database_MySQL::query()
    in sources/database/DbSearch-mysql.php on line 50
  7. Path: Read from $_POST, and $_POST is passed to Data_Validator::is_valid() in sources/controllers/Post.controller.php on line 883
  1. Read from $_POST, and $_POST is passed to Data_Validator::is_valid()
    in sources/controllers/Post.controller.php on line 883
  2. $data is passed to Data_Validator::validate()
    in sources/subs/DataValidator.class.php on line 147
  3. Data_Validator::$_data is assigned
    in sources/subs/DataValidator.class.php on line 249
  4. Tainted property Data_Validator::$_data is read
    in sources/subs/DataValidator.class.php on line 281
  5. Data_Validator::validation_data() returns tainted data
    in sources/subs/HttpReq.class.php on line 359
  6. HttpReq::cleanValue() returns tainted data, and HttpReq::$_param is assigned
    in sources/subs/HttpReq.class.php on line 219
  7. Tainted property HttpReq::$_param is read
    in sources/subs/HttpReq.class.php on line 278
  8. HttpReq::getQuery() returns tainted data, and $sort is assigned
    in sources/controllers/Memberlist.controller.php on line 407
  9. $field is assigned
    in sources/controllers/Memberlist.controller.php on line 501
  10. $customJoin is assigned
    in sources/controllers/Memberlist.controller.php on line 505
  11. $customJoin is passed through array_unique(), and array_unique($customJoin) is passed to ml_searchMembers()
    in sources/controllers/Memberlist.controller.php on line 518
  12. $customJoin is passed through implode()
    in sources/subs/Memberlist.subs.php on line 235
  13. ``' SELECT COUNT(*) FROM {db_prefix}members AS mem LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = CASE WHEN mem.id_group = {int:regular_id_group} THEN mem.id_post_group ELSE mem.id_group END) ' . (empty($customJoin) ? '' : implode(' ', $customJoin)) . ' WHERE (' . $where . ') AND mem.is_activated = {int:is_activated}'`` is passed to Database_MySQL::query()
    in sources/subs/Memberlist.subs.php on line 230
  8. Path: Fetching key HTTP_CLIENT_IP from $_SERVER, and $_SERVER['HTTP_CLIENT_IP'] is passed through explode(), and explode('.', $_SERVER['HTTP_CLIENT_IP']) is passed through array_reverse(), and array_reverse(explode('.', $_SERVER['HTTP_CLIENT_IP'])) is passed through implode(), and Request::$_ban_ip is assigned in sources/Request.php on line 207
  1. Fetching key HTTP_CLIENT_IP from $_SERVER, and $_SERVER['HTTP_CLIENT_IP'] is passed through explode(), and explode('.', $_SERVER['HTTP_CLIENT_IP']) is passed through array_reverse(), and array_reverse(explode('.', $_SERVER['HTTP_CLIENT_IP'])) is passed through implode(), and Request::$_ban_ip is assigned
    in sources/Request.php on line 207
  2. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  3. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  4. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  5. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  6. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  8. $var is assigned
    in sources/subs/Members.subs.php on line 677
  9. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  10. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  11. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  12. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  13. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751
  9. Path: Fetching key HTTP_CLIENT_IP from $_SERVER, and Request::$_ban_ip is assigned in sources/Request.php on line 209
  1. Fetching key HTTP_CLIENT_IP from $_SERVER, and Request::$_ban_ip is assigned
    in sources/Request.php on line 209
  2. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  3. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  4. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  5. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  6. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  8. $var is assigned
    in sources/subs/Members.subs.php on line 677
  9. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  10. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  11. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  12. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  13. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751
  10. Path: Fetching key HTTP_CLIENT_IP from $_SERVER, and $_SERVER['HTTP_CLIENT_IP'] is passed through explode(), and explode('.', $_SERVER['HTTP_CLIENT_IP']) is passed through array_reverse(), and array_reverse(explode('.', $_SERVER['HTTP_CLIENT_IP'])) is passed through implode(), and Request::$_ban_ip is assigned in sources/Request.php on line 216
  1. Fetching key HTTP_CLIENT_IP from $_SERVER, and $_SERVER['HTTP_CLIENT_IP'] is passed through explode(), and explode('.', $_SERVER['HTTP_CLIENT_IP']) is passed through array_reverse(), and array_reverse(explode('.', $_SERVER['HTTP_CLIENT_IP'])) is passed through implode(), and Request::$_ban_ip is assigned
    in sources/Request.php on line 216
  2. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  3. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  4. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  5. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  6. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  8. $var is assigned
    in sources/subs/Members.subs.php on line 677
  9. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  10. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  11. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  12. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  13. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751
  11. Path: Fetching key HTTP_CLIENT_IP from $_SERVER, and Request::$_ban_ip is assigned in sources/Request.php on line 218
  1. Fetching key HTTP_CLIENT_IP from $_SERVER, and Request::$_ban_ip is assigned
    in sources/Request.php on line 218
  2. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  3. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  4. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  5. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  6. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  8. $var is assigned
    in sources/subs/Members.subs.php on line 677
  9. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  10. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  11. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  12. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  13. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751
  12. Path: Fetching key HTTP_X_FORWARDED_FOR from $_SERVER, and $_SERVER['HTTP_X_FORWARDED_FOR'] is passed through explode(), and explode(', ', $_SERVER['HTTP_X_FORWARDED_FOR']) is passed through array_reverse(), and $ips is assigned in sources/Request.php on line 225
  1. Fetching key HTTP_X_FORWARDED_FOR from $_SERVER, and $_SERVER['HTTP_X_FORWARDED_FOR'] is passed through explode(), and explode(', ', $_SERVER['HTTP_X_FORWARDED_FOR']) is passed through array_reverse(), and $ips is assigned
    in sources/Request.php on line 225
  2. $ip is assigned
    in sources/Request.php on line 228
  3. $ip is passed through trim(), and Request::$_ban_ip is assigned
    in sources/Request.php on line 235
  4. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  5. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  6. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  8. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  9. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  10. $var is assigned
    in sources/subs/Members.subs.php on line 677
  11. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  12. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  13. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  14. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  15. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751
  13. Path: Fetching key HTTP_X_FORWARDED_FOR from $_SERVER, and Request::$_ban_ip is assigned in sources/Request.php on line 241
  1. Fetching key HTTP_X_FORWARDED_FOR from $_SERVER, and Request::$_ban_ip is assigned
    in sources/Request.php on line 241
  2. Tainted property Request::$_ban_ip is read
    in sources/Request.php on line 100
  3. Request::ban_ip() returns tainted data, and $regOptions is assigned
    in sources/controllers/Register.controller.php on line 487
  4. $regOptions is passed to registerMember()
    in sources/controllers/Register.controller.php on line 488
  5. $regOptions is assigned
    in sources/subs/Members.subs.php on line 502
  6. $regOptions is assigned
    in sources/subs/Members.subs.php on line 586
  7. $regOptions is assigned
    in sources/subs/Members.subs.php on line 628
  8. $var is assigned
    in sources/subs/Members.subs.php on line 677
  9. $column_names is assigned
    in sources/subs/Members.subs.php on line 687
  10. $column_names is passed to Database_MySQL::insert()
    in sources/subs/Members.subs.php on line 694
  11. $columns is passed through array_keys(), and $indexed_columns is assigned
    in sources/database/Db-mysql.class.php on line 736
  12. $indexed_columns is passed through implode()
    in sources/database/Db-mysql.class.php on line 752
  13. ``' ' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`) VALUES ' . implode(', ', $insertRows)`` is passed to Database_MySQL::query()
    in sources/database/Db-mysql.class.php on line 751

Preventing SQL Injection

There are two options to prevent SQL injection. Generally, it is recommended to use parameter binding:

$stmt = mysqli_prepare("SELECT * FROM users WHERE name = ?");
$stmt->bind_param("s", $taintedUserName);

An alternative – although generally not recommended – is to escape your data manually:

$mysqli = new mysqli('localhost', 'user', 'pass', 'dbname');

$escaped = $mysqli->real_escape_string($taintedUserName);
$mysqli->query("SELECT * FROM users WHERE name = '".$escaped."'");

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
259
260
		// @deprecated since 1.1 - use skip_next_error method
261
		if (!empty($db_values['db_error_skip']))
262
		{
263
			$this->_skip_error = true;
264
		}
265
266
		if ($ret === false && $this->_skip_error === false)
267
		{
268
			$ret = $this->error($db_string, $connection);
0 ignored issues
show
Bug introduced by
It seems like $connection defined by $connection === null ? $...onnection : $connection on line 149 can also be of type false or object<mysqli_result> or resource; however, Database_MySQL::error() does only seem to accept object<mysqli>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
269
		}
270
271
		// Revert not to skip errors
272
		if ($this->_skip_error === true)
273
		{
274
			$this->_skip_error = false;
275
		}
276
277
		// Debugging.
278 View Code Duplication
		if ($db_show_debug === true)
279
		{
280
			$db_cache['t'] = microtime(true) - $st;
0 ignored issues
show
Bug introduced by
The variable $db_cache 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...
Bug introduced by
The variable $st 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...
281
			$debug->db_query($db_cache);
0 ignored issues
show
Bug introduced by
The variable $debug 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...
282
		}
283
284
		return $ret;
285
	}
286
287
	/**
288
	 * Checks if the string contains any 4byte chars and if so,
289
	 * converts them into HTML entities.
290
	 *
291
	 * This is necessary because MySQL utf8 doesn't know how to store such
292
	 * characters and would generate an error any time one is used.
293
	 * The 4byte chars are used by emoji
294
	 *
295
	 * @param string $string
296
	 * @return string
297
	 */
298
	private function _clean_4byte_chars($string)
299
	{
300
		global $modSettings;
301
302
		if (!empty($modSettings['using_utf8mb4']))
303
			return $string;
304
305
		$result = $string;
306
		$ord = array_map('ord', str_split($string));
307
308
		// If we are in the 4-byte range
309
		if (max($ord) >= 240)
310
		{
311
			// Byte length
312
			$length = strlen($string);
313
			$result = '';
314
315
			// Look for a 4byte marker
316
			for ($i = 0; $i < $length; $i++)
317
			{
318
				// The first byte of a 4-byte character encoding starts with the bytes 0xF0-0xF4 (240 <-> 244)
319
				// but look all the way to 247 for safe measure
320
				$ord1 = $ord[$i];
321
				if ($ord1 >= 240 && $ord1 <= 247)
322
				{
323
					// Replace it with the corresponding html entity
324
					$entity = $this->_uniord(chr($ord[$i]) . chr($ord[$i + 1]) . chr($ord[$i + 2]) . chr($ord[$i + 3]));
325
					if ($entity === false)
326
						$result .= "\xEF\xBF\xBD";
327
					else
328
						$result .= '&#x' . dechex($entity) . ';';
329
					$i += 3;
330
				}
331
				else
332
					$result .= $string[$i];
333
			}
334
		}
335
336
		return $result;
337
	}
338
339
	/**
340
	 * Converts a 4byte char into the corresponding HTML entity code.
341
	 *
342
	 * This function is derived from:
343
	 * http://www.greywyvern.com/code/php/utf8_html.phps
344
	 *
345
	 * @param string $c
346
	 * @return integer|false
347
	 */
348
	private function _uniord($c)
349
	{
350 View Code Duplication
		if (ord($c[0]) >= 0 && ord($c[0]) <= 127)
351
			return ord($c[0]);
352
		if (ord($c[0]) >= 192 && ord($c[0]) <= 223)
353
			return (ord($c[0]) - 192) * 64 + (ord($c[1]) - 128);
354
		if (ord($c[0]) >= 224 && ord($c[0]) <= 239)
355
			return (ord($c[0]) - 224) * 4096 + (ord($c[1]) - 128) * 64 + (ord($c[2]) - 128);
356
		if (ord($c[0]) >= 240 && ord($c[0]) <= 247)
357
			return (ord($c[0]) - 240) * 262144 + (ord($c[1]) - 128) * 4096 + (ord($c[2]) - 128) * 64 + (ord($c[3]) - 128);
358
		if (ord($c[0]) >= 248 && ord($c[0]) <= 251)
359
			return (ord($c[0]) - 248) * 16777216 + (ord($c[1]) - 128) * 262144 + (ord($c[2]) - 128) * 4096 + (ord($c[3]) - 128) * 64 + (ord($c[4]) - 128);
360
		if (ord($c[0]) >= 252 && ord($c[0]) <= 253)
361
			return (ord($c[0]) - 252) * 1073741824 + (ord($c[1]) - 128) * 16777216 + (ord($c[2]) - 128) * 262144 + (ord($c[3]) - 128) * 4096 + (ord($c[4]) - 128) * 64 + (ord($c[5]) - 128);
362 View Code Duplication
		if (ord($c[0]) >= 254 && ord($c[0]) <= 255)
363
			return false;
364
	}
365
366
	/**
367
	 * Affected rows from previous operation.
368
	 *
369
	 * @param mysqli|null $connection
370
	 */
371
	public function affected_rows($connection = null)
372
	{
373
		return mysqli_affected_rows($connection === null ? $this->_connection : $connection);
374
	}
375
376
	/**
377
	 * Last inserted id.
378
	 *
379
	 * @param string $table
380
	 * @param string|null $field = null
381
	 * @param mysqli|null $connection = null
382
	 */
383
	public function insert_id($table, $field = null, $connection = null)
384
	{
385
		// MySQL doesn't need the table or field information.
386
		return mysqli_insert_id($connection === null ? $this->_connection : $connection);
387
	}
388
389
	/**
390
	 * Fetch a row from the result set given as parameter.
391
	 * MySQL implementation doesn't use $counter parameter.
392
	 *
393
	 * @param mysqli_result $result
394
	 * @param integer|bool $counter = false
395
	 */
396
	public function fetch_row($result, $counter = false)
397
	{
398
		// Just delegate to MySQL's function
399
		return mysqli_fetch_row($result);
400
	}
401
402
	/**
403
	 * Free the resultset.
404
	 *
405
	 * @param mysqli_result $result
406
	 */
407
	public function free_result($result)
408
	{
409
		// Just delegate to MySQL's function
410
		mysqli_free_result($result);
411
	}
412
413
	/**
414
	 * Get the number of rows in the result.
415
	 *
416
	 * @param mysqli_result $result
417
	 */
418
	public function num_rows($result)
419
	{
420
		// Simply delegate to the native function
421
		return mysqli_num_rows($result);
422
	}
423
424
	/**
425
	 * Get the number of fields in the result set.
426
	 *
427
	 * @param mysqli_result $request
428
	 */
429
	public function num_fields($request)
430
	{
431
		return mysqli_num_fields($request);
432
	}
433
434
	/**
435
	 * Reset the internal result pointer.
436
	 *
437
	 * @param mysqli_result $request
438
	 * @param integer $counter
439
	 */
440
	public function data_seek($request, $counter)
441
	{
442
		// Delegate to native mysql function
443
		return mysqli_data_seek($request, $counter);
444
	}
445
446
	/**
447
	 * Do a transaction.
448
	 *
449
	 * @param string $type - the step to perform (i.e. 'begin', 'commit', 'rollback')
450
	 * @param mysqli|null $connection = null
451
	 */
452
	public function db_transaction($type = 'commit', $connection = null)
453
	{
454
		// Decide which connection to use
455
		$connection = $connection === null ? $this->_connection : $connection;
456
457
		if ($type == 'begin')
458
			return @mysqli_query($connection, 'BEGIN');
459
		elseif ($type == 'rollback')
460
			return @mysqli_query($connection, 'ROLLBACK');
461
		elseif ($type == 'commit')
462
			return @mysqli_query($connection, 'COMMIT');
463
464
		return false;
465
	}
466
467
	/**
468
	 * Return last error string from the database server
469
	 *
470
	 * @param mysqli|null $connection = null
471
	 */
472
	public function last_error($connection = null)
473
	{
474
		// Decide which connection to use
475
		$connection = $connection === null ? $this->_connection : $connection;
476
477
		if (is_object($connection))
478
			return mysqli_error($connection);
479
	}
480
481
	/**
482
	 * Database error.
483
	 * Backtrace, log, try to fix.
484
	 *
485
	 * @param string      $db_string
486
	 * @param mysqli|null $connection = null
487
	 *
488
	 * @return bool
489
	 * @throws Elk_Exception
490
	 */
491
	public function error($db_string, $connection = null)
492
	{
493
		global $txt, $context, $webmaster_email, $modSettings, $db_persist;
494
		global $db_server, $db_user, $db_passwd, $db_name, $db_show_debug, $ssi_db_user, $ssi_db_passwd;
495
496
		// Get the file and line numbers.
497
		list ($file, $line) = $this->error_backtrace('', '', 'return', __FILE__, __LINE__);
498
499
		// Decide which connection to use
500
		$connection = $connection === null ? $this->_connection : $connection;
501
502
		// This is the error message...
503
		$query_error = mysqli_error($connection);
504
		$query_errno = mysqli_errno($connection);
505
506
		// Error numbers:
507
		//    1016: Can't open file '....MYI'
508
		//    1030: Got error ??? from table handler.
509
		//    1034: Incorrect key file for table.
510
		//    1035: Old key file for table.
511
		//    1142: Command denied
512
		//    1205: Lock wait timeout exceeded.
513
		//    1213: Deadlock found.
514
		//    2006: Server has gone away.
515
		//    2013: Lost connection to server during query.
516
517
		// We cannot do something, try to find out what and act accordingly
518
		if ($query_errno == 1142)
519
		{
520
			$command = substr(trim($db_string), 0, 6);
521
			if ($command === 'DELETE' || $command === 'UPDATE' || $command === 'INSERT')
522
			{
523
				// We can try to ignore it (warning the admin though it's a thing to do)
524
				// and serve the page just SELECTing
525
				$_SESSION['query_command_denied'][$command] = $query_error;
526
527
				// Let the admin know there is a command denied issue
528 View Code Duplication
				if (class_exists('Errors'))
529
				{
530
					Errors::instance()->log_error($txt['database_error'] . ': ' . $query_error . (!empty($modSettings['enableErrorQueryLogging']) ? "\n\n$db_string" : ''), 'database', $file, $line);
531
				}
532
533
				return false;
534
			}
535
		}
536
537
		// Log the error.
538
		if ($query_errno != 1213 && $query_errno != 1205 && class_exists('Errors'))
539
		{
540
			Errors::instance()->log_error($txt['database_error'] . ': ' . $query_error . (!empty($modSettings['enableErrorQueryLogging']) ? "\n\n$db_string" : ''), 'database', $file, $line);
541
		}
542
543
		// Database error auto fixing ;).
544
		if (function_exists('Cache::instance()->get') && (!isset($modSettings['autoFixDatabase']) || $modSettings['autoFixDatabase'] == '1'))
545
		{
546
			$db_last_error = db_last_error();
547
548
			// Force caching on, just for the error checking.
549
			$old_cache = isset($modSettings['cache_enable']) ? $modSettings['cache_enable'] : null;
550
			$modSettings['cache_enable'] = '1';
551
552
			if (Cache::instance()->getVar($temp, 'db_last_error', 600))
553
				$db_last_error = max($db_last_error, $temp);
0 ignored issues
show
Bug introduced by
The variable $temp does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
554
555
			if ($db_last_error < time() - 3600 * 24 * 3)
556
			{
557
				// We know there's a problem... but what?  Try to auto detect.
558
				if ($query_errno == 1030 && strpos($query_error, ' 127 ') !== false)
559
				{
560
					preg_match_all('~(?:[\n\r]|^)[^\']+?(?:FROM|JOIN|UPDATE|TABLE) ((?:[^\n\r(]+?(?:, )?)*)~s', $db_string, $matches);
561
562
					$fix_tables = array();
563
					foreach ($matches[1] as $tables)
564
					{
565
						$tables = array_unique(explode(',', $tables));
566
						foreach ($tables as $table)
567
						{
568
							// Now, it's still theoretically possible this could be an injection.  So backtick it!
569
							if (trim($table) != '')
570
								$fix_tables[] = '`' . strtr(trim($table), array('`' => '')) . '`';
571
						}
572
					}
573
574
					$fix_tables = array_unique($fix_tables);
575
				}
576
				// Table crashed.  Let's try to fix it.
577 View Code Duplication
				elseif ($query_errno == 1016)
578
				{
579
					if (preg_match('~\'([^\.\']+)~', $query_error, $match) != 0)
580
						$fix_tables = array('`' . $match[1] . '`');
581
				}
582
				// Indexes crashed.  Should be easy to fix!
583 View Code Duplication
				elseif ($query_errno == 1034 || $query_errno == 1035)
584
				{
585
					preg_match('~\'([^\']+?)\'~', $query_error, $match);
586
					$fix_tables = array('`' . $match[1] . '`');
587
				}
588
			}
589
590
			// Check for errors like 145... only fix it once every three days, and send an email. (can't use empty because it might not be set yet...)
591
			if (!empty($fix_tables))
592
			{
593
				// subs/Admin.subs.php for updateDbLastError(), subs/Mail.subs.php for sendmail().
594
				// @todo this should go somewhere else, not into the db-mysql layer I think
595
				require_once(SUBSDIR . '/Admin.subs.php');
596
				require_once(SUBSDIR . '/Mail.subs.php');
597
598
				// Make a note of the REPAIR...
599
				Cache::instance()->put('db_last_error', time(), 600);
600
				if (!Cache::instance()->getVar($temp, 'db_last_error', 600))
601
					updateDbLastError(time());
602
603
				// Attempt to find and repair the broken table.
604
				foreach ($fix_tables as $table)
605
				{
606
					$this->query('', "
607
						REPAIR TABLE $table", false, false);
608
				}
609
610
				// And send off an email!
611
				sendmail($webmaster_email, $txt['database_error'], $txt['tried_to_repair']);
612
613
				$modSettings['cache_enable'] = $old_cache;
614
615
				// Try the query again...?
616
				$ret = $this->query('', $db_string, false, false);
617
				if ($ret !== false)
618
					return $ret;
619
			}
620
			else
621
				$modSettings['cache_enable'] = $old_cache;
622
623
			// Check for the "lost connection" or "deadlock found" errors - and try it just one more time.
624
			if (in_array($query_errno, array(1205, 1213, 2006, 2013)))
625
			{
626
				$new_connection = false;
627
				if (in_array($query_errno, array(2006, 2013)) && $this->_connection == $connection)
628
				{
629
					// Are we in SSI mode?  If so try that username and password first
630
					if (ELK == 'SSI' && !empty($ssi_db_user) && !empty($ssi_db_passwd))
631
						$new_connection = @mysqli_connect((!empty($db_persist) ? 'p:' : '') . $db_server, $ssi_db_user, $ssi_db_passwd, $db_name);
632
633
					// Fall back to the regular username and password if need be
634
					if (!$new_connection)
635
						$new_connection = @mysqli_connect((!empty($db_persist) ? 'p:' : '') . $db_server, $db_user, $db_passwd, $db_name);
636
				}
637
638
				if ($new_connection)
639
				{
640
					$this->_connection = $new_connection;
641
642
					// Try a deadlock more than once more.
643
					for ($n = 0; $n < 4; $n++)
644
					{
645
						$ret = $this->query('', $db_string, false, false);
646
647
						$new_errno = mysqli_errno($new_connection);
648
						if ($ret !== false || in_array($new_errno, array(1205, 1213)))
649
							break;
650
					}
651
652
					// If it failed again, shucks to be you... we're not trying it over and over.
653
					if ($ret !== false)
654
						return $ret;
0 ignored issues
show
Bug introduced by
The variable $ret 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...
655
				}
656
			}
657
			// Are they out of space, perhaps?
658
			elseif ($query_errno == 1030 && (strpos($query_error, ' -1 ') !== false || strpos($query_error, ' 28 ') !== false || strpos($query_error, ' 12 ') !== false))
659
			{
660
				if (!isset($txt))
661
					$query_error .= ' - check database storage space.';
662
				else
663
				{
664
					if (!isset($txt['mysql_error_space']))
665
						loadLanguage('Errors');
666
667
					$query_error .= !isset($txt['mysql_error_space']) ? ' - check database storage space.' : $txt['mysql_error_space'];
668
				}
669
			}
670
		}
671
672
		// Nothing's defined yet... just die with it.
673
		if (empty($context) || empty($txt))
674
			die($query_error);
0 ignored issues
show
Coding Style Compatibility introduced by
The method error() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
675
676
		// Show an error message, if possible.
677
		$context['error_title'] = $txt['database_error'];
678 View Code Duplication
		if (allowedTo('admin_forum'))
679
			$context['error_message'] = nl2br($query_error) . '<br />' . $txt['file'] . ': ' . $file . '<br />' . $txt['line'] . ': ' . $line;
680
		else
681
			$context['error_message'] = $txt['try_again'];
682
683
		// Add database version that we know of, for the admin to know. (and ask for support)
684 View Code Duplication
		if (allowedTo('admin_forum'))
685
			$context['error_message'] .= '<br /><br />' . sprintf($txt['database_error_versions'], $modSettings['elkVersion']);
686
687 View Code Duplication
		if (allowedTo('admin_forum') && $db_show_debug === true)
688
			$context['error_message'] .= '<br /><br />' . nl2br($db_string);
689
690
		// It's already been logged... don't log it again.
691
		throw new Elk_Exception($context['error_message'], false);
692
	}
693
694
	/**
695
	 * Insert data.
696
	 *
697
	 * @param string $method - options 'replace', 'ignore', 'insert'
698
	 * @param string $table
699
	 * @param mixed[] $columns
700
	 * @param mixed[] $data
701
	 * @param mixed[] $keys
702
	 * @param bool $disable_trans = false
703
	 * @param mysqli|null $connection = null
704
	 * @throws Elk_Exception
705
	 */
706
	public function insert($method = 'replace', $table, $columns, $data, $keys, $disable_trans = false, $connection = null)
707
	{
708
		global $db_prefix;
709
710
		$connection = $connection === null ? $this->_connection : $connection;
711
712
		// With nothing to insert, simply return.
713
		if (empty($data))
714
			return;
715
716
		// Inserting data as a single row can be done as a single array.
717
		if (!is_array($data[array_rand($data)]))
718
			$data = array($data);
719
720
		// Replace the prefix holder with the actual prefix.
721
		$table = str_replace('{db_prefix}', $db_prefix, $table);
722
723
		// Create the mold for a single row insert.
724
		$insertData = '(';
725 View Code Duplication
		foreach ($columns as $columnName => $type)
726
		{
727
			// Are we restricting the length?
728
			if (strpos($type, 'string-') !== false)
729
				$insertData .= sprintf('SUBSTRING({string:%1$s}, 1, ' . substr($type, 7) . '), ', $columnName);
730
			else
731
				$insertData .= sprintf('{%1$s:%2$s}, ', $type, $columnName);
732
		}
733
		$insertData = substr($insertData, 0, -2) . ')';
734
735
		// Create an array consisting of only the columns.
736
		$indexed_columns = array_keys($columns);
737
738
		// Here's where the variables are injected to the query.
739
		$insertRows = array();
740
		foreach ($data as $dataRow)
741
		{
742
			$insertRows[] = $this->quote($insertData, $this->_array_combine($indexed_columns, $dataRow), $connection);
0 ignored issues
show
Bug introduced by
It seems like $connection defined by $connection === null ? $...onnection : $connection on line 710 can also be of type resource; however, Database_Abstract::quote() does only seem to accept object<mysqli>|object<postgre>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
743
		}
744
745
		// Determine the method of insertion.
746
		$queryTitle = $method === 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
747
748
		$skip_error = $table === $db_prefix . 'log_errors';
749
		$this->_skip_error = $skip_error;
750
		// Do the insert.
751
		$this->query('', '
752
			' . $queryTitle . ' INTO ' . $table . '(`' . implode('`, `', $indexed_columns) . '`)
753
			VALUES
754
				' . implode(',
755
				', $insertRows),
756
			array(
757
				'security_override' => true,
758
			),
759
			$connection
0 ignored issues
show
Bug introduced by
It seems like $connection defined by $connection === null ? $...onnection : $connection on line 710 can also be of type object<mysqli> or resource; however, Database_MySQL::query() does only seem to accept object<mysqli_result>|false|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
760
		);
761
	}
762
763
	/**
764
	 * Unescape an escaped string!
765
	 *
766
	 * @param string $string
767
	 */
768
	public function unescape_string($string)
769
	{
770
		return stripslashes($string);
771
	}
772
773
	/**
774
	 * Returns whether the database system supports ignore.
775
	 *
776
	 * @return boolean
777
	 */
778
	public function support_ignore()
779
	{
780
		return true;
781
	}
782
783
	/**
784
	 * Gets all the necessary INSERTs for the table named table_name.
785
	 * It goes in 250 row segments.
786
	 *
787
	 * @param string $tableName - the table to create the inserts for.
788
	 * @param bool $new_table
789
	 *
790
	 * @return string the query to insert the data back in, or an empty string if the table was empty.
791
	 * @throws Elk_Exception
792
	 */
793
	public function insert_sql($tableName, $new_table = false)
794
	{
795
		global $db_prefix;
796
797
		static $start = 0, $num_rows, $fields, $limit;
798
799 View Code Duplication
		if ($new_table)
800
		{
801
			$limit = strstr($tableName, 'log_') !== false ? 500 : 250;
802
			$start = 0;
803
		}
804
805
		$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
806
807
		// This will be handy...
808
		$crlf = "\r\n";
809
810
		$result = $this->query('', '
811
			SELECT /*!40001 SQL_NO_CACHE */ *
812
			FROM `' . $tableName . '`
813
			LIMIT ' . $start . ', ' . $limit,
814
			array(
815
				'security_override' => true,
816
			)
817
		);
818
819
		// The number of rows, just for record keeping and breaking INSERTs up.
820
		$num_rows = $this->num_rows($result);
821
822
		if ($num_rows == 0)
823
			return '';
824
825
		if ($new_table)
826
		{
827
			$fields = array_keys($this->fetch_assoc($result));
828
			$this->data_seek($result, 0);
829
		}
830
831
		// Start it off with the basic INSERT INTO.
832
		$data = 'INSERT INTO `' . $tableName . '`' . $crlf . "\t" . '(`' . implode('`, `', $fields) . '`)' . $crlf . 'VALUES ';
833
834
		// Loop through each row.
835 View Code Duplication
		while ($row = $this->fetch_assoc($result))
836
		{
837
			// Get the fields in this row...
838
			$field_list = array();
839
840
			foreach ($row as $key => $item)
841
			{
842
				// Try to figure out the type of each field. (NULL, number, or 'string'.)
843
				if (!isset($item))
844
					$field_list[] = 'NULL';
845
				elseif (is_numeric($item) && (int) $item == $item)
846
					$field_list[] = $item;
847
				else
848
					$field_list[] = '\'' . $this->escape_string($item) . '\'';
849
			}
850
851
			$data .= '(' . implode(', ', $field_list) . '),' . $crlf . "\t";
852
		}
853
854
		$this->free_result($result);
855
		$data = substr(trim($data), 0, -1) . ';' . $crlf . $crlf;
856
857
		$start += $limit;
858
859
		return $data;
860
	}
861
862
	/**
863
	 * Dumps the schema (CREATE) for a table.
864
	 *
865
	 * @param string $tableName - the table
866
	 *
867
	 * @return string - the CREATE statement as string
868
	 * @throws Elk_Exception
869
	 */
870
	public function db_table_sql($tableName)
871
	{
872
		global $db_prefix;
873
874
		$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
875
876
		// This will be needed...
877
		$crlf = "\r\n";
878
879
		// Drop it if it exists.
880
		$schema_create = 'DROP TABLE IF EXISTS `' . $tableName . '`;' . $crlf . $crlf;
881
882
		// Start the create table...
883
		$schema_create .= 'CREATE TABLE `' . $tableName . '` (' . $crlf;
884
885
		// Find all the fields.
886
		$result = $this->query('', '
887
			SHOW FIELDS
888
			FROM `{raw:table}`',
889
			array(
890
				'table' => $tableName,
891
			)
892
		);
893
		while ($row = $this->fetch_assoc($result))
894
		{
895
			// Make the CREATE for this column.
896
			$schema_create .= ' `' . $row['Field'] . '` ' . $row['Type'] . ($row['Null'] != 'YES' ? ' NOT NULL' : '');
897
898
			// Add a default...?
899
			if (!empty($row['Default']) || $row['Null'] !== 'YES')
900
			{
901
				// Make a special case of auto-timestamp.
902
				if ($row['Default'] == 'CURRENT_TIMESTAMP')
903
					$schema_create .= ' /*!40102 NOT NULL default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP */';
904
				// Text shouldn't have a default.
905
				elseif ($row['Default'] !== null)
906
				{
907
					// If this field is numeric the default needs no escaping.
908
					$type = strtolower($row['Type']);
909
					$isNumericColumn = strpos($type, 'int') !== false || strpos($type, 'bool') !== false || strpos($type, 'bit') !== false || strpos($type, 'float') !== false || strpos($type, 'double') !== false || strpos($type, 'decimal') !== false;
910
911
					$schema_create .= ' default ' . ($isNumericColumn ? $row['Default'] : '\'' . $this->escape_string($row['Default']) . '\'');
912
				}
913
			}
914
915
			// And now any extra information. (such as auto_increment.)
916
			$schema_create .= ($row['Extra'] != '' ? ' ' . $row['Extra'] : '') . ',' . $crlf;
917
		}
918
		$this->free_result($result);
919
920
		// Take off the last comma.
921
		$schema_create = substr($schema_create, 0, -strlen($crlf) - 1);
922
923
		// Find the keys.
924
		$result = $this->query('', '
925
			SHOW KEYS
926
			FROM `{raw:table}`',
927
			array(
928
				'table' => $tableName,
929
			)
930
		);
931
		$indexes = array();
932
		while ($row = $this->fetch_assoc($result))
933
		{
934
			// Is this a primary key, unique index, or regular index?
935
			$row['Key_name'] = $row['Key_name'] == 'PRIMARY' ? 'PRIMARY KEY' : (empty($row['Non_unique']) ? 'UNIQUE ' : ($row['Comment'] == 'FULLTEXT' || (isset($row['Index_type']) && $row['Index_type'] == 'FULLTEXT') ? 'FULLTEXT ' : 'KEY ')) . '`' . $row['Key_name'] . '`';
936
937
			// Is this the first column in the index?
938
			if (empty($indexes[$row['Key_name']]))
939
				$indexes[$row['Key_name']] = array();
940
941
			// A sub part, like only indexing 15 characters of a varchar.
942
			if (!empty($row['Sub_part']))
943
				$indexes[$row['Key_name']][$row['Seq_in_index']] = '`' . $row['Column_name'] . '`(' . $row['Sub_part'] . ')';
944
			else
945
				$indexes[$row['Key_name']][$row['Seq_in_index']] = '`' . $row['Column_name'] . '`';
946
		}
947
		$this->free_result($result);
948
949
		// Build the CREATEs for the keys.
950
		foreach ($indexes as $keyname => $columns)
951
		{
952
			// Ensure the columns are in proper order.
953
			ksort($columns);
954
955
			$schema_create .= ',' . $crlf . ' ' . $keyname . ' (' . implode(', ', $columns) . ')';
956
		}
957
958
		// Now just get the comment and type... (MyISAM, etc.)
959
		$result = $this->query('', '
960
			SHOW TABLE STATUS
961
			LIKE {string:table}',
962
			array(
963
				'table' => strtr($tableName, array('_' => '\\_', '%' => '\\%')),
964
			)
965
		);
966
		$row = $this->fetch_assoc($result);
967
		$this->free_result($result);
968
969
		// Probably MyISAM.... and it might have a comment.
970
		$schema_create .= $crlf . ') ENGINE=' . (isset($row['Type']) ? $row['Type'] : $row['Engine']) . ($row['Comment'] != '' ? ' COMMENT="' . $row['Comment'] . '"' : '');
971
972
		return $schema_create;
973
	}
974
975
	/**
976
	 * {@inheritdoc}
977
	 */
978
	public function db_list_tables($db_name_str = false, $filter = false)
979
	{
980
		global $db_name;
981
982
		$db_name_str = $db_name_str === false ? $db_name : $db_name_str;
983
		$db_name_str = trim($db_name_str);
984
		$filter = $filter === false ? '' : ' LIKE \'' . $filter . '\'';
985
986
		$request = $this->query('', '
987
			SHOW TABLES
988
			FROM `{raw:db_name_str}`
989
			{raw:filter}',
990
			array(
991
				'db_name_str' => $db_name_str[0] == '`' ? strtr($db_name_str, array('`' => '')) : $db_name_str,
992
				'filter' => $filter,
993
			)
994
		);
995
		$tables = array();
996
		while ($row = $this->fetch_row($request))
997
		{
998
			$tables[] = $row[0];
999
		}
1000
		$this->free_result($request);
1001
1002
		return $tables;
1003
	}
1004
1005
	/**
1006
	 * Backup $table_name to $backup_table.
1007
	 *
1008
	 * @param string $table_name
1009
	 * @param string $backup_table
1010
	 *
1011
	 * @return resource - the request handle to the table creation query
1012
	 * @throws Elk_Exception
1013
	 */
1014
	public function db_backup_table($table_name, $backup_table)
1015
	{
1016
		global $db_prefix;
1017
1018
		$table = str_replace('{db_prefix}', $db_prefix, $table_name);
1019
1020
		// First, get rid of the old table.
1021
		$db_table = db_table();
1022
		$db_table->db_drop_table($backup_table);
1023
1024
		// Can we do this the quick way?
1025
		$result = $this->query('', '
1026
			CREATE TABLE {raw:backup_table} LIKE {raw:table}',
1027
			array(
1028
				'backup_table' => $backup_table,
1029
				'table' => $table
1030
		));
1031
		// If this failed, we go old school.
1032
		if ($result)
1033
		{
1034
			$request = $this->query('', '
1035
				INSERT INTO {raw:backup_table}
1036
				SELECT *
1037
				FROM {raw:table}',
1038
				array(
1039
					'backup_table' => $backup_table,
1040
					'table' => $table
1041
				));
1042
1043
			// Old school or no school?
1044
			if ($request)
1045
				return $request;
1046
		}
1047
1048
		// At this point, the quick method failed.
1049
		$result = $this->query('', '
1050
			SHOW CREATE TABLE {raw:table}',
1051
			array(
1052
				'table' => $table,
1053
			)
1054
		);
1055
		list (, $create) = $this->fetch_row($result);
1056
		$this->free_result($result);
1057
1058
		$create = preg_split('/[\n\r]/', $create);
1059
1060
		$auto_inc = '';
1061
1062
		// Default engine type.
1063
		$engine = 'MyISAM';
1064
		$charset = '';
1065
		$collate = '';
1066
1067
		foreach ($create as $k => $l)
1068
		{
1069
			// Get the name of the auto_increment column.
1070
			if (strpos($l, 'auto_increment'))
1071
				$auto_inc = trim($l);
1072
1073
			// For the engine type, see if we can work out what it is.
1074
			if (strpos($l, 'ENGINE') !== false || strpos($l, 'TYPE') !== false)
1075
			{
1076
				// Extract the engine type.
1077
				preg_match('~(ENGINE|TYPE)=(\w+)(\sDEFAULT)?(\sCHARSET=(\w+))?(\sCOLLATE=(\w+))?~', $l, $match);
1078
1079
				if (!empty($match[1]))
1080
					$engine = $match[1];
1081
1082
				if (!empty($match[2]))
1083
					$engine = $match[2];
1084
1085
				if (!empty($match[5]))
1086
					$charset = $match[5];
1087
1088
				if (!empty($match[7]))
1089
					$collate = $match[7];
1090
			}
1091
1092
			// Skip everything but keys...
1093
			if (strpos($l, 'KEY') === false)
1094
				unset($create[$k]);
1095
		}
1096
1097
		if (!empty($create))
1098
			$create = '(
1099
				' . implode('
1100
				', $create) . ')';
1101
		else
1102
			$create = '';
1103
1104
		$request = $this->query('', '
1105
			CREATE TABLE {raw:backup_table} {raw:create}
1106
			ENGINE={raw:engine}' . (empty($charset) ? '' : ' CHARACTER SET {raw:charset}' . (empty($collate) ? '' : ' COLLATE {raw:collate}')) . '
1107
			SELECT *
1108
			FROM {raw:table}',
1109
			array(
1110
				'backup_table' => $backup_table,
1111
				'table' => $table,
1112
				'create' => $create,
1113
				'engine' => $engine,
1114
				'charset' => empty($charset) ? '' : $charset,
1115
				'collate' => empty($collate) ? '' : $collate,
1116
			)
1117
		);
1118
1119
		if ($auto_inc != '')
1120
		{
1121
			if (preg_match('~\`(.+?)\`\s~', $auto_inc, $match) != 0 && substr($auto_inc, -1, 1) == ',')
1122
				$auto_inc = substr($auto_inc, 0, -1);
1123
1124
			$this->query('', '
1125
				ALTER TABLE {raw:backup_table}
1126
				CHANGE COLUMN {raw:column_detail} {raw:auto_inc}',
1127
				array(
1128
					'backup_table' => $backup_table,
1129
					'column_detail' => $match[1],
1130
					'auto_inc' => $auto_inc,
1131
				)
1132
			);
1133
		}
1134
1135
		return $request;
1136
	}
1137
1138
	/**
1139
	 * Get the version number.
1140
	 *
1141
	 * @return string - the version
1142
	 * @throws Elk_Exception
1143
	 */
1144 View Code Duplication
	public function db_server_version()
1145
	{
1146
		$request = $this->query('', '
1147
			SELECT VERSION()',
1148
			array(
1149
			)
1150
		);
1151
		list ($ver) = $this->fetch_row($request);
1152
		$this->free_result($request);
1153
1154
		return $ver;
1155
	}
1156
1157
	/**
1158
	 * Get the name (title) of the database system.
1159
	 *
1160
	 * @return string
1161
	 */
1162
	public function db_title()
1163
	{
1164
		return 'MySQL';
1165
	}
1166
1167
	/**
1168
	 * Whether the database system is case sensitive.
1169
	 *
1170
	 * @return false
1171
	 */
1172
	public function db_case_sensitive()
1173
	{
1174
		return false;
1175
	}
1176
1177
	/**
1178
	 * Escape string for the database input
1179
	 *
1180
	 * @param string $string
1181
	 */
1182
	public function escape_string($string)
1183
	{
1184
		$string = $this->_clean_4byte_chars($string);
1185
1186
		return mysqli_real_escape_string($this->_connection, $string);
1187
	}
1188
1189
	/**
1190
	 * Fetch next result as association.
1191
	 * The mysql implementation simply delegates to mysqli_fetch_assoc().
1192
	 * It ignores $counter parameter.
1193
	 *
1194
	 * @param mysqli_result $request
1195
	 * @param int|bool $counter = false
1196
	 */
1197
	public function fetch_assoc($request, $counter = false)
1198
	{
1199
		return mysqli_fetch_assoc($request);
1200
	}
1201
1202
	/**
1203
	 * Return server info.
1204
	 *
1205
	 * @param mysqli|null $connection
1206
	 *
1207
	 * @return string
1208
	 */
1209
	public function db_server_info($connection = null)
1210
	{
1211
		// Decide which connection to use
1212
		$connection = $connection === null ? $this->_connection : $connection;
1213
1214
		return mysqli_get_server_info($connection);
1215
	}
1216
1217
	/**
1218
	 *  Get the version number.
1219
	 *
1220
	 * @return string - the version
1221
	 * @throws Elk_Exception
1222
	 */
1223 View Code Duplication
	public function db_client_version()
1224
	{
1225
		$request = $this->query('', '
1226
			SELECT VERSION()',
1227
			array(
1228
			)
1229
		);
1230
		list ($ver) = $this->fetch_row($request);
1231
		$this->free_result($request);
1232
1233
		return $ver;
1234
	}
1235
1236
	/**
1237
	 * Select database.
1238
	 *
1239
	 * @param string|null $dbName = null
1240
	 * @param mysqli|null $connection = null
1241
	 */
1242
	public function select_db($dbName = null, $connection = null)
1243
	{
1244
		// Decide which connection to use
1245
		$connection = $connection === null ? $this->_connection : $connection;
1246
1247
		return mysqli_select_db($connection, $dbName);
1248
	}
1249
1250
	/**
1251
	 * Returns a reference to the existing instance
1252
	 */
1253
	public static function db()
1254
	{
1255
		return self::$_db;
1256
	}
1257
1258
	/**
1259
	 * Finds out if the connection is still valid.
1260
	 *
1261
	 * @param mysqli|null $connection = null
1262
	 */
1263
	public function validConnection($connection = null)
1264
	{
1265
		return is_object($connection);
1266
	}
1267
}
1268