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