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 * Oracle database driver
18 *
19 * @see    http://php.net/pdo
20 * @since  12.1
21 */
22class FOFDatabaseDriverOracle extends FOFDatabaseDriverPdo
23{
24	/**
25	 * The name of the database driver.
26	 *
27	 * @var    string
28	 * @since  12.1
29	 */
30	public $name = 'oracle';
31
32	/**
33	 * The type of the database server family supported by this driver.
34	 *
35	 * @var    string
36	 * @since  CMS 3.5.0
37	 */
38	public $serverType = 'oracle';
39
40	/**
41	 * The character(s) used to quote SQL statement names such as table names or field names,
42	 * etc.  The child classes should define this as necessary.  If a single character string the
43	 * same character is used for both sides of the quoted name, else the first character will be
44	 * used for the opening quote and the second for the closing quote.
45	 *
46	 * @var    string
47	 * @since  12.1
48	 */
49	protected $nameQuote = '"';
50
51	/**
52	 * Returns the current dateformat
53	 *
54	 * @var   string
55	 * @since 12.1
56	 */
57	protected $dateformat;
58
59	/**
60	 * Returns the current character set
61	 *
62	 * @var   string
63	 * @since 12.1
64	 */
65	protected $charset;
66
67	/**
68	 * Constructor.
69	 *
70	 * @param   array  $options  List of options used to configure the connection
71	 *
72	 * @since   12.1
73	 */
74	public function __construct($options)
75	{
76		$options['driver'] = 'oci';
77		$options['charset']    = (isset($options['charset'])) ? $options['charset']   : 'AL32UTF8';
78		$options['dateformat'] = (isset($options['dateformat'])) ? $options['dateformat'] : 'RRRR-MM-DD HH24:MI:SS';
79
80		$this->charset = $options['charset'];
81		$this->dateformat = $options['dateformat'];
82
83		// Finalize initialisation
84		parent::__construct($options);
85	}
86
87	/**
88	 * Destructor.
89	 *
90	 * @since   12.1
91	 */
92	public function __destruct()
93	{
94		$this->freeResult();
95		unset($this->connection);
96	}
97
98	/**
99	 * Connects to the database if needed.
100	 *
101	 * @return  void  Returns void if the database connected successfully.
102	 *
103	 * @since   12.1
104	 * @throws  RuntimeException
105	 */
106	public function connect()
107	{
108		if ($this->connection)
109		{
110			return;
111		}
112
113		parent::connect();
114
115		if (isset($this->options['schema']))
116		{
117			$this->setQuery('ALTER SESSION SET CURRENT_SCHEMA = ' . $this->quoteName($this->options['schema']))->execute();
118		}
119
120		$this->setDateFormat($this->dateformat);
121	}
122
123	/**
124	 * Disconnects the database.
125	 *
126	 * @return  void
127	 *
128	 * @since   12.1
129	 */
130	public function disconnect()
131	{
132		// Close the connection.
133		$this->freeResult();
134		unset($this->connection);
135	}
136
137	/**
138	 * Drops a table from the database.
139	 *
140	 * Note: The IF EXISTS flag is unused in the Oracle driver.
141	 *
142	 * @param   string   $tableName  The name of the database table to drop.
143	 * @param   boolean  $ifExists   Optionally specify that the table must exist before it is dropped.
144	 *
145	 * @return  FOFDatabaseDriverOracle  Returns this object to support chaining.
146	 *
147	 * @since   12.1
148	 */
149	public function dropTable($tableName, $ifExists = true)
150	{
151		$this->connect();
152
153		$query = $this->getQuery(true)
154			->setQuery('DROP TABLE :tableName');
155		$query->bind(':tableName', $tableName);
156
157		$this->setQuery($query);
158
159		$this->execute();
160
161		return $this;
162	}
163
164	/**
165	 * Method to get the database collation in use by sampling a text field of a table in the database.
166	 *
167	 * @return  mixed  The collation in use by the database or boolean false if not supported.
168	 *
169	 * @since   12.1
170	 */
171	public function getCollation()
172	{
173		return $this->charset;
174	}
175
176	/**
177	 * Method to get the database connection collation, as reported by the driver. If the connector doesn't support
178	 * reporting this value please return an empty string.
179	 *
180	 * @return  string
181	 */
182	public function getConnectionCollation()
183	{
184		return $this->charset;
185	}
186
187	/**
188	 * Get a query to run and verify the database is operational.
189	 *
190	 * @return  string  The query to check the health of the DB.
191	 *
192	 * @since   12.2
193	 */
194	public function getConnectedQuery()
195	{
196		return 'SELECT 1 FROM dual';
197	}
198
199	/**
200	 * Returns the current date format
201	 * This method should be useful in the case that
202	 * somebody actually wants to use a different
203	 * date format and needs to check what the current
204	 * one is to see if it needs to be changed.
205	 *
206	 * @return string The current date format
207	 *
208	 * @since 12.1
209	 */
210	public function getDateFormat()
211	{
212		return $this->dateformat;
213	}
214
215	/**
216	 * Shows the table CREATE statement that creates the given tables.
217	 *
218	 * Note: You must have the correct privileges before this method
219	 * will return usable results!
220	 *
221	 * @param   mixed  $tables  A table name or a list of table names.
222	 *
223	 * @return  array  A list of the create SQL for the tables.
224	 *
225	 * @since   12.1
226	 * @throws  RuntimeException
227	 */
228	public function getTableCreate($tables)
229	{
230		$this->connect();
231
232		$result = array();
233		$query = $this->getQuery(true)
234			->select('dbms_metadata.get_ddl(:type, :tableName)')
235			->from('dual')
236			->bind(':type', 'TABLE');
237
238		// Sanitize input to an array and iterate over the list.
239		settype($tables, 'array');
240
241		foreach ($tables as $table)
242		{
243			$query->bind(':tableName', $table);
244			$this->setQuery($query);
245			$statement = (string) $this->loadResult();
246			$result[$table] = $statement;
247		}
248
249		return $result;
250	}
251
252	/**
253	 * Retrieves field information about a given table.
254	 *
255	 * @param   string   $table     The name of the database table.
256	 * @param   boolean  $typeOnly  True to only return field types.
257	 *
258	 * @return  array  An array of fields for the database table.
259	 *
260	 * @since   12.1
261	 * @throws  RuntimeException
262	 */
263	public function getTableColumns($table, $typeOnly = true)
264	{
265		$this->connect();
266
267		$columns = array();
268		$query = $this->getQuery(true);
269
270		$fieldCasing = $this->getOption(PDO::ATTR_CASE);
271
272		$this->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER);
273
274		$table = strtoupper($table);
275
276		$query->select('*');
277		$query->from('ALL_TAB_COLUMNS');
278		$query->where('table_name = :tableName');
279
280		$prefixedTable = str_replace('#__', strtoupper($this->tablePrefix), $table);
281		$query->bind(':tableName', $prefixedTable);
282		$this->setQuery($query);
283		$fields = $this->loadObjectList();
284
285		if ($typeOnly)
286		{
287			foreach ($fields as $field)
288			{
289				$columns[$field->COLUMN_NAME] = $field->DATA_TYPE;
290			}
291		}
292		else
293		{
294			foreach ($fields as $field)
295			{
296				$columns[$field->COLUMN_NAME] = $field;
297				$columns[$field->COLUMN_NAME]->Default = null;
298			}
299		}
300
301		$this->setOption(PDO::ATTR_CASE, $fieldCasing);
302
303		return $columns;
304	}
305
306	/**
307	 * Get the details list of keys for a table.
308	 *
309	 * @param   string  $table  The name of the table.
310	 *
311	 * @return  array  An array of the column specification for the table.
312	 *
313	 * @since   12.1
314	 * @throws  RuntimeException
315	 */
316	public function getTableKeys($table)
317	{
318		$this->connect();
319
320		$query = $this->getQuery(true);
321
322		$fieldCasing = $this->getOption(PDO::ATTR_CASE);
323
324		$this->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER);
325
326		$table = strtoupper($table);
327		$query->select('*')
328			->from('ALL_CONSTRAINTS')
329			->where('table_name = :tableName')
330			->bind(':tableName', $table);
331
332		$this->setQuery($query);
333		$keys = $this->loadObjectList();
334
335		$this->setOption(PDO::ATTR_CASE, $fieldCasing);
336
337		return $keys;
338	}
339
340	/**
341	 * Method to get an array of all tables in the database (schema).
342	 *
343	 * @param   string   $databaseName         The database (schema) name
344	 * @param   boolean  $includeDatabaseName  Whether to include the schema name in the results
345	 *
346	 * @return  array    An array of all the tables in the database.
347	 *
348	 * @since   12.1
349	 * @throws  RuntimeException
350	 */
351	public function getTableList($databaseName = null, $includeDatabaseName = false)
352	{
353		$this->connect();
354
355		$query = $this->getQuery(true);
356
357		if ($includeDatabaseName)
358		{
359			$query->select('owner, table_name');
360		}
361		else
362		{
363			$query->select('table_name');
364		}
365
366		$query->from('all_tables');
367
368		if ($databaseName)
369		{
370			$query->where('owner = :database')
371				->bind(':database', $databaseName);
372		}
373
374		$query->order('table_name');
375
376		$this->setQuery($query);
377
378		if ($includeDatabaseName)
379		{
380			$tables = $this->loadAssocList();
381		}
382		else
383		{
384			$tables = $this->loadColumn();
385		}
386
387		return $tables;
388	}
389
390	/**
391	 * Get the version of the database connector.
392	 *
393	 * @return  string  The database connector version.
394	 *
395	 * @since   12.1
396	 */
397	public function getVersion()
398	{
399		$this->connect();
400
401		$this->setQuery("select value from nls_database_parameters where parameter = 'NLS_RDBMS_VERSION'");
402
403		return $this->loadResult();
404	}
405
406	/**
407	 * Select a database for use.
408	 *
409	 * @param   string  $database  The name of the database to select for use.
410	 *
411	 * @return  boolean  True if the database was successfully selected.
412	 *
413	 * @since   12.1
414	 * @throws  RuntimeException
415	 */
416	public function select($database)
417	{
418		$this->connect();
419
420		return true;
421	}
422
423	/**
424	 * Sets the Oracle Date Format for the session
425	 * Default date format for Oracle is = DD-MON-RR
426	 * The default date format for this driver is:
427	 * 'RRRR-MM-DD HH24:MI:SS' since it is the format
428	 * that matches the MySQL one used within most Joomla
429	 * tables.
430	 *
431	 * @param   string  $dateFormat  Oracle Date Format String
432	 *
433	 * @return boolean
434	 *
435	 * @since  12.1
436	 */
437	public function setDateFormat($dateFormat = 'DD-MON-RR')
438	{
439		$this->connect();
440
441		$this->setQuery("ALTER SESSION SET NLS_DATE_FORMAT = '$dateFormat'");
442
443		if (!$this->execute())
444		{
445			return false;
446		}
447
448		$this->setQuery("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = '$dateFormat'");
449
450		if (!$this->execute())
451		{
452			return false;
453		}
454
455		$this->dateformat = $dateFormat;
456
457		return true;
458	}
459
460	/**
461	 * Set the connection to use UTF-8 character encoding.
462	 *
463	 * Returns false automatically for the Oracle driver since
464	 * you can only set the character set when the connection
465	 * is created.
466	 *
467	 * @return  boolean  True on success.
468	 *
469	 * @since   12.1
470	 */
471	public function setUtf()
472	{
473		return false;
474	}
475
476	/**
477	 * Locks a table in the database.
478	 *
479	 * @param   string  $table  The name of the table to unlock.
480	 *
481	 * @return  FOFDatabaseDriverOracle  Returns this object to support chaining.
482	 *
483	 * @since   12.1
484	 * @throws  RuntimeException
485	 */
486	public function lockTable($table)
487	{
488		$this->setQuery('LOCK TABLE ' . $this->quoteName($table) . ' IN EXCLUSIVE MODE')->execute();
489
490		return $this;
491	}
492
493	/**
494	 * Renames a table in the database.
495	 *
496	 * @param   string  $oldTable  The name of the table to be renamed
497	 * @param   string  $newTable  The new name for the table.
498	 * @param   string  $backup    Not used by Oracle.
499	 * @param   string  $prefix    Not used by Oracle.
500	 *
501	 * @return  FOFDatabaseDriverOracle  Returns this object to support chaining.
502	 *
503	 * @since   12.1
504	 * @throws  RuntimeException
505	 */
506	public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
507	{
508		$this->setQuery('RENAME ' . $oldTable . ' TO ' . $newTable)->execute();
509
510		return $this;
511	}
512
513	/**
514	 * Unlocks tables in the database.
515	 *
516	 * @return  FOFDatabaseDriverOracle  Returns this object to support chaining.
517	 *
518	 * @since   12.1
519	 * @throws  RuntimeException
520	 */
521	public function unlockTables()
522	{
523		$this->setQuery('COMMIT')->execute();
524
525		return $this;
526	}
527
528	/**
529	 * Test to see if the PDO ODBC connector is available.
530	 *
531	 * @return  boolean  True on success, false otherwise.
532	 *
533	 * @since   12.1
534	 */
535	public static function isSupported()
536	{
537		return class_exists('PDO') && in_array('oci', PDO::getAvailableDrivers());
538	}
539
540	/**
541	 * This function replaces a string identifier <var>$prefix</var> with the string held is the
542	 * <var>tablePrefix</var> class variable.
543	 *
544	 * @param   string  $query   The SQL statement to prepare.
545	 * @param   string  $prefix  The common table prefix.
546	 *
547	 * @return  string  The processed SQL statement.
548	 *
549	 * @since   11.1
550	 */
551	public function replacePrefix($query, $prefix = '#__')
552	{
553		$startPos = 0;
554		$quoteChar = "'";
555		$literal = '';
556
557		$query = trim($query);
558		$n = strlen($query);
559
560		while ($startPos < $n)
561		{
562			$ip = strpos($query, $prefix, $startPos);
563
564			if ($ip === false)
565			{
566				break;
567			}
568
569			$j = strpos($query, "'", $startPos);
570
571			if ($j === false)
572			{
573				$j = $n;
574			}
575
576			$literal .= str_replace($prefix, $this->tablePrefix, substr($query, $startPos, $j - $startPos));
577			$startPos = $j;
578
579			$j = $startPos + 1;
580
581			if ($j >= $n)
582			{
583				break;
584			}
585
586			// Quote comes first, find end of quote
587			while (true)
588			{
589				$k = strpos($query, $quoteChar, $j);
590				$escaped = false;
591
592				if ($k === false)
593				{
594					break;
595				}
596
597				$l = $k - 1;
598
599				while ($l >= 0 && $query[$l] == '\\')
600				{
601					$l--;
602					$escaped = !$escaped;
603				}
604
605				if ($escaped)
606				{
607					$j = $k + 1;
608					continue;
609				}
610
611				break;
612			}
613
614			if ($k === false)
615			{
616				// Error in the query - no end quote; ignore it
617				break;
618			}
619
620			$literal .= substr($query, $startPos, $k - $startPos + 1);
621			$startPos = $k + 1;
622		}
623
624		if ($startPos < $n)
625		{
626			$literal .= substr($query, $startPos, $n - $startPos);
627		}
628
629		return $literal;
630	}
631
632	/**
633	 * Method to commit a transaction.
634	 *
635	 * @param   boolean  $toSavepoint  If true, commit to the last savepoint.
636	 *
637	 * @return  void
638	 *
639	 * @since   12.3
640	 * @throws  RuntimeException
641	 */
642	public function transactionCommit($toSavepoint = false)
643	{
644		$this->connect();
645
646		if (!$toSavepoint || $this->transactionDepth <= 1)
647		{
648			parent::transactionCommit($toSavepoint);
649		}
650		else
651		{
652			$this->transactionDepth--;
653		}
654	}
655
656	/**
657	 * Method to roll back a transaction.
658	 *
659	 * @param   boolean  $toSavepoint  If true, rollback to the last savepoint.
660	 *
661	 * @return  void
662	 *
663	 * @since   12.3
664	 * @throws  RuntimeException
665	 */
666	public function transactionRollback($toSavepoint = false)
667	{
668		$this->connect();
669
670		if (!$toSavepoint || $this->transactionDepth <= 1)
671		{
672			parent::transactionRollback($toSavepoint);
673		}
674		else
675		{
676			$savepoint = 'SP_' . ($this->transactionDepth - 1);
677			$this->setQuery('ROLLBACK TO SAVEPOINT ' . $this->quoteName($savepoint));
678
679			if ($this->execute())
680			{
681				$this->transactionDepth--;
682			}
683		}
684	}
685
686	/**
687	 * Method to initialize a transaction.
688	 *
689	 * @param   boolean  $asSavepoint  If true and a transaction is already active, a savepoint will be created.
690	 *
691	 * @return  void
692	 *
693	 * @since   12.3
694	 * @throws  RuntimeException
695	 */
696	public function transactionStart($asSavepoint = false)
697	{
698		$this->connect();
699
700		if (!$asSavepoint || !$this->transactionDepth)
701		{
702			return parent::transactionStart($asSavepoint);
703		}
704
705		$savepoint = 'SP_' . $this->transactionDepth;
706		$this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint));
707
708		if ($this->execute())
709		{
710			$this->transactionDepth++;
711		}
712	}
713}
714