1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage Database
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 /**
13 * MySQL database driver
14 *
15 * @link https://dev.mysql.com/doc/
16 * @since 12.1
17 * @deprecated 4.0 Use MySQLi or PDO MySQL instead
18 */
19 class JDatabaseDriverMysql extends JDatabaseDriverMysqli
20 {
21 /**
22 * The name of the database driver.
23 *
24 * @var string
25 * @since 12.1
26 */
27 public $name = 'mysql';
28
29 /**
30 * Constructor.
31 *
32 * @param array $options Array of database options with keys: host, user, password, database, select.
33 *
34 * @since 12.1
35 */
36 public function __construct($options)
37 {
38 // PHP's `mysql` extension is not present in PHP 7, block instantiation in this environment
39 if (PHP_MAJOR_VERSION >= 7)
40 {
41 throw new JDatabaseExceptionUnsupported(
42 'This driver is unsupported in PHP 7, please use the MySQLi or PDO MySQL driver instead.'
43 );
44 }
45
46 // Get some basic values from the options.
47 $options['host'] = (isset($options['host'])) ? $options['host'] : 'localhost';
48 $options['user'] = (isset($options['user'])) ? $options['user'] : '';
49 $options['password'] = (isset($options['password'])) ? $options['password'] : '';
50 $options['database'] = (isset($options['database'])) ? $options['database'] : '';
51 $options['select'] = (isset($options['select'])) ? (bool) $options['select'] : true;
52
53 // Finalize initialisation.
54 parent::__construct($options);
55 }
56
57 /**
58 * Destructor.
59 *
60 * @since 12.1
61 */
62 public function __destruct()
63 {
64 $this->disconnect();
65 }
66
67 /**
68 * Connects to the database if needed.
69 *
70 * @return void Returns void if the database connected successfully.
71 *
72 * @since 12.1
73 * @throws RuntimeException
74 */
75 public function connect()
76 {
77 if ($this->connection)
78 {
79 return;
80 }
81
82 // Make sure the MySQL extension for PHP is installed and enabled.
83 if (!self::isSupported())
84 {
85 throw new JDatabaseExceptionUnsupported('Could not connect to MySQL.');
86 }
87
88 // Attempt to connect to the server.
89 if (!($this->connection = @ mysql_connect($this->options['host'], $this->options['user'], $this->options['password'], true)))
90 {
91 throw new JDatabaseExceptionConnecting('Could not connect to MySQL.');
92 }
93
94 // Set sql_mode to non_strict mode
95 mysql_query("SET @@SESSION.sql_mode = '';", $this->connection);
96
97 // If auto-select is enabled select the given database.
98 if ($this->options['select'] && !empty($this->options['database']))
99 {
100 $this->select($this->options['database']);
101 }
102
103 // Pre-populate the UTF-8 Multibyte compatibility flag based on server version
104 $this->utf8mb4 = $this->serverClaimsUtf8mb4Support();
105
106 // Set the character set (needed for MySQL 4.1.2+).
107 $this->utf = $this->setUtf();
108
109 // Turn MySQL profiling ON in debug mode:
110 if ($this->debug && $this->hasProfiling())
111 {
112 mysql_query('SET profiling = 1;', $this->connection);
113 }
114 }
115
116 /**
117 * Disconnects the database.
118 *
119 * @return void
120 *
121 * @since 12.1
122 */
123 public function disconnect()
124 {
125 // Close the connection.
126 if (is_resource($this->connection))
127 {
128 foreach ($this->disconnectHandlers as $h)
129 {
130 call_user_func_array($h, array( &$this));
131 }
132
133 mysql_close($this->connection);
134 }
135
136 $this->connection = null;
137 }
138
139 /**
140 * Method to escape a string for usage in an SQL statement.
141 *
142 * @param string $text The string to be escaped.
143 * @param boolean $extra Optional parameter to provide extra escaping.
144 *
145 * @return string The escaped string.
146 *
147 * @since 12.1
148 */
149 public function escape($text, $extra = false)
150 {
151 $this->connect();
152
153 $result = mysql_real_escape_string($text, $this->getConnection());
154
155 if ($extra)
156 {
157 $result = addcslashes($result, '%_');
158 }
159
160 return $result;
161 }
162
163 /**
164 * Test to see if the MySQL connector is available.
165 *
166 * @return boolean True on success, false otherwise.
167 *
168 * @since 12.1
169 */
170 public static function isSupported()
171 {
172 return PHP_MAJOR_VERSION < 7 && function_exists('mysql_connect');
173 }
174
175 /**
176 * Determines if the connection to the server is active.
177 *
178 * @return boolean True if connected to the database engine.
179 *
180 * @since 12.1
181 */
182 public function connected()
183 {
184 if (is_resource($this->connection))
185 {
186 return @mysql_ping($this->connection);
187 }
188
189 return false;
190 }
191
192 /**
193 * Get the number of affected rows by the last INSERT, UPDATE, REPLACE or DELETE for the previous executed SQL statement.
194 *
195 * @return integer The number of affected rows.
196 *
197 * @since 12.1
198 */
199 public function getAffectedRows()
200 {
201 $this->connect();
202
203 return mysql_affected_rows($this->connection);
204 }
205
206 /**
207 * Get the number of returned rows for the previous executed SQL statement.
208 * This command is only valid for statements like SELECT or SHOW that return an actual result set.
209 * To retrieve the number of rows affected by an INSERT, UPDATE, REPLACE or DELETE query, use getAffectedRows().
210 *
211 * @param resource $cursor An optional database cursor resource to extract the row count from.
212 *
213 * @return integer The number of returned rows.
214 *
215 * @since 12.1
216 */
217 public function getNumRows($cursor = null)
218 {
219 $this->connect();
220
221 return mysql_num_rows($cursor ? $cursor : $this->cursor);
222 }
223
224 /**
225 * Get the version of the database connector.
226 *
227 * @return string The database connector version.
228 *
229 * @since 12.1
230 */
231 public function getVersion()
232 {
233 $this->connect();
234
235 return mysql_get_server_info($this->connection);
236 }
237
238 /**
239 * Method to get the auto-incremented value from the last INSERT statement.
240 *
241 * @return integer The value of the auto-increment field from the last inserted row.
242 *
243 * @since 12.1
244 */
245 public function insertid()
246 {
247 $this->connect();
248
249 return mysql_insert_id($this->connection);
250 }
251
252 /**
253 * Execute the SQL statement.
254 *
255 * @return mixed A database cursor resource on success, boolean false on failure.
256 *
257 * @since 12.1
258 * @throws RuntimeException
259 */
260 public function execute()
261 {
262 $this->connect();
263
264 // Take a local copy so that we don't modify the original query and cause issues later
265 $query = $this->replacePrefix((string) $this->sql);
266
267 if (!($this->sql instanceof JDatabaseQuery) && ($this->limit > 0 || $this->offset > 0))
268 {
269 $query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
270 }
271
272 if (!is_resource($this->connection))
273 {
274 JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database');
275 throw new JDatabaseExceptionExecuting($query, $this->errorMsg, $this->errorNum);
276 }
277
278 // Increment the query counter.
279 $this->count++;
280
281 // Reset the error values.
282 $this->errorNum = 0;
283 $this->errorMsg = '';
284
285 // If debugging is enabled then let's log the query.
286 if ($this->debug)
287 {
288 // Add the query to the object queue.
289 $this->log[] = $query;
290
291 JLog::add($query, JLog::DEBUG, 'databasequery');
292
293 $this->timings[] = microtime(true);
294 }
295
296 // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
297 $this->cursor = @mysql_query($query, $this->connection);
298
299 if ($this->debug)
300 {
301 $this->timings[] = microtime(true);
302
303 if (defined('DEBUG_BACKTRACE_IGNORE_ARGS'))
304 {
305 $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
306 }
307 else
308 {
309 $this->callStacks[] = debug_backtrace();
310 }
311 }
312
313 // If an error occurred handle it.
314 if (!$this->cursor)
315 {
316 // Get the error number and message before we execute any more queries.
317 $this->errorNum = $this->getErrorNumber();
318 $this->errorMsg = $this->getErrorMessage();
319
320 // Check if the server was disconnected.
321 if (!$this->connected())
322 {
323 try
324 {
325 // Attempt to reconnect.
326 $this->connection = null;
327 $this->connect();
328 }
329 // If connect fails, ignore that exception and throw the normal exception.
330 catch (RuntimeException $e)
331 {
332 // Get the error number and message.
333 $this->errorNum = $this->getErrorNumber();
334 $this->errorMsg = $this->getErrorMessage();
335
336 // Throw the normal query exception.
337 JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error');
338
339 throw new JDatabaseExceptionExecuting($query, $this->errorMsg, $this->errorNum, $e);
340 }
341
342 // Since we were able to reconnect, run the query again.
343 return $this->execute();
344 }
345 // The server was not disconnected.
346 else
347 {
348 // Throw the normal query exception.
349 JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error');
350
351 throw new JDatabaseExceptionExecuting($query, $this->errorMsg, $this->errorNum);
352 }
353 }
354
355 return $this->cursor;
356 }
357
358 /**
359 * Select a database for use.
360 *
361 * @param string $database The name of the database to select for use.
362 *
363 * @return boolean True if the database was successfully selected.
364 *
365 * @since 12.1
366 * @throws RuntimeException
367 */
368 public function select($database)
369 {
370 $this->connect();
371
372 if (!$database)
373 {
374 return false;
375 }
376
377 if (!mysql_select_db($database, $this->connection))
378 {
379 throw new JDatabaseExceptionConnecting('Could not connect to database');
380 }
381
382 return true;
383 }
384
385 /**
386 * Set the connection to use UTF-8 character encoding.
387 *
388 * @return boolean True on success.
389 *
390 * @since 12.1
391 */
392 public function setUtf()
393 {
394 // If UTF is not supported return false immediately
395 if (!$this->utf)
396 {
397 return false;
398 }
399
400 // Make sure we're connected to the server
401 $this->connect();
402
403 // Which charset should I use, plain utf8 or multibyte utf8mb4?
404 $charset = $this->utf8mb4 ? 'utf8mb4' : 'utf8';
405
406 $result = @mysql_set_charset($charset, $this->connection);
407
408 /**
409 * If I could not set the utf8mb4 charset then the server doesn't support utf8mb4 despite claiming otherwise.
410 * This happens on old MySQL server versions (less than 5.5.3) using the mysqlnd PHP driver. Since mysqlnd
411 * masks the server version and reports only its own we can not be sure if the server actually does support
412 * UTF-8 Multibyte (i.e. it's MySQL 5.5.3 or later). Since the utf8mb4 charset is undefined in this case we
413 * catch the error and determine that utf8mb4 is not supported!
414 */
415 if (!$result && $this->utf8mb4)
416 {
417 $this->utf8mb4 = false;
418 $result = @mysql_set_charset('utf8', $this->connection);
419 }
420
421 return $result;
422 }
423
424 /**
425 * Method to fetch a row from the result set cursor as an array.
426 *
427 * @param mixed $cursor The optional result set cursor from which to fetch the row.
428 *
429 * @return mixed Either the next row from the result set or false if there are no more rows.
430 *
431 * @since 12.1
432 */
433 protected function fetchArray($cursor = null)
434 {
435 return mysql_fetch_row($cursor ? $cursor : $this->cursor);
436 }
437
438 /**
439 * Method to fetch a row from the result set cursor as an associative array.
440 *
441 * @param mixed $cursor The optional result set cursor from which to fetch the row.
442 *
443 * @return mixed Either the next row from the result set or false if there are no more rows.
444 *
445 * @since 12.1
446 */
447 protected function fetchAssoc($cursor = null)
448 {
449 return mysql_fetch_assoc($cursor ? $cursor : $this->cursor);
450 }
451
452 /**
453 * Method to fetch a row from the result set cursor as an object.
454 *
455 * @param mixed $cursor The optional result set cursor from which to fetch the row.
456 * @param string $class The class name to use for the returned row object.
457 *
458 * @return mixed Either the next row from the result set or false if there are no more rows.
459 *
460 * @since 12.1
461 */
462 protected function fetchObject($cursor = null, $class = 'stdClass')
463 {
464 return mysql_fetch_object($cursor ? $cursor : $this->cursor, $class);
465 }
466
467 /**
468 * Method to free up the memory used for the result set.
469 *
470 * @param mixed $cursor The optional result set cursor from which to fetch the row.
471 *
472 * @return void
473 *
474 * @since 12.1
475 */
476 protected function freeResult($cursor = null)
477 {
478 mysql_free_result($cursor ? $cursor : $this->cursor);
479 }
480
481 /**
482 * Internal function to check if profiling is available
483 *
484 * @return boolean
485 *
486 * @since 3.1.3
487 */
488 private function hasProfiling()
489 {
490 try
491 {
492 $res = mysql_query("SHOW VARIABLES LIKE 'have_profiling'", $this->connection);
493 $row = mysql_fetch_assoc($res);
494
495 return isset($row);
496 }
497 catch (Exception $e)
498 {
499 return false;
500 }
501 }
502
503 /**
504 * Does the database server claim to have support for UTF-8 Multibyte (utf8mb4) collation?
505 *
506 * libmysql supports utf8mb4 since 5.5.3 (same version as the MySQL server). mysqlnd supports utf8mb4 since 5.0.9.
507 *
508 * @return boolean
509 *
510 * @since CMS 3.5.0
511 */
512 private function serverClaimsUtf8mb4Support()
513 {
514 $client_version = mysql_get_client_info();
515 $server_version = $this->getVersion();
516
517 if (version_compare($server_version, '5.5.3', '<'))
518 {
519 return false;
520 }
521 else
522 {
523 if (strpos($client_version, 'mysqlnd') !== false)
524 {
525 $client_version = preg_replace('/^\D+([\d.]+).*/', '$1', $client_version);
526
527 return version_compare($client_version, '5.0.9', '>=');
528 }
529 else
530 {
531 return version_compare($client_version, '5.5.3', '>=');
532 }
533 }
534 }
535
536 /**
537 * Return the actual SQL Error number
538 *
539 * @return integer The SQL Error number
540 *
541 * @since 3.4.6
542 */
543 protected function getErrorNumber()
544 {
545 return (int) mysql_errno($this->connection);
546 }
547
548 /**
549 * Return the actual SQL Error message
550 *
551 * @return string The SQL Error message
552 *
553 * @since 3.4.6
554 */
555 protected function getErrorMessage()
556 {
557 $errorMessage = (string) mysql_error($this->connection);
558
559 // Replace the Databaseprefix with `#__` if we are not in Debug
560 if (!$this->debug)
561 {
562 $errorMessage = str_replace($this->tablePrefix, '#__', $errorMessage);
563 }
564
565 return $errorMessage;
566 }
567 }
568