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

Issues (412)

src/lib/Smr/Database.php (1 issue)

Severity
1
<?php declare(strict_types=1);
2
3
namespace Smr;
4
5
use Exception;
6
use mysqli;
7
use RuntimeException;
8
use Smr\Container\DiContainer;
9
10
/**
11
 * Wraps an active connection to the database.
12
 * Primarily provides query, escaping, and locking methods.
13
 */
14
class Database {
15
16
	/**
17
	 * Returns the instance of this class from the DI container.
18
	 * If one does not exist yet, it will be created.
19
	 * This is the intended way to construct this class.
20
	 */
21
	public static function getInstance(): self {
22
		return DiContainer::get(self::class);
23
	}
24
25
	/**
26
	 * This should not be needed except perhaps by persistent services
27
	 * (such as Dicord/IRC clients) to prevent connection timeouts between
28
	 * callbacks.
29
	 *
30
	 * Closes the underlying database connection and resets the state of the
31
	 * DI container so that a new Database and mysqli instance will be made
32
	 * the next time Database::getInstance() is called. Existing instances of
33
	 * this class will no longer be valid, and will throw when attempting to
34
	 * perform database operations.
35
	 *
36
	 * This function is safe to use even if the DI container or the Database
37
	 * instances have not been initialized yet.
38
	 */
39
	public static function resetInstance(): void {
40
		if (DiContainer::initialized(mysqli::class)) {
41
			$container = DiContainer::getContainer();
42
			if (DiContainer::initialized(self::class)) {
43
				self::getInstance()->dbConn->close();
44
				$container->reset(self::class);
45
			}
46
			$container->reset(mysqli::class);
47
		}
48
	}
49
50
	/**
51
	 * Used by the DI container to construct a mysqli instance.
52
	 * Not intended to be used outside the DI context.
53
	 */
54
	public static function mysqliFactory(DatabaseProperties $dbProperties): mysqli {
55
		if (!mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)) {
56
			throw new RuntimeException('Failed to enable mysqli error reporting');
57
		}
58
		$mysql = new mysqli(
59
			$dbProperties->getHost(),
60
			$dbProperties->getUser(),
61
			$dbProperties->getPassword(),
62
			$dbProperties->getDatabaseName()
63
		);
64
		$charset = $mysql->character_set_name();
65
		if ($charset != 'utf8') {
66
			throw new RuntimeException('Unexpected charset: ' . $charset);
67
		}
68
		return $mysql;
69
	}
70
71
	/**
72
	 * Not intended to be constructed by hand. If you need an instance of Database,
73
	 * use Database::getInstance();
74
	 *
75
	 * @param \mysqli $dbConn The mysqli instance
76
	 * @param string $dbName The name of the database that was used to construct the mysqli instance
77
	 */
78
	public function __construct(
79
		private readonly mysqli $dbConn,
80
		private readonly string $dbName,
81
	) {}
82
83
	/**
84
	 * This method will switch the connection to the specified database.
85
	 * Useful for switching back and forth between historical, and live databases.
86
	 *
87
	 * @param string $databaseName The name of the database to switch to
88
	 */
89
	public function switchDatabases(string $databaseName): void {
90
		$this->dbConn->select_db($databaseName);
91
	}
92
93
	/**
94
	 * Switch back to the configured live database
95
	 */
96
	public function switchDatabaseToLive(): void {
97
		$this->switchDatabases($this->dbName);
98
	}
99
100
	/**
101
	 * Returns the size of the current database in bytes.
102
	 */
103
	public function getDbBytes(): int {
104
		$query = 'SELECT SUM(data_length + index_length) as db_bytes FROM information_schema.tables WHERE table_schema=(SELECT database())';
105
		return $this->read($query)->record()->getInt('db_bytes');
106
	}
107
108
	/**
109
	 * Perform a write-only query on the database.
110
	 * Used for UPDATE, DELETE, REPLACE and INSERT queries, for example.
111
	 */
112
	public function write(string $query): void {
113
		$result = $this->dbConn->query($query);
114
		if ($result !== true) {
115
			throw new RuntimeException('Wrong query type or query failed');
116
		}
117
	}
118
119
	/**
120
	 * Perform a read-only query on the database.
121
	 * Used for SELECT queries, for example.
122
	 */
123
	public function read(string $query): DatabaseResult {
124
		$result = $this->dbConn->query($query);
125
		if (is_bool($result)) {
126
			throw new RuntimeException('Wrong query type or query failed');
127
		}
128
		return new DatabaseResult($result);
129
	}
