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 / Pdo / Mysql.php
matomo / app / core / Tracker / Db / Pdo Last commit date
Mysql.php 3 months ago
Mysql.php
390 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
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 * @throws \Piwik\Tracker\Db\DbException
171 * @return string
172 */
173 public function fetchCol($sql, $bind = array())
174 {
175 try {
176 $sth = $this->query($sql, $bind);
177 if ($sth === \false) {
178 return \false;
179 }
180 $result = $sth->fetchAll(PDO::FETCH_COLUMN, 0);
181 return $result;
182 } catch (PDOException $e) {
183 throw new DbException("Error query: " . $e->getMessage());
184 }
185 }
186 /**
187 * Returns the first row of a query result, using optional bound parameters.
188 *
189 * @param string $query Query
190 * @param array $parameters Parameters to bind
191 * @return bool|mixed
192 * @see query()
193 * @throws Exception|DbException if an exception occurred
194 */
195 public function fetch($query, $parameters = array())
196 {
197 try {
198 $sth = $this->query($query, $parameters);
199 if ($sth === \false) {
200 return \false;
201 }
202 return $sth->fetch(PDO::FETCH_ASSOC);
203 } catch (PDOException $e) {
204 throw new DbException("Error query: " . $e->getMessage());
205 }
206 }
207 /**
208 * Executes a query, using optional bound parameters.
209 *
210 * @param string $query Query
211 * @param array|string $parameters Parameters to bind array('idsite'=> 1)
212 * @return PDOStatement|bool PDOStatement or false if failed
213 * @throws DbException if an exception occurred
214 */
215 public function query($query, $parameters = array())
216 {
217 try {
218 return $this->executeQuery($query, $parameters);
219 } catch (Exception $e) {
220 $isSelectQuery = stripos(trim($query), 'select ') === 0;
221 if ($isSelectQuery && null === $this->activeTransaction && $this->isMysqlServerHasGoneAwayError($e)) {
222 // mysql may return a MySQL server has gone away error when trying to execute the query
223 // in that case we want to retry establishing the connection once after a short sleep
224 // we're only retrying SELECT queries to prevent updating or inserting records twice for some reason
225 // when transactions are used, then we just want it to fail as we'd be only writing partial data
226 $this->reconnect($e);
227 return $this->executeQuery($query, $parameters);
228 } else {
229 $message = $e->getMessage() . " In query: {$query} Parameters: " . var_export($parameters, \true);
230 throw new DbException("Error query: " . $message, (int) $e->getCode());
231 }
232 }
233 }
234 /**
235 * @internal for tests only
236 * @throws Exception
237 */
238 public function reconnect(Exception $e)
239 {
240 $this->disconnect();
241 usleep(100 * 1000);
242 // wait for 100ms
243 try {
244 $this->establishConnection();
245 } catch (Exception $exceptionReconnect) {
246 // forward the original exception so we get a better stack trace of where this error happens
247 // and what happened originally
248 throw $e;
249 }
250 }
251 /**
252 * Executes a query, using optional bound parameters.
253 *
254 * @param string $query Query
255 * @param array|string $parameters Parameters to bind array('idsite'=> 1)
256 * @return PDOStatement|bool PDOStatement or false if failed
257 * @throws DbException if an exception occurred
258 */
259 private function executeQuery($query, $parameters = array())
260 {
261 if (is_null($this->connection)) {
262 return \false;
263 }
264 try {
265 if (self::$profiling) {
266 $timer = $this->initProfiler();
267 }
268 if (!is_array($parameters)) {
269 $parameters = array($parameters);
270 }
271 $sth = $this->connection->prepare($query);
272 $sth->execute($parameters);
273 if (self::$profiling && isset($timer)) {
274 $this->recordQueryProfile($query, $timer);
275 }
276 return $sth;
277 } catch (PDOException $e) {
278 $message = $e->getMessage() . " In query: {$query} Parameters: " . var_export($parameters, \true);
279 throw new DbException("Error query: " . $message, (int) $e->getCode());
280 }
281 }
282 /**
283 * Returns the last inserted ID in the DB
284 * Wrapper of PDO::lastInsertId()
285 *
286 * @return int
287 */
288 public function lastInsertId()
289 {
290 return $this->connection->lastInsertId();
291 }
292 /**
293 * Test error number
294 *
295 * @param Exception $e
296 * @param string $errno
297 * @return bool
298 */
299 public function isErrNo($e, $errno)
300 {
301 return \Piwik\Db\Adapter\Pdo\Mysql::isPdoErrorNumber($e, $errno);
302 }
303 /**
304 * Return number of affected rows in last query
305 *
306 * @param mixed $queryResult Result from query()
307 * @return int
308 */
309 public function rowCount($queryResult)
310 {
311 return $queryResult->rowCount();
312 }
313 /**
314 * Start Transaction
315 * @return ?string TransactionID
316 */
317 public function beginTransaction()
318 {
319 if ($this->activeTransaction !== null) {
320 return;
321 }
322 try {
323 $success = $this->connection->beginTransaction();
324 } catch (Exception $e) {
325 if ($this->isMysqlServerHasGoneAwayError($e)) {
326 // mysql may return a MySQL server has gone away error when trying begin transaction, in that case we
327 // want to retry this once
328 $this->reconnect($e);
329 $success = $this->connection->beginTransaction();
330 } else {
331 throw $e;
332 }
333 }
334 if ($success) {
335 $this->activeTransaction = uniqid();
336 return $this->activeTransaction;
337 }
338 }
339 /**
340 * Commit Transaction
341 * @param $xid
342 * @throws DbException
343 * @internal param TransactionID $string from beginTransaction
344 */
345 public function commit($xid)
346 {
347 if ($this->activeTransaction != $xid || $this->activeTransaction === null) {
348 return;
349 }
350 $this->activeTransaction = null;
351 if (!$this->connection->commit()) {
352 throw new DbException("Commit failed");
353 }
354 }
355 /**
356 * Rollback Transaction
357 * @param $xid
358 * @throws DbException
359 * @internal param TransactionID $string from beginTransaction
360 */
361 public function rollBack($xid)
362 {
363 if ($this->activeTransaction != $xid || $this->activeTransaction === null) {
364 return;
365 }
366 $this->activeTransaction = null;
367 if (!$this->connection->rollBack()) {
368 throw new DbException("Rollback failed");
369 }
370 }
371 private function establishConnection() : void
372 {
373 $this->connection = @new PDO($this->dsn, $this->username, $this->password, $this->mysqlOptions);
374 // we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked
375 // the matomo.php would stay waiting for the database... bad!
376 /*
377 * Lazy initialization via MYSQL_ATTR_INIT_COMMAND depends
378 * on mysqlnd support, PHP version, and OS.
379 * see ZF-7428 and https://bugs.php.net/bug.php?id=47224
380 */
381 if (!empty($this->charset)) {
382 $sql = "SET NAMES '" . $this->charset . "'";
383 if (!empty($this->collation)) {
384 $sql .= " COLLATE '" . $this->collation . "'";
385 }
386 $this->connection->exec($sql);
387 }
388 }
389 }
390