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 * MySQLi database driver
17 *
18 * @see http://php.net/manual/en/book.mysqli.php
19 * @since 12.1
20 */
21 class FOFDatabaseDriverMysqli extends FOFDatabaseDriver
22 {
23 /**
24 * The name of the database driver.
25 *
26 * @var string
27 * @since 12.1
28 */
29 public $name = 'mysqli';
30
31 /**
32 * The type of the database server family supported by this driver.
33 *
34 * @var string
35 * @since CMS 3.5.0
36 */
37 public $serverType = 'mysql';
38
39 /**
40 * @var mysqli The database connection resource.
41 * @since 11.1
42 */
43 protected $connection;
44
45 /**
46 * The character(s) used to quote SQL statement names such as table names or field names,
47 * etc. The child classes should define this as necessary. If a single character string the
48 * same character is used for both sides of the quoted name, else the first character will be
49 * used for the opening quote and the second for the closing quote.
50 *
51 * @var string
52 * @since 12.2
53 */
54 protected $nameQuote = '`';
55
56 /**
57 * The null or zero representation of a timestamp for the database driver. This should be
58 * defined in child classes to hold the appropriate value for the engine.
59 *
60 * @var string
61 * @since 12.2
62 */
63 protected $nullDate = '0000-00-00 00:00:00';
64
65 /**
66 * @var string The minimum supported database version.
67 * @since 12.2
68 */
69 protected static $dbMinimum = '5.0.4';
70
71 /**
72 * Constructor.
73 *
74 * @param array $options List of options used to configure the connection
75 *
76 * @since 12.1
77 */
78 public function __construct($options)
79 {
80 // Get some basic values from the options.
81 $options['host'] = (isset($options['host'])) ? $options['host'] : 'localhost';
82 $options['user'] = (isset($options['user'])) ? $options['user'] : 'root';
83 $options['password'] = (isset($options['password'])) ? $options['password'] : '';
84 $options['database'] = (isset($options['database'])) ? $options['database'] : '';
85 $options['select'] = (isset($options['select'])) ? (bool) $options['select'] : true;
86 $options['port'] = null;
87 $options['socket'] = null;
88
89 // Finalize initialisation.
90 parent::__construct($options);
91 }
92
93 /**
94 * Destructor.
95 *
96 * @since 12.1
97 */
98 public function __destruct()
99 {
100 $this->disconnect();
101 }
102
103 /**
104 * Connects to the database if needed.
105 *
106 * @return void Returns void if the database connected successfully.
107 *
108 * @since 12.1
109 * @throws RuntimeException
110 */
111 public function connect()
112 {
113 if ($this->connection)
114 {
115 return;
116 }
117
118 /*
119 * Unlike mysql_connect(), mysqli_connect() takes the port and socket as separate arguments. Therefore, we
120 * have to extract them from the host string.
121 */
122 $port = isset($this->options['port']) ? $this->options['port'] : 3306;
123 $regex = '/^(?P<host>((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(:(?P<port>.+))?$/';
124
125 if (preg_match($regex, $this->options['host'], $matches))
126 {
127 // It's an IPv4 address with ot without port
128 $this->options['host'] = $matches['host'];
129
130 if (!empty($matches['port']))
131 {
132 $port = $matches['port'];
133 }
134 }
135 elseif (preg_match('/^(?P<host>\[.*\])(:(?P<port>.+))?$/', $this->options['host'], $matches))
136 {
137 // We assume square-bracketed IPv6 address with or without port, e.g. [fe80:102::2%eth1]:3306
138 $this->options['host'] = $matches['host'];
139
140 if (!empty($matches['port']))
141 {
142 $port = $matches['port'];
143 }
144 }
145 elseif (preg_match('/^(?P<host>(\w+:\/{2,3})?[a-z0-9\.\-]+)(:(?P<port>[^:]+))?$/i', $this->options['host'], $matches))
146 {
147 // Named host (e.g domain.com or localhost) with ot without port
148 $this->options['host'] = $matches['host'];
149
150 if (!empty($matches['port']))
151 {
152 $port = $matches['port'];
153 }
154 }
155 elseif (preg_match('/^:(?P<port>[^:]+)$/', $this->options['host'], $matches))
156 {
157 // Empty host, just port, e.g. ':3306'
158 $this->options['host'] = 'localhost';
159 $port = $matches['port'];
160 }
161 // ... else we assume normal (naked) IPv6 address, so host and port stay as they are or default
162
163 // Get the port number or socket name
164 if (is_numeric($port))
165 {
166 $this->options['port'] = (int) $port;
167 }
168 else
169 {
170 $this->options['socket'] = $port;
171 }
172
173 // Make sure the MySQLi extension for PHP is installed and enabled.
174 if (!function_exists('mysqli_connect'))
175 {
176 throw new RuntimeException('The MySQL adapter mysqli is not available');
177 }
178
179 $this->connection = @mysqli_connect(
180 $this->options['host'], $this->options['user'], $this->options['password'], null, $this->options['port'], $this->options['socket']
181 );
182
183 // Attempt to connect to the server.
184 if (!$this->connection)
185 {
186 throw new RuntimeException('Could not connect to MySQL.');
187 }
188
189 // Set sql_mode to non_strict mode
190 mysqli_query($this->connection, "SET @@SESSION.sql_mode = '';");
191
192 // If auto-select is enabled select the given database.
193 if ($this->options['select'] && !empty($this->options['database']))
194 {
195 $this->select($this->options['database']);
196 }
197
198 // Pre-populate the UTF-8 Multibyte compatibility flag based on server version
199 $this->utf8mb4 = $this->serverClaimsUtf8mb4Support();
200
201 // Set the character set (needed for MySQL 4.1.2+).
202 $this->utf = $this->setUtf();
203
204 // Turn MySQL profiling ON in debug mode:
205 if ($this->debug && $this->hasProfiling())
206 {
207 mysqli_query($this->connection, "SET profiling_history_size = 100;");
208 mysqli_query($this->connection, "SET profiling = 1;");
209 }
210 }
211
212 /**
213 * Disconnects the database.
214 *
215 * @return void
216 *
217 * @since 12.1
218 */
219 public function disconnect()
220 {
221 // Close the connection.
222 if ($this->connection)
223 {
224 foreach ($this->disconnectHandlers as $h)
225 {
226 call_user_func_array($h, array( &$this));
227 }
228
229 mysqli_close($this->connection);
230 }
231
232 $this->connection = null;
233 }
234
235 /**
236 * Method to escape a string for usage in an SQL statement.
237 *
238 * @param string $text The string to be escaped.
239 * @param boolean $extra Optional parameter to provide extra escaping.
240 *
241 * @return string The escaped string.
242 *
243 * @since 12.1
244 */
245 public function escape($text, $extra = false)
246 {
247 $this->connect();
248
249 $result = mysqli_real_escape_string($this->getConnection(), $text);
250
251 if ($extra)
252 {
253 $result = addcslashes($result, '%_');
254 }
255
256 return $result;
257 }
258
259 /**
260 * Test to see if the MySQL connector is available.
261 *
262 * @return boolean True on success, false otherwise.
263 *
264 * @since 12.1
265 */
266 public static function isSupported()
267 {
268 return (function_exists('mysqli_connect'));
269 }
270
271 /**
272 * Determines if the connection to the server is active.
273 *
274 * @return boolean True if connected to the database engine.
275 *
276 * @since 12.1
277 */
278 public function connected()
279 {
280 if (is_object($this->connection))
281 {
282 return mysqli_ping($this->connection);
283 }
284
285 return false;
286 }
287
288 /**
289 * Drops a table from the database.
290 *
291 * @param string $tableName The name of the database table to drop.
292 * @param boolean $ifExists Optionally specify that the table must exist before it is dropped.
293 *
294 * @return FOFDatabaseDriverMysqli Returns this object to support chaining.
295 *
296 * @since 12.2
297 * @throws RuntimeException
298 */
299 public function dropTable($tableName, $ifExists = true)
300 {
301 $this->connect();
302
303 $query = $this->getQuery(true);
304
305 $this->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $query->quoteName($tableName));
306
307 $this->execute();
308
309 return $this;
310 }
311
312 /**
313 * Get the number of affected rows by the last INSERT, UPDATE, REPLACE or DELETE for the previous executed SQL statement.
314 *
315 * @return integer The number of affected rows.
316 *
317 * @since 12.1
318 */
319 public function getAffectedRows()
320 {
321 $this->connect();
322
323 return mysqli_affected_rows($this->connection);
324 }
325
326 /**
327 * Method to get the database collation.
328 *
329 * @return mixed The collation in use by the database (string) or boolean false if not supported.
330 *
331 * @since 12.2
332 * @throws RuntimeException
333 */
334 public function getCollation()
335 {
336 $this->connect();
337
338 // Attempt to get the database collation by accessing the server system variable.
339 $this->setQuery('SHOW VARIABLES LIKE "collation_database"');
340 $result = $this->loadObject();
341
342 if (property_exists($result, 'Value'))
343 {
344 return $result->Value;
345 }
346 else
347 {
348 return false;
349 }
350 }
351
352 /**
353 * Method to get the database connection collation, as reported by the driver. If the connector doesn't support
354 * reporting this value please return an empty string.
355 *
356 * @return string
357 */
358 public function getConnectionCollation()
359 {
360 $this->connect();
361
362 // Attempt to get the database collation by accessing the server system variable.
363 $this->setQuery('SHOW VARIABLES LIKE "collation_connection"');
364 $result = $this->loadObject();
365
366 if (property_exists($result, 'Value'))
367 {
368 return $result->Value;
369 }
370 else
371 {
372 return false;
373 }
374 }
375
376 /**
377 * Get the number of returned rows for the previous executed SQL statement.
378 * This command is only valid for statements like SELECT or SHOW that return an actual result set.
379 * To retrieve the number of rows affected by a INSERT, UPDATE, REPLACE or DELETE query, use getAffectedRows().
380 *
381 * @param resource $cursor An optional database cursor resource to extract the row count from.
382 *
383 * @return integer The number of returned rows.
384 *
385 * @since 12.1
386 */
387 public function getNumRows($cursor = null)
388 {
389 return mysqli_num_rows($cursor ? $cursor : $this->cursor);
390 }
391
392 /**
393 * Shows the table CREATE statement that creates the given tables.
394 *
395 * @param mixed $tables A table name or a list of table names.
396 *
397 * @return array A list of the create SQL for the tables.
398 *
399 * @since 12.1
400 * @throws RuntimeException
401 */
402 public function getTableCreate($tables)
403 {
404 $this->connect();
405
406 $result = array();
407
408 // Sanitize input to an array and iterate over the list.
409 settype($tables, 'array');
410
411 foreach ($tables as $table)
412 {
413 // Set the query to get the table CREATE statement.
414 $this->setQuery('SHOW CREATE table ' . $this->quoteName($this->escape($table)));
415 $row = $this->loadRow();
416
417 // Populate the result array based on the create statements.
418 $result[$table] = $row[1];
419 }
420
421 return $result;
422 }
423
424 /**
425 * Retrieves field information about a given table.
426 *
427 * @param string $table The name of the database table.
428 * @param boolean $typeOnly True to only return field types.
429 *
430 * @return array An array of fields for the database table.
431 *
432 * @since 12.2
433 * @throws RuntimeException
434 */
435 public function getTableColumns($table, $typeOnly = true)
436 {
437 $this->connect();
438
439 $result = array();
440
441 // Set the query to get the table fields statement.
442 $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($this->escape($table)));
443 $fields = $this->loadObjectList();
444
445 // If we only want the type as the value add just that to the list.
446 if ($typeOnly)
447 {
448 foreach ($fields as $field)
449 {
450 $result[$field->Field] = preg_replace("/[(0-9)]/", '', $field->Type);
451 }
452 }
453 // If we want the whole field data object add that to the list.
454 else
455 {
456 foreach ($fields as $field)
457 {
458 $result[$field->Field] = $field;
459 }
460 }
461
462 return $result;
463 }
464
465 /**
466 * Get the details list of keys for a table.
467 *
468 * @param string $table The name of the table.
469 *
470 * @return array An array of the column specification for the table.
471 *
472 * @since 12.2
473 * @throws RuntimeException
474 */
475 public function getTableKeys($table)
476 {
477 $this->connect();
478
479 // Get the details columns information.
480 $this->setQuery('SHOW KEYS FROM ' . $this->quoteName($table));
481 $keys = $this->loadObjectList();
482
483 return $keys;
484 }
485
486 /**
487 * Method to get an array of all tables in the database.
488 *
489 * @return array An array of all the tables in the database.
490 *
491 * @since 12.2
492 * @throws RuntimeException
493 */
494 public function getTableList()
495 {
496 $this->connect();
497
498 // Set the query to get the tables statement.
499 $this->setQuery('SHOW TABLES');
500 $tables = $this->loadColumn();
501
502 return $tables;
503 }
504
505 /**
506 * Get the version of the database connector.
507 *
508 * @return string The database connector version.
509 *
510 * @since 12.1
511 */
512 public function getVersion()
513 {
514 $this->connect();
515
516 return mysqli_get_server_info($this->connection);
517 }
518
519 /**
520 * Method to get the auto-incremented value from the last INSERT statement.
521 *
522 * @return mixed The value of the auto-increment field from the last inserted row.
523 * If the value is greater than maximal int value, it will return a string.
524 *
525 * @since 12.1
526 */
527 public function insertid()
528 {
529 $this->connect();
530
531 return mysqli_insert_id($this->connection);
532 }
533
534 /**
535 * Locks a table in the database.
536 *
537 * @param string $table The name of the table to unlock.
538 *
539 * @return FOFDatabaseDriverMysqli Returns this object to support chaining.
540 *
541 * @since 12.2
542 * @throws RuntimeException
543 */
544 public function lockTable($table)
545 {
546 $this->setQuery('LOCK TABLES ' . $this->quoteName($table) . ' WRITE')->execute();
547
548 return $this;
549 }
550
551 /**
552 * Execute the SQL statement.
553 *
554 * @return mixed A database cursor resource on success, boolean false on failure.
555 *
556 * @since 12.1
557 * @throws RuntimeException
558 */
559 public function execute()
560 {
561 $this->connect();
562
563 if (!is_object($this->connection))
564 {
565 if (class_exists('JLog'))
566 {
567 JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database');
568 }
569 throw new RuntimeException($this->errorMsg, $this->errorNum);
570 }
571
572 // Take a local copy so that we don't modify the original query and cause issues later
573 $query = $this->replacePrefix((string) $this->sql);
574
575 if (!($this->sql instanceof FOFDatabaseQuery) && ($this->limit > 0 || $this->offset > 0))
576 {
577 $query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
578 }
579
580 // Increment the query counter.
581 $this->count++;
582
583 // Reset the error values.
584 $this->errorNum = 0;
585 $this->errorMsg = '';
586 $memoryBefore = null;
587
588 // If debugging is enabled then let's log the query.
589 if ($this->debug)
590 {
591 // Add the query to the object queue.
592 $this->log[] = $query;
593
594 if (class_exists('JLog'))
595 {
596 JLog::add($query, JLog::DEBUG, 'databasequery');
597 }
598
599 $this->timings[] = microtime(true);
600
601 if (is_object($this->cursor))
602 {
603 // Avoid warning if result already freed by third-party library
604 @$this->freeResult();
605 }
606
607 $memoryBefore = memory_get_usage();
608 }
609
610 // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
611 $this->cursor = @mysqli_query($this->connection, $query);
612
613 if ($this->debug)
614 {
615 $this->timings[] = microtime(true);
616
617 if (defined('DEBUG_BACKTRACE_IGNORE_ARGS'))
618 {
619 $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
620 }
621 else
622 {
623 $this->callStacks[] = debug_backtrace();
624 }
625
626 $this->callStacks[count($this->callStacks) - 1][0]['memory'] = array(
627 $memoryBefore,
628 memory_get_usage(),
629 is_object($this->cursor) ? $this->getNumRows() : null
630 );
631 }
632
633 // If an error occurred handle it.
634 if (!$this->cursor)
635 {
636 // Get the error number and message before we execute any more queries.
637 $this->errorNum = $this->getErrorNumber();
638 $this->errorMsg = $this->getErrorMessage($query);
639
640 // Check if the server was disconnected.
641 if (!$this->connected())
642 {
643 try
644 {
645 // Attempt to reconnect.
646 $this->connection = null;
647 $this->connect();
648 }
649 // If connect fails, ignore that exception and throw the normal exception.
650 catch (RuntimeException $e)
651 {
652 // Get the error number and message.
653 $this->errorNum = $this->getErrorNumber();
654 $this->errorMsg = $this->getErrorMessage($query);
655
656 if (class_exists('JLog'))
657 {
658 JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error');
659 }
660
661 throw new RuntimeException($this->errorMsg, $this->errorNum, $e);
662 }
663
664 // Since we were able to reconnect, run the query again.
665 return $this->execute();
666 }
667 // The server was not disconnected.
668 else
669 {
670 if (class_exists('JLog'))
671 {
672 JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error');
673 }
674
675 throw new RuntimeException($this->errorMsg, $this->errorNum);
676 }
677 }
678
679 return $this->cursor;
680 }
681
682 /**
683 * Renames a table in the database.
684 *
685 * @param string $oldTable The name of the table to be renamed
686 * @param string $newTable The new name for the table.
687 * @param string $backup Not used by MySQL.
688 * @param string $prefix Not used by MySQL.
689 *
690 * @return FOFDatabaseDriverMysqli Returns this object to support chaining.
691 *
692 * @since 12.2
693 * @throws RuntimeException
694 */
695 public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
696 {
697 $this->setQuery('RENAME TABLE ' . $oldTable . ' TO ' . $newTable)->execute();
698
699 return $this;
700 }
701
702 /**
703 * Select a database for use.
704 *
705 * @param string $database The name of the database to select for use.
706 *
707 * @return boolean True if the database was successfully selected.
708 *
709 * @since 12.1
710 * @throws RuntimeException
711 */
712 public function select($database)
713 {
714 $this->connect();
715
716 if (!$database)
717 {
718 return false;
719 }
720
721 if (!mysqli_select_db($this->connection, $database))
722 {
723 throw new RuntimeException('Could not connect to database.');
724 }
725
726 return true;
727 }
728
729 /**
730 * Set the connection to use UTF-8 character encoding.
731 *
732 * @return boolean True on success.
733 *
734 * @since 12.1
735 */
736 public function setUtf()
737 {
738 // If UTF is not supported return false immediately
739 if (!$this->utf)
740 {
741 return false;
742 }
743
744 // Make sure we're connected to the server
745 $this->connect();
746
747 // Which charset should I use, plain utf8 or multibyte utf8mb4?
748 $charset = $this->utf8mb4 ? 'utf8mb4' : 'utf8';
749
750 $result = @$this->connection->set_charset($charset);
751
752 /**
753 * If I could not set the utf8mb4 charset then the server doesn't support utf8mb4 despite claiming otherwise.
754 * This happens on old MySQL server versions (less than 5.5.3) using the mysqlnd PHP driver. Since mysqlnd
755 * masks the server version and reports only its own we can not be sure if the server actually does support
756 * UTF-8 Multibyte (i.e. it's MySQL 5.5.3 or later). Since the utf8mb4 charset is undefined in this case we
757 * catch the error and determine that utf8mb4 is not supported!
758 */
759 if (!$result && $this->utf8mb4)
760 {
761 $this->utf8mb4 = false;
762 $result = @$this->connection->set_charset('utf8');
763 }
764
765 return $result;
766 }
767
768 /**
769 * Method to commit a transaction.
770 *
771 * @param boolean $toSavepoint If true, commit to the last savepoint.
772 *
773 * @return void
774 *
775 * @since 12.2
776 * @throws RuntimeException
777 */
778 public function transactionCommit($toSavepoint = false)
779 {
780 $this->connect();
781
782 if (!$toSavepoint || $this->transactionDepth <= 1)
783 {
784 if ($this->setQuery('COMMIT')->execute())
785 {
786 $this->transactionDepth = 0;
787 }
788
789 return;
790 }
791
792 $this->transactionDepth--;
793 }
794
795 /**
796 * Method to roll back a transaction.
797 *
798 * @param boolean $toSavepoint If true, rollback to the last savepoint.
799 *
800 * @return void
801 *
802 * @since 12.2
803 * @throws RuntimeException
804 */
805 public function transactionRollback($toSavepoint = false)
806 {
807 $this->connect();
808
809 if (!$toSavepoint || $this->transactionDepth <= 1)
810 {
811 if ($this->setQuery('ROLLBACK')->execute())
812 {
813 $this->transactionDepth = 0;
814 }
815
816 return;
817 }
818
819 $savepoint = 'SP_' . ($this->transactionDepth - 1);
820 $this->setQuery('ROLLBACK TO SAVEPOINT ' . $this->quoteName($savepoint));
821
822 if ($this->execute())
823 {
824 $this->transactionDepth--;
825 }
826 }
827
828 /**
829 * Method to initialize a transaction.
830 *
831 * @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created.
832 *
833 * @return void
834 *
835 * @since 12.2
836 * @throws RuntimeException
837 */
838 public function transactionStart($asSavepoint = false)
839 {
840 $this->connect();
841
842 if (!$asSavepoint || !$this->transactionDepth)
843 {
844 if ($this->setQuery('START TRANSACTION')->execute())
845 {
846 $this->transactionDepth = 1;
847 }
848
849 return;
850 }
851
852 $savepoint = 'SP_' . $this->transactionDepth;
853 $this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint));
854
855 if ($this->execute())
856 {
857 $this->transactionDepth++;
858 }
859 }
860
861 /**
862 * Method to fetch a row from the result set cursor as an array.
863 *
864 * @param mixed $cursor The optional result set cursor from which to fetch the row.
865 *
866 * @return mixed Either the next row from the result set or false if there are no more rows.
867 *
868 * @since 12.1
869 */
870 protected function fetchArray($cursor = null)
871 {
872 return mysqli_fetch_row($cursor ? $cursor : $this->cursor);
873 }
874
875 /**
876 * Method to fetch a row from the result set cursor as an associative array.
877 *
878 * @param mixed $cursor The optional result set cursor from which to fetch the row.
879 *
880 * @return mixed Either the next row from the result set or false if there are no more rows.
881 *
882 * @since 12.1
883 */
884 protected function fetchAssoc($cursor = null)
885 {
886 return mysqli_fetch_assoc($cursor ? $cursor : $this->cursor);
887 }
888
889 /**
890 * Method to fetch a row from the result set cursor as an object.
891 *
892 * @param mixed $cursor The optional result set cursor from which to fetch the row.
893 * @param string $class The class name to use for the returned row object.
894 *
895 * @return mixed Either the next row from the result set or false if there are no more rows.
896 *
897 * @since 12.1
898 */
899 protected function fetchObject($cursor = null, $class = 'stdClass')
900 {
901 return mysqli_fetch_object($cursor ? $cursor : $this->cursor, $class);
902 }
903
904 /**
905 * Method to free up the memory used for the result set.
906 *
907 * @param mixed $cursor The optional result set cursor from which to fetch the row.
908 *
909 * @return void
910 *
911 * @since 12.1
912 */
913 protected function freeResult($cursor = null)
914 {
915 mysqli_free_result($cursor ? $cursor : $this->cursor);
916
917 if ((! $cursor) || ($cursor === $this->cursor))
918 {
919 $this->cursor = null;
920 }
921 }
922
923 /**
924 * Unlocks tables in the database.
925 *
926 * @return FOFDatabaseDriverMysqli Returns this object to support chaining.
927 *
928 * @since 12.1
929 * @throws RuntimeException
930 */
931 public function unlockTables()
932 {
933 $this->setQuery('UNLOCK TABLES')->execute();
934
935 return $this;
936 }
937
938 /**
939 * Internal function to check if profiling is available
940 *
941 * @return boolean
942 *
943 * @since 3.1.3
944 */
945 private function hasProfiling()
946 {
947 try
948 {
949 $res = mysqli_query($this->connection, "SHOW VARIABLES LIKE 'have_profiling'");
950 $row = mysqli_fetch_assoc($res);
951
952 return isset($row);
953 }
954 catch (Exception $e)
955 {
956 return false;
957 }
958 }
959
960 /**
961 * Does the database server claim to have support for UTF-8 Multibyte (utf8mb4) collation?
962 *
963 * libmysql supports utf8mb4 since 5.5.3 (same version as the MySQL server). mysqlnd supports utf8mb4 since 5.0.9.
964 *
965 * @return boolean
966 *
967 * @since CMS 3.5.0
968 */
969 private function serverClaimsUtf8mb4Support()
970 {
971 $client_version = mysqli_get_client_info();
972
973 if (strpos($client_version, 'mysqlnd') !== false)
974 {
975 $client_version = preg_replace('/^\D+([\d.]+).*/', '$1', $client_version);
976
977 return version_compare($client_version, '5.0.9', '>=');
978 }
979 else
980 {
981 return version_compare($client_version, '5.5.3', '>=');
982 }
983 }
984
985 /**
986 * Return the actual SQL Error number
987 *
988 * @return integer The SQL Error number
989 *
990 * @since 3.4.6
991 */
992 protected function getErrorNumber()
993 {
994 return (int) mysqli_errno($this->connection);
995 }
996
997 /**
998 * Return the actual SQL Error message
999 *
1000 * @param string $query The SQL Query that fails
1001 *
1002 * @return string The SQL Error message
1003 *
1004 * @since 3.4.6
1005 */
1006 protected function getErrorMessage($query)
1007 {
1008 $errorMessage = (string) mysqli_error($this->connection);
1009
1010 // Replace the Databaseprefix with `#__` if we are not in Debug
1011 if (!$this->debug)
1012 {
1013 $errorMessage = str_replace($this->tablePrefix, '#__', $errorMessage);
1014 $query = str_replace($this->tablePrefix, '#__', $query);
1015 }
1016
1017 return $errorMessage . ' SQL=' . $query;
1018 }
1019 }
1020