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