Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Failed Conditions
Pull Request — master (#1038)
by Dan
04:32
created

Database::escapeArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 3
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Smr;
4
5
use mysqli;
6
use mysqli_result;
7
use RuntimeException;
8
use Smr\Container\DiContainer;
9
use Smr\DatabaseProperties;
10
11
class Database {
12
	private mysqli $dbConn;
13
	private DatabaseProperties $dbProperties;
14
	private string $selectedDbName;
15
	private bool|mysqli_result $dbResult;
16
	private ?array $dbRecord;
17
18
	public static function getInstance(): self {
19
		return DiContainer::make(self::class);
20
	}
21
22
	/**
23
	 * Reconnects to the MySQL database, and replaces the managed mysqli instance
24
	 * in the dependency injection container for future retrievals.
25
	 * @throws \DI\DependencyException
26
	 * @throws \DI\NotFoundException
27
	 */
28
	private static function reconnectMysql(): mysqli {
29
		$newMysqli = DiContainer::make(mysqli::class);
30
		DiContainer::getContainer()->set(mysqli::class, $newMysqli);
31
		return $newMysqli;
32
	}
33
34
	public static function mysqliFactory(DatabaseProperties $dbProperties): mysqli {
35
		if (!mysqli_report(MYSQLI_REPORT_ERROR|MYSQLI_REPORT_STRICT)) {
36
			throw new RuntimeException('Failed to enable mysqli error reporting');
37
		}
38
		$mysql = new mysqli(
39
			$dbProperties->getHost(),
40
			$dbProperties->getUser(),
41
			$dbProperties->getPassword(),
42
			$dbProperties->getDatabaseName());
43
		$charset = $mysql->character_set_name();
44
		if ($charset != 'utf8') {
45
			throw new RuntimeException('Unexpected charset: ' . $charset);
46
		}
47
		return $mysql;
48
	}
49
50
	/**
51
	 * Database constructor.
52
	 * Not intended to be constructed by hand. If you need an instance of Database,
53
	 * use Database::getInstance();
54
	 * @param ?mysqli $dbConn The mysqli instance (null if reconnect needed)
55
	 * @param DatabaseProperties $dbProperties The properties object that was used to construct the mysqli instance
56
	 */
57
	public function __construct(?mysqli $dbConn, DatabaseProperties $dbProperties) {
58
		if (is_null($dbConn)) {
59
			$dbConn = self::reconnectMysql();
60
		}
61
		$this->dbConn = $dbConn;
62
		$this->dbProperties = $dbProperties;
63
		$this->selectedDbName = $dbProperties->getDatabaseName();
64
	}
65
66
	/**
67
	 * This method will switch the connection to the specified database.
68
	 * Useful for switching back and forth between historical, and live databases.
69
	 *
70
	 * @param string $databaseName The name of the database to switch to
71
	 */
72
	public function switchDatabases(string $databaseName) {
73
		$this->dbConn->select_db($databaseName);
74
		$this->selectedDbName = $databaseName;
75
	}
76
77
	/**
78
	 * Switch back to the configured live database
79
	 */
80
	public function switchDatabaseToLive() {
81
		$this->switchDatabases($this->dbProperties->getDatabaseName());
82
	}
83
84
	/**
85
	 * Returns the size of the selected database in bytes.
86
	 */
87
	public function getDbBytes() {
88
		$query = 'SELECT SUM(data_length + index_length) as db_bytes FROM information_schema.tables WHERE table_schema=' . $this->escapeString($this->selectedDbName);
89
		$result = $this->dbConn->query($query);
90
		return (int)$result->fetch_assoc()['db_bytes'];
91
	}
92
93
	/**
94
	 * This should not be needed except perhaps by persistent connections
95
	 *
96
	 * Closes the connection to the MySQL database. After closing this connection,
97
	 * this instance is no longer valid, and will subsequently throw exceptions when
98
	 * attempting to perform database operations.
99
	 *
100
	 * You must call Database::getInstance() again to retrieve a valid instance that
101
	 * is reconnected to the database.
102
	 *
103
	 * @return bool Whether the underlying connection was closed by this call.
104
	 */
105
	public function close() : bool {
106
		if (!isset($this->dbConn)) {
107
			// Connection is already closed; nothing to do.
108
			return false;
109
		}
110
		$this->dbConn->close();
111
		unset($this->dbConn);
112
		// Set the mysqli instance in the dependency injection container to
113
		// null so that the Database constructor will reconnect the next time
114
		// it is called.
115
		DiContainer::getContainer()->set(mysqli::class, null);
116
		return true;
117
	}
118
119
	public function query($query) {
120
		$this->dbResult = $this->dbConn->query($query);
121
	}
122
123
	/**
124
	 * Use to populate this instance with the next record of the active query.
125
	 */
126
	public function nextRecord(): bool {
127
		if (!$this->dbResult) {
128
			$this->error('No resource to get record from.');
129
		}
130
		if ($this->dbRecord = $this->dbResult->fetch_assoc()) {
131
			return true;
132
		}
133
		return false;
134
	}
135
136
	/**
137
	 * Use instead of nextRecord when exactly one record is expected from the
138
	 * active query.
139
	 */
140
	public function requireRecord(): void {
141
		if (!$this->nextRecord() || $this->getNumRows() != 1) {
142
			$this->error('One record required, but found ' . $this->getNumRows());
143
		}
144
	}
145
146
	public function hasField($name) {
147
		return isset($this->dbRecord[$name]);
148
	}
149
150
	public function getField($name) {
151
		return $this->dbRecord[$name];
152
	}
153
154
	public function getBoolean(string $name) : bool {
155
		if ($this->dbRecord[$name] === 'TRUE') {
156
			return true;
157
		} elseif ($this->dbRecord[$name] === 'FALSE') {
158
			return false;
159
		}
160
		$this->error('Field is not a boolean: ' . $name);
161
	}
162
163
	public function getInt($name) {
164
		return (int)$this->dbRecord[$name];
165
	}
166
167
	public function getFloat($name) {
168
		return (float)$this->dbRecord[$name];
169
	}
170
171
	public function getMicrotime(string $name) : string {
172
		// All digits of precision are stored in a MySQL bigint
173
		$data = $this->dbRecord[$name];
174
		return sprintf('%f', $data / 1E6);
175
	}
176
177
	public function getObject($name, $compressed = false, $nullable = false) {
178
		$object = $this->getField($name);
179
		if ($nullable === true && $object === null) {
180
			return null;
181
		}
182
		if ($compressed === true) {
183
			$object = gzuncompress($object);
184
		}
185
		return unserialize($object);
186
	}
187
188
	public function getRow() {
189
		return $this->dbRecord;
190
	}
191
192
	public function lockTable($table) {
193
		$this->dbConn->query('LOCK TABLES ' . $table . ' WRITE');
194
	}
195
196
	public function unlock() {
197
		$this->dbConn->query('UNLOCK TABLES');
198
	}
199
200
	public function getNumRows() {
201
		return $this->dbResult->num_rows;
202
	}
203
204
	public function getChangedRows() {
205
		return $this->dbConn->affected_rows;
206
	}
207
208
	public function getInsertID() {
209
		return $this->dbConn->insert_id;
210
	}
211
212
	protected function error($err) {
213
		throw new RuntimeException($err);
214
	}
215
216
	public function escape($escape) {
217
		if (is_bool($escape)) {
218
			return $this->escapeBoolean($escape);
219
		}
220
		if (is_numeric($escape)) {
221
			return $this->escapeNumber($escape);
222
		}
223
		if (is_string($escape)) {
224
			return $this->escapeString($escape);
225
		}
226
		if (is_array($escape)) {
227
			return $this->escapeArray($escape);
228
		}
229
		if (is_object($escape)) {
230
			return $this->escapeObject($escape);
231
		}
232
	}
233
234
	public function escapeString(?string $string, bool $nullable = false) : string {
235
		if ($nullable === true && ($string === null || $string === '')) {
236
			return 'NULL';
237
		}
238
		return '\'' . $this->dbConn->real_escape_string($string) . '\'';
239
	}
240
241
	public function escapeBinary($binary) {
242
		return '0x' . bin2hex($binary);
243
	}
244
245
	/**
246
	 * Warning: If escaping a nested array, use escapeIndividually=true,
247
	 * but beware that the escaped array is flattened!
248
	 */
249
	public function escapeArray(array $array, string $delimiter = ',', bool $escapeIndividually = true) : string {
250
		if ($escapeIndividually) {
251
			$string = join($delimiter, array_map(function($item) { return $this->escape($item); }, $array));
252
		} else {
253
			$string = $this->escape(join($delimiter, $array));
254
		}
255
		return $string;
256
	}
257
258
	public function escapeNumber($num) {
259
		// Numbers need not be quoted in MySQL queries, so if we know $num is
260
		// numeric, we can simply return its value (no quoting or escaping).
261
		if (is_numeric($num)) {
262
			return $num;
263
		} else {
264
			$this->error('Not a number! (' . $num . ')');
265
		}
266
	}
267
268
	public function escapeMicrotime(float $microtime) : string {
269
		// Retain all digits of precision for storing in a MySQL bigint
270
		return sprintf('%d', $microtime * 1E6);
271
	}
272
273
	public function escapeBoolean(bool $bool) : string {
274
		// We store booleans as an enum
275
		if ($bool) {
276
			return '\'TRUE\'';
277
		} else {
278
			return '\'FALSE\'';
279
		}
280
	}
281
282
	public function escapeObject($object, bool $compress = false, bool $nullable = false) : string {
283
		if ($nullable === true && $object === null) {
284
			return 'NULL';
285
		}
286
		if ($compress === true) {
287
			return $this->escapeBinary(gzcompress(serialize($object)));
288
		}
289
		return $this->escapeString(serialize($object));
290
	}
291
}
292