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 class FOFDatabaseInstaller
13 {
14 /** @var FOFDatabase The database connector object */
15 private $db = null;
16
17 /**
18 * @var FOFInput Input variables
19 */
20 protected $input = array();
21
22 /** @var string The directory where the XML schema files are stored */
23 private $xmlDirectory = null;
24
25 /** @var array A list of the base names of the XML schema files */
26 public $xmlFiles = array('mysql', 'mysqli', 'pdomysql', 'postgresql', 'sqlsrv', 'mssql');
27
28 /** @var array Internal cache for table list */
29 protected static $allTables = array();
30
31 /**
32 * Public constructor
33 *
34 * @param array $config The configuration array
35 */
36 public function __construct($config = array())
37 {
38 // Make sure $config is an array
39 if (is_object($config))
40 {
41 $config = (array) $config;
42 }
43 elseif (!is_array($config))
44 {
45 $config = array();
46 }
47
48 // Get the input
49 if (array_key_exists('input', $config))
50 {
51 if ($config['input'] instanceof FOFInput)
52 {
53 $this->input = $config['input'];
54 }
55 else
56 {
57 $this->input = new FOFInput($config['input']);
58 }
59 }
60 else
61 {
62 $this->input = new FOFInput;
63 }
64
65 // Set the database object
66 if (array_key_exists('dbo', $config))
67 {
68 $this->db = $config['dbo'];
69 }
70 else
71 {
72 $this->db = FOFPlatform::getInstance()->getDbo();
73 }
74
75 // Set the $name/$_name variable
76 $component = $this->input->getCmd('option', 'com_foobar');
77
78 if (array_key_exists('option', $config))
79 {
80 $component = $config['option'];
81 }
82
83 // Figure out where the XML schema files are stored
84 if (array_key_exists('dbinstaller_directory', $config))
85 {
86 $this->xmlDirectory = $config['dbinstaller_directory'];
87 }
88 else
89 {
90 // Nothing is defined, assume the files are stored in the sql/xml directory inside the component's administrator section
91 $directories = FOFPlatform::getInstance()->getComponentBaseDirs($component);
92 $this->setXmlDirectory($directories['admin'] . '/sql/xml');
93 }
94
95 // Do we have a set of XML files to look for?
96 if (array_key_exists('dbinstaller_files', $config))
97 {
98 $files = $config['dbinstaller_files'];
99
100 if (!is_array($files))
101 {
102 $files = explode(',', $files);
103 }
104
105 $this->xmlFiles = $files;
106 }
107
108 }
109
110 /**
111 * Sets the directory where XML schema files are stored
112 *
113 * @param string $xmlDirectory
114 */
115 public function setXmlDirectory($xmlDirectory)
116 {
117 $this->xmlDirectory = $xmlDirectory;
118 }
119
120 /**
121 * Returns the directory where XML schema files are stored
122 *
123 * @return string
124 */
125 public function getXmlDirectory()
126 {
127 return $this->xmlDirectory;
128 }
129
130 /**
131 * Creates or updates the database schema
132 *
133 * @return void
134 *
135 * @throws Exception When a database query fails and it doesn't have the canfail flag
136 */
137 public function updateSchema()
138 {
139 // Get the schema XML file
140 $xml = $this->findSchemaXml();
141
142 if (empty($xml))
143 {
144 return;
145 }
146
147 // Make sure there are SQL commands in this file
148 if (!$xml->sql)
149 {
150 return;
151 }
152
153 // Walk the sql > action tags to find all tables
154 /** @var SimpleXMLElement $actions */
155 $actions = $xml->sql->children();
156
157 /**
158 * The meta/autocollation node defines if I should automatically apply the correct collation (utf8 or utf8mb4)
159 * to the database tables managed by the schema updater. When enabled (default) the queries are automatically
160 * converted to the correct collation (utf8mb4_unicode_ci or utf8_general_ci) depending on whether your Joomla!
161 * and MySQL server support Multibyte UTF-8 (UTF8MB4). Moreover, if UTF8MB4 is supported, all CREATE TABLE
162 * queries are analyzed and the tables referenced in them are auto-converted to the proper utf8mb4 collation.
163 */
164 $autoCollationConversion = true;
165
166 if ($xml->meta->autocollation)
167 {
168 $value = (string) $xml->meta->autocollation;
169 $value = trim($value);
170 $value = strtolower($value);
171
172 $autoCollationConversion = in_array($value, array('true', '1', 'on', 'yes'));
173 }
174
175 try
176 {
177 $hasUtf8mb4Support = $this->db->hasUTF8mb4Support();
178 }
179 catch (\Exception $e)
180 {
181 $hasUtf8mb4Support = false;
182 }
183
184 $tablesToConvert = array();
185
186 /** @var SimpleXMLElement $action */
187 foreach ($actions as $action)
188 {
189 // Get the attributes
190 $attributes = $action->attributes();
191
192 // Get the table / view name
193 $table = $attributes->table ? (string)$attributes->table : '';
194
195 if (empty($table))
196 {
197 continue;
198 }
199
200 // Am I allowed to let this action fail?
201 $canFailAction = $attributes->canfail ? $attributes->canfail : 0;
202
203 // Evaluate conditions
204 $shouldExecute = true;
205
206 /** @var SimpleXMLElement $node */
207 foreach ($action->children() as $node)
208 {
209 if ($node->getName() == 'condition')
210 {
211 // Get the operator
212 $operator = $node->attributes()->operator ? (string)$node->attributes()->operator : 'and';
213 $operator = empty($operator) ? 'and' : $operator;
214
215 $condition = $this->conditionMet($table, $node);
216
217 switch ($operator)
218 {
219 case 'not':
220 $shouldExecute = $shouldExecute && !$condition;
221 break;
222
223 case 'or':
224 $shouldExecute = $shouldExecute || $condition;
225 break;
226
227 case 'nor':
228 $shouldExecute = !$shouldExecute && !$condition;
229 break;
230
231 case 'xor':
232 $shouldExecute = ($shouldExecute xor $condition);
233 break;
234
235 case 'maybe':
236 $shouldExecute = $condition ? true : $shouldExecute;
237 break;
238
239 default:
240 $shouldExecute = $shouldExecute && $condition;
241 break;
242 }
243 }
244
245 // DO NOT USE BOOLEAN SHORT CIRCUIT EVALUATION!
246 // if (!$shouldExecute) break;
247 }
248
249 // Do I have to only collect the tables from CREATE TABLE queries?
250 $onlyCollectTables = !$shouldExecute && $autoCollationConversion && $hasUtf8mb4Support;
251
252 // Make sure all conditions are met OR I have to collect tables from CREATE TABLE queries.
253 if (!$shouldExecute && !$onlyCollectTables)
254 {
255 continue;
256 }
257
258 // Execute queries
259 foreach ($action->children() as $node)
260 {
261 if ($node->getName() == 'query')
262 {
263 $query = (string) $node;
264
265 if ($autoCollationConversion && $hasUtf8mb4Support)
266 {
267 $this->extractTablesToConvert($query, $tablesToConvert);
268 }
269
270 // If we're only collecting tables do not run the queries
271 if ($onlyCollectTables)
272 {
273 continue;
274 }
275
276 $canFail = $node->attributes->canfail ? (string)$node->attributes->canfail : $canFailAction;
277
278 if (is_string($canFail))
279 {
280 $canFail = strtoupper($canFail);
281 }
282
283 $canFail = (in_array($canFail, array(true, 1, 'YES', 'TRUE')));
284
285 // Do I need to automatically convert the collation of all CREATE / ALTER queries?
286 if ($autoCollationConversion)
287 {
288 if ($hasUtf8mb4Support)
289 {
290 // We have UTF8MB4 support. Convert all queries to UTF8MB4.
291 $query = $this->convertUtf8QueryToUtf8mb4($query);
292 }
293 else
294 {
295 // We do not have UTF8MB4 support. Convert all queries to plain old UTF8.
296 $query = $this->convertUtf8mb4QueryToUtf8($query);
297 }
298 }
299
300 $this->db->setQuery($query);
301
302 try
303 {
304 $this->db->execute();
305 }
306 catch (Exception $e)
307 {
308 // If we are not allowed to fail, throw back the exception we caught
309 if (!$canFail)
310 {
311 throw $e;
312 }
313 }
314 }
315 }
316 }
317
318 // Auto-convert the collation of tables if we are told to do so, have utf8mb4 support and a list of tables.
319 if ($autoCollationConversion && $hasUtf8mb4Support && !empty($tablesToConvert))
320 {
321 $this->convertTablesToUtf8mb4($tablesToConvert);
322 }
323 }
324
325 /**
326 * Uninstalls the database schema
327 *
328 * @return void
329 */
330 public function removeSchema()
331 {
332 // Get the schema XML file
333 $xml = $this->findSchemaXml();
334
335 if (empty($xml))
336 {
337 return;
338 }
339
340 // Make sure there are SQL commands in this file
341 if (!$xml->sql)
342 {
343 return;
344 }
345
346 // Walk the sql > action tags to find all tables
347 $tables = array();
348 /** @var SimpleXMLElement $actions */
349 $actions = $xml->sql->children();
350
351 /** @var SimpleXMLElement $action */
352 foreach ($actions as $action)
353 {
354 $attributes = $action->attributes();
355 $tables[] = (string)$attributes->table;
356 }
357
358 // Simplify the tables list
359 $tables = array_unique($tables);
360
361 // Start dropping tables
362 foreach ($tables as $table)
363 {
364 try
365 {
366 $this->db->dropTable($table);
367 }
368 catch (Exception $e)
369 {
370 // Do not fail if I can't drop the table
371 }
372 }
373 }
374
375 /**
376 * Find an suitable schema XML file for this database type and return the SimpleXMLElement holding its information
377 *
378 * @return null|SimpleXMLElement Null if no suitable schema XML file is found
379 */
380 protected function findSchemaXml()
381 {
382 $driverType = $this->db->name;
383 $xml = null;
384
385 // And now look for the file
386 foreach ($this->xmlFiles as $baseName)
387 {
388 // Remove any accidental whitespace
389 $baseName = trim($baseName);
390
391 // Get the full path to the file
392 $fileName = $this->xmlDirectory . '/' . $baseName . '.xml';
393
394 // Make sure the file exists
395 if (!@file_exists($fileName))
396 {
397 continue;
398 }
399
400 // Make sure the file is a valid XML document
401 try
402 {
403 $xml = new SimpleXMLElement($fileName, LIBXML_NONET, true);
404 }
405 catch (Exception $e)
406 {
407 $xml = null;
408 continue;
409 }
410
411 // Make sure the file is an XML schema file
412 if ($xml->getName() != 'schema')
413 {
414 $xml = null;
415 continue;
416 }
417
418 if (!$xml->meta)
419 {
420 $xml = null;
421 continue;
422 }
423
424 if (!$xml->meta->drivers)
425 {
426 $xml = null;
427 continue;
428 }
429
430 /** @var SimpleXMLElement $drivers */
431 $drivers = $xml->meta->drivers;
432
433 // Strict driver name match
434 foreach ($drivers->children() as $driverTypeTag)
435 {
436 $thisDriverType = (string)$driverTypeTag;
437
438 if ($thisDriverType == $driverType)
439 {
440 return $xml;
441 }
442 }
443
444 // Some custom database drivers use a non-standard $name variable. Let try a relaxed match.
445 foreach ($drivers->children() as $driverTypeTag)
446 {
447 $thisDriverType = (string)$driverTypeTag;
448
449 if (
450 // e.g. $driverType = 'mysqlistupid', $thisDriverType = 'mysqli' => driver matched
451 strpos($driverType, $thisDriverType) === 0
452 // e.g. $driverType = 'stupidmysqli', $thisDriverType = 'mysqli' => driver matched
453 || (substr($driverType, -strlen($thisDriverType)) == $thisDriverType)
454 )
455 {
456 return $xml;
457 }
458 }
459
460 $xml = null;
461 }
462
463 return $xml;
464 }
465
466 /**
467 * Checks if a condition is met
468 *
469 * @param string $table The table we're operating on
470 * @param SimpleXMLElement $node The condition definition node
471 *
472 * @return bool
473 */
474 protected function conditionMet($table, SimpleXMLElement $node)
475 {
476 if (empty(static::$allTables))
477 {
478 static::$allTables = $this->db->getTableList();
479 }
480
481 // Does the table exist?
482 $tableNormal = $this->db->replacePrefix($table);
483 $tableExists = in_array($tableNormal, static::$allTables);
484
485 // Initialise
486 $condition = false;
487
488 // Get the condition's attributes
489 $attributes = $node->attributes();
490 $type = $attributes->type ? $attributes->type : null;
491 $value = $attributes->value ? (string) $attributes->value : null;
492
493 switch ($type)
494 {
495 // Check if a table or column is missing
496 case 'missing':
497 $fieldName = (string)$value;
498
499 if (empty($fieldName))
500 {
501 $condition = !$tableExists;
502 }
503 else
504 {
505 try
506 {
507 $tableColumns = $this->db->getTableColumns($tableNormal, true);
508 }
509 catch (\Exception $e)
510 {
511 $tableColumns = array();
512 }
513
514 $condition = !array_key_exists($fieldName, $tableColumns);
515 }
516
517 break;
518
519 // Check if a column type matches the "coltype" attribute
520 case 'type':
521 try
522 {
523 $tableColumns = $this->db->getTableColumns($tableNormal, false);
524 }
525 catch (\Exception $e)
526 {
527 $tableColumns = array();
528 }
529
530 $condition = false;
531
532 if (array_key_exists($value, $tableColumns))
533 {
534 $coltype = $attributes->coltype ? $attributes->coltype : null;
535
536 if (!empty($coltype))
537 {
538 $coltype = strtolower($coltype);
539 $currentType = strtolower($tableColumns[$value]->Type);
540
541 $condition = ($coltype == $currentType);
542 }
543 }
544
545 break;
546
547 // Check if a (named) index exists on the table. Currently only supported on MySQL.
548 case 'index':
549 $indexName = (string) $value;
550 $condition = true;
551
552 if (!empty($indexName))
553 {
554 $indexName = str_replace('#__', $this->db->getPrefix(), $indexName);
555 $condition = $this->hasIndex($tableNormal, $indexName);
556 }
557
558 break;
559
560 // Check if a table or column needs to be upgraded to utf8mb4
561 case 'utf8mb4upgrade':
562 $condition = false;
563
564 // Check if the driver and the database connection have UTF8MB4 support
565 try
566 {
567 $hasUtf8mb4Support = $this->db->hasUTF8mb4Support();
568 }
569 catch (\Exception $e)
570 {
571 $hasUtf8mb4Support = false;
572 }
573
574 if ($hasUtf8mb4Support)
575 {
576 $fieldName = (string)$value;
577
578 if (empty($fieldName))
579 {
580 $collation = $this->getTableCollation($tableNormal);
581 }
582 else
583 {
584 $collation = $this->getColumnCollation($tableNormal, $fieldName);
585 }
586
587 $parts = explode('_', $collation, 3);
588 $encoding = empty($parts[0]) ? '' : strtolower($parts[0]);
589
590 $condition = $encoding != 'utf8mb4';
591 }
592
593 break;
594
595 // Check if the result of a query matches our expectation
596 case 'equals':
597 $query = (string)$node;
598 $this->db->setQuery($query);
599
600 try
601 {
602 $result = $this->db->loadResult();
603 $condition = ($result == $value);
604 }
605 catch (Exception $e)
606 {
607 return false;
608 }
609
610 break;
611
612 // Always returns true
613 case 'true':
614 return true;
615 break;
616
617 default:
618 return false;
619 break;
620 }
621
622 return $condition;
623 }
624
625 /**
626 * Get the collation of a table. Uses an internal cache for efficiency.
627 *
628 * @param string $tableName The name of the table
629 *
630 * @return string The collation, e.g. "utf8_general_ci"
631 */
632 private function getTableCollation($tableName)
633 {
634 static $cache = array();
635
636 $tableName = $this->db->replacePrefix($tableName);
637
638 if (!isset($cache[$tableName]))
639 {
640 $cache[$tableName] = $this->realGetTableCollation($tableName);
641 }
642
643 return $cache[$tableName];
644 }
645
646 /**
647 * Get the collation of a table. This is the internal method used by getTableCollation.
648 *
649 * @param string $tableName The name of the table
650 *
651 * @return string The collation, e.g. "utf8_general_ci"
652 */
653 private function realGetTableCollation($tableName)
654 {
655 try
656 {
657 $utf8Support = $this->db->hasUTFSupport();
658 }
659 catch (\Exception $e)
660 {
661 $utf8Support = false;
662 }
663
664 try
665 {
666 $utf8mb4Support = $utf8Support && $this->db->hasUTF8mb4Support();
667 }
668 catch (\Exception $e)
669 {
670 $utf8mb4Support = false;
671 }
672
673 $collation = $utf8mb4Support ? 'utf8mb4_unicode_ci' : ($utf8Support ? 'utf_general_ci' : 'latin1_swedish_ci');
674
675 $query = 'SHOW TABLE STATUS LIKE ' . $this->db->q($tableName);
676
677 try
678 {
679 $row = $this->db->setQuery($query)->loadAssoc();
680 }
681 catch (\Exception $e)
682 {
683 return $collation;
684 }
685
686 if (empty($row))
687 {
688 return $collation;
689 }
690
691 if (!isset($row['Collation']))
692 {
693 return $collation;
694 }
695
696 if (empty($row['Collation']))
697 {
698 return $collation;
699 }
700
701 return $row['Collation'];
702 }
703
704 /**
705 * Get the collation of a column. Uses an internal cache for efficiency.
706 *
707 * @param string $tableName The name of the table
708 * @param string $columnName The name of the column
709 *
710 * @return string The collation, e.g. "utf8_general_ci"
711 */
712 private function getColumnCollation($tableName, $columnName)
713 {
714 static $cache = array();
715
716 $tableName = $this->db->replacePrefix($tableName);
717 $columnName = $this->db->replacePrefix($columnName);
718
719 if (!isset($cache[$tableName]))
720 {
721 $cache[$tableName] = array();
722 }
723
724 if (!isset($cache[$tableName][$columnName]))
725 {
726 $cache[$tableName][$columnName] = $this->realGetColumnCollation($tableName, $columnName);
727 }
728
729 return $cache[$tableName][$columnName];
730 }
731
732 /**
733 * Get the collation of a column. This is the internal method used by getColumnCollation.
734 *
735 * @param string $tableName The name of the table
736 * @param string $columnName The name of the column
737 *
738 * @return string The collation, e.g. "utf8_general_ci"
739 */
740 private function realGetColumnCollation($tableName, $columnName)
741 {
742 $collation = $this->getTableCollation($tableName);
743
744 $query = 'SHOW FULL COLUMNS FROM ' . $this->db->qn($tableName) . ' LIKE ' . $this->db->q($columnName);
745
746 try
747 {
748 $row = $this->db->setQuery($query)->loadAssoc();
749 }
750 catch (\Exception $e)
751 {
752 return $collation;
753 }
754
755 if (empty($row))
756 {
757 return $collation;
758 }
759
760 if (!isset($row['Collation']))
761 {
762 return $collation;
763 }
764
765 if (empty($row['Collation']))
766 {
767 return $collation;
768 }
769
770 return $row['Collation'];
771 }
772
773 /**
774 * Automatically downgrade a CREATE TABLE or ALTER TABLE query from utf8mb4 (UTF-8 Multibyte) to plain utf8.
775 *
776 * We use our own method so we can be site it works even on Joomla! 3.4 or earlier, where UTF8MB4 support is not
777 * implemented.
778 *
779 * @param string $query The query to convert
780 *
781 * @return string The converted query
782 */
783 private function convertUtf8mb4QueryToUtf8($query)
784 {
785 // If it's not an ALTER TABLE or CREATE TABLE command there's nothing to convert
786 $beginningOfQuery = substr($query, 0, 12);
787 $beginningOfQuery = strtoupper($beginningOfQuery);
788
789 if (!in_array($beginningOfQuery, array('ALTER TABLE ', 'CREATE TABLE')))
790 {
791 return $query;
792 }
793
794 // Replace utf8mb4 with utf8
795 $from = array(
796 'utf8mb4_unicode_ci',
797 'utf8mb4_',
798 'utf8mb4',
799 );
800
801 $to = array(
802 'utf8_general_ci', // Yeah, we convert utf8mb4_unicode_ci to utf8_general_ci per Joomla!'s conventions
803 'utf8_',
804 'utf8',
805 );
806
807 return str_replace($from, $to, $query);
808 }
809
810 /**
811 * Automatically upgrade a CREATE TABLE or ALTER TABLE query from plain utf8 to utf8mb4 (UTF-8 Multibyte).
812 *
813 * @param string $query The query to convert
814 *
815 * @return string The converted query
816 */
817 private function convertUtf8QueryToUtf8mb4($query)
818 {
819 // If it's not an ALTER TABLE or CREATE TABLE command there's nothing to convert
820 $beginningOfQuery = substr($query, 0, 12);
821 $beginningOfQuery = strtoupper($beginningOfQuery);
822
823 if (!in_array($beginningOfQuery, array('ALTER TABLE ', 'CREATE TABLE')))
824 {
825 return $query;
826 }
827
828 // Replace utf8 with utf8mb4
829 $from = array(
830 'utf8_general_ci',
831 'utf8_',
832 'utf8',
833 );
834
835 $to = array(
836 'utf8mb4_unicode_ci', // Yeah, we convert utf8_general_ci to utf8mb4_unicode_ci per Joomla!'s conventions
837 'utf8mb4_',
838 'utf8mb4',
839 );
840
841 return str_replace($from, $to, $query);
842 }
843
844 /**
845 * Analyzes a query. If it's a CREATE TABLE query the table is added to the $tables array.
846 *
847 * @param string $query The query to analyze
848 * @param string $tables The array where the name of the detected table is added
849 *
850 * @return void
851 */
852 private function extractTablesToConvert($query, &$tables)
853 {
854 // Normalize the whitespace of the query
855 $query = trim($query);
856 $query = str_replace(array("\r\n", "\r", "\n"), ' ', $query);
857
858 while (strstr($query, ' ') !== false)
859 {
860 $query = str_replace(' ', ' ', $query);
861 }
862
863 // Is it a create table query?
864 $queryStart = substr($query, 0, 12);
865 $queryStart = strtoupper($queryStart);
866
867 if ($queryStart != 'CREATE TABLE')
868 {
869 return;
870 }
871
872 // Remove the CREATE TABLE keyword. Also, If there's an IF NOT EXISTS clause remove it.
873 $query = substr($query, 12);
874 $query = str_ireplace('IF NOT EXISTS', '', $query);
875 $query = trim($query);
876
877 // Make sure there is a space between the table name and its definition, denoted by an open parenthesis
878 $query = str_replace('(', ' (', $query);
879
880 // Now we should have the name of the table, a space and the rest of the query. Extract the table name.
881 $parts = explode(' ', $query, 2);
882 $tableName = $parts[0];
883
884 /**
885 * The table name may be quoted. Since UTF8MB4 is only supported in MySQL, the table name can only be
886 * quoted with surrounding backticks. Therefore we can trim backquotes from the table name to unquote it!
887 **/
888 $tableName = trim($tableName, '`');
889
890 // Finally, add the table name to $tables if it doesn't already exist.
891 if (!in_array($tableName, $tables))
892 {
893 $tables[] = $tableName;
894 }
895 }
896
897 /**
898 * Converts the collation of tables listed in $tablesToConvert to utf8mb4_unicode_ci
899 *
900 * @param array $tablesToConvert The list of tables to convert
901 *
902 * @return void
903 */
904 private function convertTablesToUtf8mb4($tablesToConvert)
905 {
906 try
907 {
908 $utf8mb4Support = $this->db->hasUTF8mb4Support();
909 }
910 catch (\Exception $e)
911 {
912 $utf8mb4Support = false;
913 }
914
915 // Make sure the database driver REALLY has support for converting character sets
916 if (!$utf8mb4Support)
917 {
918 return;
919 }
920
921 asort($tablesToConvert);
922
923 foreach ($tablesToConvert as $tableName)
924 {
925 $collation = $this->getTableCollation($tableName);
926
927 $parts = explode('_', $collation, 3);
928 $encoding = empty($parts[0]) ? '' : strtolower($parts[0]);
929
930 if ($encoding != 'utf8mb4')
931 {
932 $queries = $this->db->getAlterTableCharacterSet($tableName);
933
934 try
935 {
936 foreach ($queries as $query)
937 {
938 $this->db->setQuery($query)->execute();
939 }
940 }
941 catch (\Exception $e)
942 {
943 // We ignore failed conversions. Remember, you MUST change your indices MANUALLY.
944 }
945 }
946 }
947 }
948
949 /**
950 * Returns true if table $tableName has an index named $indexName or if it's impossible to retrieve index names for
951 * the table (not enough privileges, not a MySQL database, ...)
952 *
953 * @param string $tableName The name of the table
954 * @param string $indexName The name of the index
955 *
956 * @return bool
957 */
958 private function hasIndex($tableName, $indexName)
959 {
960 static $isMySQL = null;
961 static $cache = array();
962
963 if (is_null($isMySQL))
964 {
965 $driverType = $this->db->name;
966 $driverType = strtolower($driverType);
967 $isMySQL = true;
968
969 if (
970 !strpos($driverType, 'mysql') === 0
971 && !(substr($driverType, -5) == 'mysql')
972 && !(substr($driverType, -6) == 'mysqli')
973 )
974 {
975 $isMySQL = false;
976 }
977 }
978
979 // Not MySQL? Lie and return true.
980 if (!$isMySQL)
981 {
982 return true;
983 }
984
985 if (!isset($cache[$tableName]))
986 {
987 $cache[$tableName] = array();
988 }
989
990 if (!isset($cache[$tableName][$indexName]))
991 {
992 $cache[$tableName][$indexName] = true;
993
994 try
995 {
996 $indices = array();
997 $query = 'SHOW INDEXES FROM ' . $this->db->qn($tableName);
998 $indexDefinitions = $this->db->setQuery($query)->loadAssocList();
999
1000 if (!empty($indexDefinitions) && is_array($indexDefinitions))
1001 {
1002 foreach ($indexDefinitions as $def)
1003 {
1004 $indices[] = $def['Key_name'];
1005 }
1006
1007 $indices = array_unique($indices);
1008 }
1009
1010 $cache[$tableName][$indexName] = in_array($indexName, $indices);
1011 }
1012 catch (\Exception $e)
1013 {
1014 // Ignore errors
1015 }
1016 }
1017
1018 return $cache[$tableName][$indexName];
1019 }
1020 }
1021