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 * @note        This file has been modified by the Joomla! Project and no longer reflects the original work of its author.
8 *
9 * This file is adapted from the Joomla! Platform. It is used to iterate a database cursor returning FOFTable objects
10 * instead of plain stdClass objects
11 */
12
13// Protect from unauthorized access
14defined('FOF_INCLUDED') or die;
15
16/**
17 * MySQL database driver supporting PDO based connections
18 *
19 * @package     Joomla.Platform
20 * @subpackage  Database
21 * @see         http://php.net/manual/en/ref.pdo-mysql.php
22 * @since       3.4
23 */
24class FOFDatabaseDriverPdomysql extends FOFDatabaseDriverPdo
25{
26	/**
27	 * The name of the database driver.
28	 *
29	 * @var    string
30	 * @since  3.4
31	 */
32	public $name = 'pdomysql';
33
34	/**
35	 * The type of the database server family supported by this driver.
36	 *
37	 * @var    string
38	 * @since  CMS 3.5.0
39	 */
40	public $serverType = 'mysql';
41
42	/**
43	 * The character(s) used to quote SQL statement names such as table names or field names,
44	 * etc. The child classes should define this as necessary.  If a single character string the
45	 * same character is used for both sides of the quoted name, else the first character will be
46	 * used for the opening quote and the second for the closing quote.
47	 *
48	 * @var    string
49	 * @since  3.4
50	 */
51	protected $nameQuote = '`';
52
53	/**
54	 * The null or zero representation of a timestamp for the database driver.  This should be
55	 * defined in child classes to hold the appropriate value for the engine.
56	 *
57	 * @var    string
58	 * @since  3.4
59	 */
60	protected $nullDate = '0000-00-00 00:00:00';
61
62	/**
63	 * The minimum supported database version.
64	 *
65	 * @var    string
66	 * @since  3.4
67	 */
68	protected static $dbMinimum = '5.0.4';
69
70	/**
71	 * Constructor.
72	 *
73	 * @param   array  $options  Array of database options with keys: host, user, password, database, select.
74	 *
75	 * @since   3.4
76	 */
77	public function __construct($options)
78	{
79		/**
80		 * Pre-populate the UTF-8 Multibyte compatibility flag. Unfortunately PDO won't report the server version
81		 * unless we're connected to it and we cannot connect to it unless we know if it supports utf8mb4 which requires
82		 * us knowing the server version. Between this chicken and egg issue we _assume_ it's supported and we'll just
83		 * catch any problems at connection time.
84		 */
85		$this->utf8mb4 = true;
86
87		// Get some basic values from the options.
88		$options['driver']  = 'mysql';
89		$options['charset'] = (isset($options['charset'])) ? $options['charset'] : 'utf8';
90
91		if ($this->utf8mb4 && ($options['charset'] == 'utf8'))
92		{
93			$options['charset'] = 'utf8mb4';
94		}
95
96		$this->charset = $options['charset'];
97
98		// Finalize initialisation.
99		parent::__construct($options);
100	}
101
102	/**
103	 * Connects to the database if needed.
104	 *
105	 * @return  void  Returns void if the database connected successfully.
106	 *
107	 * @since   3.4
108	 * @throws  RuntimeException
109	 */
110	public function connect()
111	{
112		try
113		{
114			// Try to connect to MySQL
115			parent::connect();
116		}
117		catch (\RuntimeException $e)
118		{
119			// If the connection failed but not because of the wrong character set bubble up the exception
120			if (!$this->utf8mb4 || ($this->options['charset'] != 'utf8mb4'))
121			{
122				throw $e;
123			}
124
125			/**
126			 * If the connection failed and I was trying to use the utf8mb4 charset then it is likely that the server
127			 * doesn't support utf8mb4 despite claiming otherwise.
128			 *
129			 * This happens on old MySQL server versions (less than 5.5.3) using the mysqlnd PHP driver. Since mysqlnd
130			 * masks the server version and reports only its own we can not be sure if the server actually does support
131			 * UTF-8 Multibyte (i.e. it's MySQL 5.5.3 or later). Since the utf8mb4 charset is undefined in this case we
132			 * catch the error and determine that utf8mb4 is not supported!
133			 */
134			$this->utf8mb4 = false;
135			$this->options['charset'] = 'utf8';
136
137			parent::connect();
138		}
139
140		$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
141		$this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
142	}
143
144	/**
145	 * Test to see if the MySQL connector is available.
146	 *
147	 * @return  boolean  True on success, false otherwise.
148	 *
149	 * @since   3.4
150	 */
151	public static function isSupported()
152	{
153		return class_exists('PDO') && in_array('mysql', PDO::getAvailableDrivers());
154	}
155
156	/**
157	 * Drops a table from the database.
158	 *
159	 * @param   string   $tableName  The name of the database table to drop.
160	 * @param   boolean  $ifExists   Optionally specify that the table must exist before it is dropped.
161	 *
162	 * @return  FOFDatabaseDriverPdomysql  Returns this object to support chaining.
163	 *
164	 * @since   3.4
165	 * @throws  RuntimeException
166	 */
167	public function dropTable($tableName, $ifExists = true)
168	{
169		$this->connect();
170
171		$query = $this->getQuery(true);
172
173		$query->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $this->quoteName($tableName));
174
175		$this->setQuery($query);
176
177		$this->execute();
178
179		return $this;
180	}
181
182	/**
183	 * Select a database for use.
184	 *
185	 * @param   string  $database  The name of the database to select for use.
186	 *
187	 * @return  boolean  True if the database was successfully selected.
188	 *
189	 * @since   3.4
190	 * @throws  RuntimeException
191	 */
192	public function select($database)
193	{
194		$this->connect();
195
196		$this->setQuery('USE ' . $this->quoteName($database));
197
198		$this->execute();
199
200		return $this;
201	}
202
203	/**
204	 * Method to get the database collation in use by sampling a text field of a table in the database.
205	 *
206	 * @return  mixed  The collation in use by the database (string) or boolean false if not supported.
207	 *
208	 * @since   3.4
209	 * @throws  RuntimeException
210	 */
211	public function getCollation()
212	{
213		$this->connect();
214
215		// Attempt to get the database collation by accessing the server system variable.
216		$this->setQuery('SHOW VARIABLES LIKE "collation_database"');
217		$result = $this->loadObject();
218
219		if (property_exists($result, 'Value'))
220		{
221			return $result->Value;
222		}
223		else
224		{
225			return false;
226		}
227	}
228
229	/**
230	 * Method to get the database connection collation, as reported by the driver. If the connector doesn't support
231	 * reporting this value please return an empty string.
232	 *
233	 * @return  string
234	 */
235	public function getConnectionCollation()
236	{
237		$this->connect();
238
239		// Attempt to get the database collation by accessing the server system variable.
240		$this->setQuery('SHOW VARIABLES LIKE "collation_connection"');
241		$result = $this->loadObject();
242
243		if (property_exists($result, 'Value'))
244		{
245			return $result->Value;
246		}
247		else
248		{
249			return false;
250		}
251	}
252
253	/**
254	 * Shows the table CREATE statement that creates the given tables.
255	 *
256	 * @param   mixed  $tables  A table name or a list of table names.
257	 *
258	 * @return  array  A list of the create SQL for the tables.
259	 *
260	 * @since   3.4
261	 * @throws  RuntimeException
262	 */
263	public function getTableCreate($tables)
264	{
265		$this->connect();
266
267		// Initialise variables.
268		$result = array();
269
270		// Sanitize input to an array and iterate over the list.
271		settype($tables, 'array');
272
273		foreach ($tables as $table)
274		{
275			$this->setQuery('SHOW CREATE TABLE ' . $this->quoteName($table));
276
277			$row = $this->loadRow();
278
279			// Populate the result array based on the create statements.
280			$result[$table] = $row[1];
281		}
282
283		return $result;
284	}
285
286	/**
287	 * Retrieves field information about a given table.
288	 *
289	 * @param   string   $table     The name of the database table.
290	 * @param   boolean  $typeOnly  True to only return field types.
291	 *
292	 * @return  array  An array of fields for the database table.
293	 *
294	 * @since   3.4
295	 * @throws  RuntimeException
296	 */
297	public function getTableColumns($table, $typeOnly = true)
298	{
299		$this->connect();
300
301		$result = array();
302
303		// Set the query to get the table fields statement.
304		$this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($table));
305
306		$fields = $this->loadObjectList();
307
308		// If we only want the type as the value add just that to the list.
309		if ($typeOnly)
310		{
311			foreach ($fields as $field)
312			{
313				$result[$field->Field] = preg_replace("/[(0-9)]/", '', $field->Type);
314			}
315		}
316		// If we want the whole field data object add that to the list.
317		else
318		{
319			foreach ($fields as $field)
320			{
321				$result[$field->Field] = $field;
322			}
323		}
324
325		return $result;
326	}
327
328	/**
329	 * Get the details list of keys for a table.
330	 *
331	 * @param   string  $table  The name of the table.
332	 *
333	 * @return  array  An array of the column specification for the table.
334	 *
335	 * @since   3.4
336	 * @throws  RuntimeException
337	 */
338	public function getTableKeys($table)
339	{
340		$this->connect();
341
342		// Get the details columns information.
343		$this->setQuery('SHOW KEYS FROM ' . $this->quoteName($table));
344
345		$keys = $this->loadObjectList();
346
347		return $keys;
348	}
349
350	/**
351	 * Method to get an array of all tables in the database.
352	 *
353	 * @return  array  An array of all the tables in the database.
354	 *
355	 * @since   3.4
356	 * @throws  RuntimeException
357	 */
358	public function getTableList()
359	{
360		$this->connect();
361
362		// Set the query to get the tables statement.
363		$this->setQuery('SHOW TABLES');
364		$tables = $this->loadColumn();
365
366		return $tables;
367	}
368
369	/**
370	 * Get the version of the database connector.
371	 *
372	 * @return  string  The database connector version.
373	 *
374	 * @since   3.4
375	 */
376	public function getVersion()
377	{
378		$this->connect();
379
380		return $this->getOption(PDO::ATTR_SERVER_VERSION);
381	}
382
383	/**
384	 * Locks a table in the database.
385	 *
386	 * @param   string  $table  The name of the table to unlock.
387	 *
388	 * @return  FOFDatabaseDriverPdomysql  Returns this object to support chaining.
389	 *
390	 * @since   3.4
391	 * @throws  RuntimeException
392	 */
393	public function lockTable($table)
394	{
395		$this->setQuery('LOCK TABLES ' . $this->quoteName($table) . ' WRITE')->execute();
396
397		return $this;
398	}
399
400	/**
401	 * Renames a table in the database.
402	 *
403	 * @param   string  $oldTable  The name of the table to be renamed
404	 * @param   string  $newTable  The new name for the table.
405	 * @param   string  $backup    Not used by MySQL.
406	 * @param   string  $prefix    Not used by MySQL.
407	 *
408	 * @return  FOFDatabaseDriverPdomysql  Returns this object to support chaining.
409	 *
410	 * @since   3.4
411	 * @throws  RuntimeException
412	 */
413	public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
414	{
415		$this->setQuery('RENAME TABLE ' . $this->quoteName($oldTable) . ' TO ' . $this->quoteName($newTable));
416
417		$this->execute();
418
419		return $this;
420	}
421
422	/**
423	 * Method to escape a string for usage in an SQL statement.
424	 *
425	 * Oracle escaping reference:
426	 * http://www.orafaq.com/wiki/SQL_FAQ#How_does_one_escape_special_characters_when_writing_SQL_queries.3F
427	 *
428	 * SQLite escaping notes:
429	 * http://www.sqlite.org/faq.html#q14
430	 *
431	 * Method body is as implemented by the Zend Framework
432	 *
433	 * Note: Using query objects with bound variables is
434	 * preferable to the below.
435	 *
436	 * @param   string   $text   The string to be escaped.
437	 * @param   boolean  $extra  Unused optional parameter to provide extra escaping.
438	 *
439	 * @return  string  The escaped string.
440	 *
441	 * @since   3.4
442	 */
443	public function escape($text, $extra = false)
444	{
445		$this->connect();
446
447		if (is_int($text) || is_float($text))
448		{
449			return $text;
450		}
451
452		$result = substr($this->connection->quote($text), 1, -1);
453
454		if ($extra)
455		{
456			$result = addcslashes($result, '%_');
457		}
458
459		return $result;
460	}
461
462	/**
463	 * Unlocks tables in the database.
464	 *
465	 * @return  FOFDatabaseDriverPdomysql  Returns this object to support chaining.
466	 *
467	 * @since   3.4
468	 * @throws  RuntimeException
469	 */
470	public function unlockTables()
471	{
472		$this->setQuery('UNLOCK TABLES')->execute();
473
474		return $this;
475	}
476
477	/**
478	 * Method to commit a transaction.
479	 *
480	 * @param   boolean  $toSavepoint  If true, commit to the last savepoint.
481	 *
482	 * @return  void
483	 *
484	 * @since   3.4
485	 * @throws  RuntimeException
486	 */
487	public function transactionCommit($toSavepoint = false)
488	{
489		$this->connect();
490
491		if (!$toSavepoint || $this->transactionDepth <= 1)
492		{
493			parent::transactionCommit($toSavepoint);
494		}
495		else
496		{
497			$this->transactionDepth--;
498		}
499	}
500
501	/**
502	 * Method to roll back a transaction.
503	 *
504	 * @param   boolean  $toSavepoint  If true, rollback to the last savepoint.
505	 *
506	 * @return  void
507	 *
508	 * @since   3.4
509	 * @throws  RuntimeException
510	 */
511	public function transactionRollback($toSavepoint = false)
512	{
513		$this->connect();
514
515		if (!$toSavepoint || $this->transactionDepth <= 1)
516		{
517			parent::transactionRollback($toSavepoint);
518		}
519		else
520		{
521			$savepoint = 'SP_' . ($this->transactionDepth - 1);
522			$this->setQuery('ROLLBACK TO SAVEPOINT ' . $this->quoteName($savepoint));
523
524			if ($this->execute())
525			{
526				$this->transactionDepth--;
527			}
528		}
529	}
530
531	/**
532	 * Method to initialize a transaction.
533	 *
534	 * @param   boolean  $asSavepoint  If true and a transaction is already active, a savepoint will be created.
535	 *
536	 * @return  void
537	 *
538	 * @since   3.4
539	 * @throws  RuntimeException
540	 */
541	public function transactionStart($asSavepoint = false)
542	{
543		$this->connect();
544
545		if (!$asSavepoint || !$this->transactionDepth)
546		{
547			parent::transactionStart($asSavepoint);
548		}
549		else
550		{
551			$savepoint = 'SP_' . $this->transactionDepth;
552			$this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint));
553
554			if ($this->execute())
555			{
556				$this->transactionDepth++;
557			}
558		}
559	}
560}
561