130
131
	/**
132
	 * INSERT a row into $table.
133
	 *
134
	 * @param string $table
135
	 * @param array<string, mixed> $fields
136
	 * @return int Insert ID of auto-incrementing column, if applicable
137
	 */
138
	public function insert(string $table, array $fields): int {
139
		$query = 'INSERT INTO ' . $table . ' (' . implode(', ', array_keys($fields))
140
			. ') VALUES (' . implode(', ', array_values($fields)) . ')';
141
		$this->write($query);
142
		return $this->getInsertID();
143
	}
144
145
	/**
146
	 * REPLACE a row into $table.
147
	 *
148
	 * @param string $table
149
	 * @param array<string, mixed> $fields
150
	 * @return int Insert ID of auto-incrementing column, if applicable
151
	 */
152
	public function replace(string $table, array $fields): int {
153
		$query = 'REPLACE INTO ' . $table . ' (' . implode(', ', array_keys($fields))
154
			. ') VALUES (' . implode(', ', array_values($fields)) . ')';
155
		$this->write($query);
156
		return $this->getInsertID();
157
	}
158
159
	public function lockTable(string $table): void {
160
		$this->write('LOCK TABLES ' . $table . ' WRITE');
161
	}
162
163
	public function unlock(): void {
164
		$this->write('UNLOCK TABLES');
165
	}
166
167
	public function getChangedRows(): int {
168
		$affectedRows = $this->dbConn->affected_rows;
169
		if (is_string($affectedRows)) {
0 ignored issues
show
The condition is_string($affectedRows) is always false.
Loading history...
170
			throw new Exception('Number of rows is too large to represent as an int: ' . $affectedRows);
171
		}
172
		return $affectedRows;
173
	}
174
175
	public function getInsertID(): int {
176
		$insertID = $this->dbConn->insert_id;
177
		if (is_string($insertID)) {
178
			throw new Exception('Number of rows is too large to represent as an int: ' . $insertID);
179
		}
180
		return $insertID;
181
	}
182
183
	public function escape(mixed $escape): mixed {
184
		return match (true) {
185
			is_bool($escape) => $this->escapeBoolean($escape),
186
			is_numeric($escape) => $this->escapeNumber($escape),
187
			is_string($escape) => $this->escapeString($escape),
188
			is_array($escape) => $this->escapeArray($escape),
189
			is_object($escape) => $this->escapeObject($escape),
190
			default => throw new Exception('Unhandled value: ' . $escape)
191
		};
192
	}
193
194
	public function escapeString(?string $string, bool $nullable = false): string {
195
		if ($nullable === true && ($string === null || $string === '')) {
196
			return 'NULL';
197
		}
198
		return '\'' . $this->dbConn->real_escape_string($string) . '\'';
199
	}
200
201
	public function escapeBinary(string $binary): string {
202
		return '0x' . bin2hex($binary);
203
	}
204
205
	/**
206
	 * Warning: If escaping a nested array, beware that the escaped array is
207
	 * flattened!
208
	 *
209
	 * @param array<mixed> $array
210
	 */
211
	public function escapeArray(array $array): string {
212
		return implode(',', array_map(fn($item) => $this->escape($item), $array));
213
	}
214
215
	public function escapeNumber(mixed $num): mixed {
216
		// Numbers need not be quoted in MySQL queries, so if we know $num is
217
		// numeric, we can simply return its value (no quoting or escaping).
218
		if (!is_numeric($num)) {
219
			throw new RuntimeException('Not a number: ' . $num);
220
		}
221
		return $num;
222
	}
223
224
	public function escapeBoolean(bool $bool): string {
225
		// We store booleans as an enum
226
		return $bool ? '\'TRUE\'' : '\'FALSE\'';
227
	}
228
229
	public function escapeObject(mixed $object, bool $compress = false, bool $nullable = false): string {
230
		if ($nullable === true && $object === null) {
231
			return 'NULL';
232
		}
233
		$objectStr = serialize($object);
234
		if ($compress === true) {
235
			$objectBin = gzcompress($objectStr);
236
			if ($objectBin === false) {
237
				throw new Exception('An error occurred while compressing the object');
238
			}
239
			return $this->escapeBinary($objectBin);
240
		}
241
		return $this->escapeString($objectStr);
242
	}
243
244
}
245