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