PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Tracker / Db / Mysqli.php
matomo / app / core / Tracker / Db Last commit date
Pdo 3 months ago DbException.php 2 years ago Mysqli.php 7 months ago
Mysqli.php
408 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\Tracker\Db;
10
11 use Exception;
12 use Piwik\Tracker\Db;
13 /**
14 * mysqli wrapper
15 *
16 */
17 class Mysqli extends Db
18 {
19 protected $connection = null;
20 protected $host;
21 protected $port;
22 protected $socket;
23 protected $dbname;
24 protected $username;
25 protected $password;
26 protected $charset;
27 protected $collation;
28 protected $activeTransaction = null;
29 protected $enable_ssl;
30 protected $ssl_key;
31 protected $ssl_cert;
32 protected $ssl_ca;
33 protected $ssl_ca_path;
34 protected $ssl_cipher;
35 protected $ssl_no_verify;
36 protected $paramNb;
37 protected $params;
38 /**
39 * Builds the DB object
40 *
41 * @param array $dbInfo
42 */
43 public function __construct($dbInfo)
44 {
45 if (isset($dbInfo['unix_socket']) && substr($dbInfo['unix_socket'], 0, 1) == '/') {
46 $this->host = null;
47 $this->port = null;
48 $this->socket = $dbInfo['unix_socket'];
49 } elseif (isset($dbInfo['port']) && substr($dbInfo['port'], 0, 1) == '/') {
50 $this->host = null;
51 $this->port = null;
52 $this->socket = $dbInfo['port'];
53 } else {
54 $this->host = $dbInfo['host'];
55 $this->port = (int) $dbInfo['port'];
56 $this->socket = null;
57 }
58 $this->dbname = $dbInfo['dbname'];
59 $this->username = $dbInfo['username'];
60 $this->password = $dbInfo['password'];
61 $this->charset = $dbInfo['charset'] ?? null;
62 $this->collation = $dbInfo['collation'] ?? null;
63 if (!empty($dbInfo['enable_ssl'])) {
64 $this->enable_ssl = $dbInfo['enable_ssl'];
65 }
66 if (!empty($dbInfo['ssl_key'])) {
67 $this->ssl_key = $dbInfo['ssl_key'];
68 }
69 if (!empty($dbInfo['ssl_cert'])) {
70 $this->ssl_cert = $dbInfo['ssl_cert'];
71 }
72 if (!empty($dbInfo['ssl_ca'])) {
73 $this->ssl_ca = $dbInfo['ssl_ca'];
74 }
75 if (!empty($dbInfo['ssl_ca_path'])) {
76 $this->ssl_ca_path = $dbInfo['ssl_ca_path'];
77 }
78 if (!empty($dbInfo['ssl_cipher'])) {
79 $this->ssl_cipher = $dbInfo['ssl_cipher'];
80 }
81 if (!empty($dbInfo['ssl_no_verify'])) {
82 $this->ssl_no_verify = $dbInfo['ssl_no_verify'];
83 }
84 }
85 /**
86 * Destructor
87 */
88 public function __destruct()
89 {
90 $this->connection = null;
91 }
92 /**
93 * Connects to the DB
94 *
95 * @throws Exception|DbException if there was an error connecting the DB
96 */
97 public function connect()
98 {
99 if (self::$profiling) {
100 $timer = $this->initProfiler();
101 }
102 // The default error reporting of mysqli changed in PHP 8.1. To circumvent problems in our error handling we set
103 // the erroring reporting to the default that was used prior PHP 8.1
104 // See https://php.watch/versions/8.1/mysqli-error-mode for more details
105 mysqli_report(\MYSQLI_REPORT_OFF);
106 $this->connection = mysqli_init();
107 if ($this->enable_ssl) {
108 mysqli_ssl_set($this->connection, $this->ssl_key, $this->ssl_cert, $this->ssl_ca, $this->ssl_ca_path, $this->ssl_cipher);
109 }
110 // Make sure MySQL returns all matched rows on update queries including
111 // rows that actually didn't have to be updated because the values didn't
112 // change. This matches common behaviour among other database systems.
113 // See #6296 why this is important in tracker
114 $flags = \MYSQLI_CLIENT_FOUND_ROWS;
115 if ($this->enable_ssl) {
116 $flags = $flags | \MYSQLI_CLIENT_SSL;
117 }
118 if ($this->ssl_no_verify && defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT')) {
119 $flags = $flags | \MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
120 }
121 mysqli_real_connect($this->connection, $this->host, $this->username, $this->password, $this->dbname, $this->port, $this->socket, $flags);
122 if (!$this->connection || mysqli_connect_errno()) {
123 throw new \Piwik\Tracker\Db\DbException("Connect failed: " . mysqli_connect_error());
124 }
125 if ($this->charset && $this->collation) {
126 // mysqli_set_charset does not support setting a collation
127 $query = "SET NAMES '" . $this->charset . "' COLLATE '" . $this->collation . "'";
128 if (!mysqli_query($this->connection, $query)) {
129 throw new \Piwik\Tracker\Db\DbException("Set charset/connection collation failed: " . mysqli_error($this->connection));
130 }
131 } elseif ($this->charset) {
132 if (!mysqli_set_charset($this->connection, $this->charset)) {
133 throw new \Piwik\Tracker\Db\DbException("Set Charset failed: " . mysqli_error($this->connection));
134 }
135 }
136 $this->password = '';
137 if (self::$profiling && isset($timer)) {
138 $this->recordQueryProfile('connect', $timer);
139 }
140 }
141 /**
142 * Disconnects from the server
143 */
144 public function disconnect()
145 {
146 mysqli_close($this->connection);
147 $this->connection = null;
148 }
149 /**
150 * @param \mysqli_stmt $stmt
151 * @param $fields
152 * @return array|bool|false
153 */
154 private function fetchResult($stmt, $fields)
155 {
156 $values = array_fill(0, count($fields), null);
157 $refs = array();
158 foreach ($values as $i => &$f) {
159 $refs[$i] =& $f;
160 }
161 call_user_func_array(array($stmt, 'bind_result'), $values);
162 $result = $stmt->fetch();
163 if ($result === null || $result === \false) {
164 $stmt->reset();
165 return \false;
166 }
167 $val = array();
168 foreach ($values as $key => $value) {
169 $val[] = $value;
170 }
171 $row = array_combine($fields, $values);
172 return $row;
173 }
174 /**
175 * Returns an array containing all the rows of a query result, using optional bound parameters.
176 *
177 * @see query()
178 *
179 * @param string $query Query
180 * @param array $parameters Parameters to bind
181 * @return array
182 * @throws Exception|DbException if an exception occurred
183 */
184 public function fetchAll($query, $parameters = array())
185 {
186 try {
187 if (self::$profiling) {
188 $timer = $this->initProfiler();
189 }
190 list($stmt, $fields) = $this->executeQuery($query, $parameters);
191 $rows = array();
192 while ($row = $this->fetchResult($stmt, $fields)) {
193 $rows[] = $row;
194 }
195 $stmt->free_result();
196 $stmt->close();
197 if (self::$profiling && isset($timer)) {
198 $this->recordQueryProfile($query, $timer);
199 }
200 return $rows;
201 } catch (Exception $e) {
202 throw new \Piwik\Tracker\Db\DbException("Error query: " . $e->getMessage());
203 }
204 }
205 /**
206 * Returns the first row of a query result, using optional bound parameters.
207 *
208 * @see query()
209 *
210 * @param string $query Query
211 * @param array $parameters Parameters to bind
212 *
213 * @return array
214 *
215 * @throws DbException if an exception occurred
216 */
217 public function fetch($query, $parameters = array())
218 {
219 try {
220 if (self::$profiling) {
221 $timer = $this->initProfiler();
222 }
223 list($stmt, $fields) = $this->executeQuery($query, $parameters);
224 $row = $this->fetchResult($stmt, $fields);
225 $stmt->free_result();
226 $stmt->close();
227 if (self::$profiling && isset($timer)) {
228 $this->recordQueryProfile($query, $timer);
229 }
230 if ($row === null) {
231 $row = \false;
232 }
233 return $row;
234 } catch (Exception $e) {
235 throw new \Piwik\Tracker\Db\DbException("Error query: " . $e->getMessage());
236 }
237 }
238 /**
239 * Executes a query, using optional bound parameters.
240 *
241 * @param string $query Query
242 * @param array|string $parameters Parameters to bind array('idsite'=> 1)
243 *
244 * @return bool|resource false if failed
245 * @throws DbException if an exception occurred
246 */
247 public function query($query, $parameters = array())
248 {
249 if (is_null($this->connection)) {
250 return \false;
251 }
252 try {
253 if (self::$profiling) {
254 $timer = $this->initProfiler();
255 }
256 list($stmt, $fields) = $this->executeQuery($query, $parameters);
257 if (self::$profiling && isset($timer)) {
258 $this->recordQueryProfile($query, $timer);
259 }
260 return $stmt;
261 } catch (Exception $e) {
262 throw new \Piwik\Tracker\Db\DbException("Error query: " . $e->getMessage() . "\n In query: {$query}\n Parameters: " . var_export($parameters, \true), $e->getCode());
263 }
264 }
265 /**
266 * Returns the last inserted ID in the DB
267 *
268 * @return int
269 */
270 public function lastInsertId()
271 {
272 return mysqli_insert_id($this->connection);
273 }
274 private function executeQuery($sql, $bind)
275 {
276 $stmt = mysqli_prepare($this->connection, $sql);
277 if (!$stmt) {
278 throw new \Piwik\Tracker\Db\DbException('preparing query failed: ' . mysqli_error($this->connection) . ' : ' . $sql);
279 }
280 if (!is_array($bind)) {
281 $bind = array($bind);
282 }
283 if (!empty($bind)) {
284 array_unshift($bind, str_repeat('s', count($bind)));
285 $refs = array();
286 foreach ($bind as $key => $value) {
287 $refs[$key] =& $bind[$key];
288 }
289 call_user_func_array(array($stmt, 'bind_param'), $refs);
290 }
291 $stmtResult = $stmt->execute();
292 if ($stmtResult === \false) {
293 throw new \Piwik\Tracker\Db\DbException("Mysqli statement execute error : " . $stmt->error, $stmt->errno);
294 }
295 if (!empty($stmt->error)) {
296 throw new \Piwik\Tracker\Db\DbException('executeQuery() failed: ' . mysqli_error($this->connection) . ' : ' . $sql);
297 }
298 $metaResults = $stmt->result_metadata();
299 if ($stmt->errno) {
300 throw new \Piwik\Tracker\Db\DbException("Mysqli statement metadata error: " . $stmt->error, $stmt->errno);
301 }
302 $fields = array();
303 if ($metaResults) {
304 $fetchedFields = $metaResults->fetch_fields();
305 foreach ($fetchedFields as $fetchedField) {
306 $fields[] = $fetchedField->name;
307 }
308 $stmt->store_result();
309 }
310 return array($stmt, $fields);
311 }
312 /**
313 * Input is a prepared SQL statement and parameters
314 * Returns the SQL statement
315 *
316 * @param string $query
317 * @param array $parameters
318 * @return string
319 */
320 private function prepare($query, $parameters)
321 {
322 if (!$parameters) {
323 $parameters = array();
324 } elseif (!is_array($parameters)) {
325 $parameters = array($parameters);
326 }
327 $this->paramNb = 0;
328 $this->params =& $parameters;
329 $query = preg_replace_callback('/\\?/', array($this, 'replaceParam'), $query);
330 return $query;
331 }
332 public function replaceParam($match)
333 {
334 $param =& $this->params[$this->paramNb];
335 $this->paramNb++;
336 if ($param === null) {
337 return 'NULL';
338 } else {
339 return "'" . addslashes($param) . "'";
340 }
341 }
342 public function isErrNo($e, $errno)
343 {
344 return \Piwik\Db\Adapter\Mysqli::isMysqliErrorNumber($e, $this->connection, $errno);
345 }
346 /**
347 * Return number of affected rows in last query
348 *
349 * @param mixed $queryResult Result from query()
350 * @return int
351 */
352 public function rowCount($queryResult)
353 {
354 if (!empty($queryResult) && is_object($queryResult) && $queryResult instanceof \mysqli_stmt) {
355 return $queryResult->affected_rows;
356 }
357 return mysqli_affected_rows($this->connection);
358 }
359 /**
360 * Start Transaction
361 * @return ?string TransactionID
362 */
363 public function beginTransaction()
364 {
365 if ($this->activeTransaction !== null) {
366 return;
367 }
368 if ($this->connection->autocommit(\false)) {
369 $this->activeTransaction = uniqid();
370 return $this->activeTransaction;
371 }
372 }
373 /**
374 * Commit Transaction
375 * @param $xid
376 * @throws DbException
377 * @internal param TransactionID $string from beginTransaction
378 */
379 public function commit($xid)
380 {
381 if ($this->activeTransaction != $xid || $this->activeTransaction === null) {
382 return;
383 }
384 $this->activeTransaction = null;
385 if (!$this->connection->commit()) {
386 throw new \Piwik\Tracker\Db\DbException("Commit failed");
387 }
388 $this->connection->autocommit(\true);
389 }
390 /**
391 * Rollback Transaction
392 * @param $xid
393 * @throws DbException
394 * @internal param TransactionID $string from beginTransaction
395 */
396 public function rollBack($xid)
397 {
398 if ($this->activeTransaction != $xid || $this->activeTransaction === null) {
399 return;
400 }
401 $this->activeTransaction = null;
402 if (!$this->connection->rollback()) {
403 throw new \Piwik\Tracker\Db\DbException("Rollback failed");
404 }
405 $this->connection->autocommit(\true);
406 }
407 }
408