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