PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.12.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.12.1
5.12.1 5.12.0 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 / Pdo / Mysql.php
matomo / app / core / Tracker / Db / Pdo Last commit date
Mysql.php 2 weeks ago
Mysql.php
385 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\Pdo;
10
11 use Exception;
12 use PDO;
13 use PDOException;
14 use PDOStatement;
15 use Piwik\Tracker\Db;
16 use Piwik\Tracker\Db\DbException;
17 /**
18 * PDO MySQL wrapper
19 *
20 */
21 class Mysql extends Db
22 {
23 /**
24 * @var PDO|null
25 */
26 protected $connection = null;
27 /**
28 * @var string
29 */
30 protected $dsn;
31 /**
32 * @var string
33 */
34 private $username;
35 /**
36 * @var string
37 */
38 private $password;
39 /**
40 * @var string|null
41 */
42 protected $charset;
43 /**
44 * @var string|null
45 */
46 private $collation;
47 protected $mysqlOptions = [];
48 protected $activeTransaction = null;
49 /**
50 * Builds the DB object
51 *
52 * @param array $dbInfo
53 * @param string $driverName
54 */
55 public function __construct($dbInfo, $driverName = 'mysql')
56 {
57 if (isset($dbInfo['unix_socket']) && substr($dbInfo['unix_socket'], 0, 1) == '/') {
58 $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['unix_socket'];
59 } elseif (!empty($dbInfo['port']) && substr($dbInfo['port'], 0, 1) == '/') {
60 $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['port'];
61 } else {
62 $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';host=' . $dbInfo['host'] . ';port=' . $dbInfo['port'];
63 }
64 $this->username = $dbInfo['username'];
65 $this->password = $dbInfo['password'];
66 if (isset($dbInfo['charset'])) {
67 $this->charset = $dbInfo['charset'];
68 $this->dsn .= ';charset=' . $this->charset;
69 if (!empty($dbInfo['collation'])) {
70 $this->collation = $dbInfo['collation'];
71 }
72 }
73 if (isset($dbInfo['enable_ssl']) && $dbInfo['enable_ssl']) {
74 if (!empty($dbInfo['ssl_key'])) {
75 $this->mysqlOptions[PDO::MYSQL_ATTR_SSL_KEY] = $dbInfo['ssl_key'];
76 }
77 if (!empty($dbInfo['ssl_cert'])) {
78 $this->mysqlOptions[PDO::MYSQL_ATTR_SSL_CERT] = $dbInfo['ssl_cert'];
79 }
80 if (!empty($dbInfo['ssl_ca'])) {
81 $this->mysqlOptions[PDO::MYSQL_ATTR_SSL_CA] = $dbInfo['ssl_ca'];
82 }
83 if (!empty($dbInfo['ssl_ca_path'])) {
84 $this->mysqlOptions[PDO::MYSQL_ATTR_SSL_CAPATH] = $dbInfo['ssl_ca_path'];
85 }
86 if (!empty($dbInfo['ssl_cipher'])) {
87 $this->mysqlOptions[PDO::MYSQL_ATTR_SSL_CIPHER] = $dbInfo['ssl_cipher'];
88 }
89 if (!empty($dbInfo['ssl_no_verify']) && defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
90 $this->mysqlOptions[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = \false;
91 }
92 }
93 }
94 public function __destruct()
95 {
96 $this->connection = null;
97 }
98 /**
99 * Connects to the DB
100 *
101 * @throws Exception if there was an error connecting the DB
102 */
103 public function connect()
104 {
105 if (self::$profiling) {
106 $timer = $this->initProfiler();
107 }
108 // Make sure MySQL returns all matched rows on update queries including
109 // rows that actually didn't have to be updated because the values didn't
110 // change. This matches common behaviour among other database systems.
111 // See #6296 why this is important in tracker
112 $this->mysqlOptions[PDO::MYSQL_ATTR_FOUND_ROWS] = \true;
113 $this->mysqlOptions[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
114 try {
115 $this->establishConnection();
116 } catch (Exception $e) {
117 if ($this->isMysqlServerHasGoneAwayError($e)) {
118 // mysql may return a MySQL server has gone away error when trying to establish the connection.
119 // in that case we want to retry establishing the connection once after a short sleep
120 $this->reconnect($e);
121 } else {
122 throw $e;
123 }
124 }
125 if (self::$profiling && isset($timer)) {
126 $this->recordQueryProfile('connect', $timer);
127 }
128 }
129 /**
130 * @internal tests only
131 * @return bool
132 */
133 public function isMysqlServerHasGoneAwayError(Exception $e)
134 {
135 return $this->isErrNo($e, \Piwik\Updater\Migration\Db::ERROR_CODE_MYSQL_SERVER_HAS_GONE_AWAY) || stripos($e->getMessage(), 'MySQL server has gone away') !== \false;
136 }
137 /**
138 * Disconnects from the server
139 */
140 public function disconnect()
141 {
142 $this->connection = null;
143 }
144 /**
145 * Returns an array containing all the rows of a query result, using optional bound parameters.
146 *
147 * @param string $query Query
148 * @param array $parameters Parameters to bind
149 * @return array|bool
150 * @see query()
151 * @throws Exception|DbException if an exception occurred
152 */
153 public function fetchAll($query, $parameters = array())
154 {
155 try {
156 $sth = $this->query($query, $parameters);
157 if ($sth === \false) {
158 return \false;
159 }
160 return $sth->fetchAll(PDO::FETCH_ASSOC);
161 } catch (PDOException $e) {
162 throw new DbException("Error query: " . $e->getMessage());
163 }
164 }
165 /**
166 * Fetches the first column of all SQL result rows as an array.
167 *
168 * @param string $sql An SQL SELECT statement.
169 * @param mixed $bind Data to bind into SELECT placeholders.
170 * @return array|false
171 */
172 public function fetchCol($sql, $bind = array())
173 {
174 try {
175 $sth = $this->query($sql, $bind);
176 if ($sth === \false) {
177 return \false;
178 }
179 $result = $sth->fetchAll(PDO::FETCH_COLUMN, 0);
180 return $result;
181 } catch (PDOException $e) {
182 throw new DbException("Error query: " . $e->getMessage());
183 }
184 }
185 /**
186 * Returns the first row of a query result, using optional bound parameters.
187 *
188 * @param string $query Query
189 * @param array $parameters Parameters to bind
190 * @return bool|mixed
191 * @see query()
192 * @throws Exception|DbException if an exception occurred
193 */
194 public function fetch($query, $parameters = array())
195 {
196 try {
197 $sth = $this->query($query, $parameters);
198 if ($sth === \false) {
199 return \false;
200 }
201 return $sth->fetch(PDO::FETCH_ASSOC);
202 } catch (PDOException $e) {
203 throw new DbException("Error query: " . $e->getMessage());
204 }
205 }
206 /**
207 * Executes a query, using optional bound parameters.
208 *
209 * @param string $query Query
210 * @param array|string $parameters Parameters to bind array('idsite'=> 1)
211 * @return PDOStatement|bool PDOStatement or false if failed
212 * @throws DbException if an exception occurred
213 */
214 public function query($query, $parameters = array())
215 {
216 try {
217 return $this->executeQuery($query, $parameters);
218 } catch (Exception $e) {
219 $isSelectQuery = stripos(trim($query), 'select ') === 0;
220 if ($isSelectQuery && null === $this->activeTransaction && $this->isMysqlServerHasGoneAwayError($e)) {
221 // mysql may return a MySQL server has gone away error when trying to execute the query
222 // in that case we want to retry establishing the connection once after a short sleep
223 // we're only retrying SELECT queries to prevent updating or inserting records twice for some reason
224 // when transactions are used, then we just want it to fail as we'd be only writing partial data
225 $this->reconnect($e);
226 return $this->executeQuery($query, $parameters);
227 } else {
228 $message = $e->getMessage() . " In query: {$query} Parameters: " . var_export($parameters, \true);
229 throw new DbException("Error query: " . $message, (int) $e->getCode());
230 }
231 }
232 }
233 /**
234 * @internal for tests only
235 * @throws Exception
236 */
237 public function reconnect(Exception $e)
238 {
239 $this->disconnect();
240 usleep(100 * 1000);
241 // wait for 100ms
242 try {
243 $this->establishConnection();
244 } catch (Exception $exceptionReconnect) {
245 // forward the original exception so we get a better stack trace of where this error happens
246 // and what happened originally
247 throw $e;
248 }
249 }
250 /**
251 * Executes a query, using optional bound parameters.
252 *
253 * @param string $query Query
254 * @param array|string $parameters Parameters to bind array('idsite'=> 1)
255 * @return PDOStatement|bool PDOStatement or false if failed
256 * @throws DbException if an exception occurred
257 */
258 private function executeQuery($query, $parameters = array())
259 {
260 if (is_null($this->connection)) {
261 return \false;
262 }
263 try {
264 if (self::$profiling) {
265 $timer = $this->initProfiler();
266 }
267 if (!is_array($parameters)) {
268 $parameters = array($parameters);
269 }
270 $sth = $this->connection->prepare($query);
271 $sth->execute($parameters);
272 if (self::$profiling && isset($timer)) {
273 $this->recordQueryProfile($query, $timer);
274 }
275 return $sth;
276 } catch (PDOException $e) {
277 $message = $e->getMessage() . " In query: {$query} Parameters: " . var_export($parameters, \true);
278 throw new DbException("Error query: " . $message, (int) $e->getCode());
279 }
280 }
281 /**
282 * Returns the last inserted ID in the DB
283 * Wrapper of PDO::lastInsertId()
284 *
285 * @return int
286 */
287 public function lastInsertId()
288 {
289 return $this->connection->lastInsertId();
290 }
291 /**
292 * Test error number
293 *
294 * @param Exception $e
295 * @param string $errno
296 * @return bool
297 */
298 public function isErrNo($e, $errno)
299 {
300 return \Piwik\Db\Adapter\Pdo\Mysql::isPdoErrorNumber($e, $errno);
301 }
302 /**
303 * Return number of affected rows in last query
304 *
305 * @param mixed $queryResult Result from query()
306 * @return int
307 */
308 public function rowCount($queryResult)
309 {
310 return $queryResult->rowCount();
311 }
312 /**
313 * Start Transaction
314 * @return ?string TransactionID
315 */
316 public function beginTransaction()
317 {
318 if ($this->activeTransaction !== null) {
319 return;
320 }
321 try {
322 $success = $this->connection->beginTransaction();
323 } catch (Exception $e) {
324 if ($this->isMysqlServerHasGoneAwayError($e)) {
325 // mysql may return a MySQL server has gone away error when trying begin transaction, in that case we
326 // want to retry this once
327 $this->reconnect($e);
328 $success = $this->connection->beginTransaction();
329 } else {
330 throw $e;
331 }
332 }
333 if ($success) {
334 $this->activeTransaction = uniqid();
335 return $this->activeTransaction;
336 }
337 }
338 /**
339 * Commit Transaction
340 * @param $xid
341 */
342 public function commit($xid)
343 {
344 if ($this->activeTransaction != $xid || $this->activeTransaction === null) {
345 return;
346 }
347 $this->activeTransaction = null;
348 if (!$this->connection->commit()) {
349 throw new DbException("Commit failed");
350 }
351 }
352 /**
353 * Rollback Transaction
354 * @param $xid
355 */
356 public function rollBack($xid)
357 {
358 if ($this->activeTransaction != $xid || $this->activeTransaction === null) {
359 return;
360 }
361 $this->activeTransaction = null;
362 if (!$this->connection->rollBack()) {
363 throw new DbException("Rollback failed");
364 }
365 }
366 private function establishConnection() : void
367 {
368 $this->connection = @new PDO($this->dsn, $this->username, $this->password, $this->mysqlOptions);
369 // we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked
370 // the matomo.php would stay waiting for the database... bad!
371 /*
372 * Lazy initialization via MYSQL_ATTR_INIT_COMMAND depends
373 * on mysqlnd support, PHP version, and OS.
374 * see ZF-7428 and https://bugs.php.net/bug.php?id=47224
375 */
376 if (!empty($this->charset)) {
377 $sql = "SET NAMES '" . $this->charset . "'";
378 if (!empty($this->collation)) {
379 $sql .= " COLLATE '" . $this->collation . "'";
380 }
381 $this->connection->exec($sql);
382 }
383 }
384 }
385