<?php /** * @package akeebabackup * @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace Akeeba\Engine\Driver; // Protection against direct access defined('_JEXEC') || die(); use Exception; use Joomla\CMS\Factory; use Joomla\Database\DatabaseDriver; use Joomla\Database\DatabaseInterface; use Joomla\Database\Mysql\MysqlDriver; use Joomla\Database\Mysqli\MysqliDriver; use Joomla\Database\Pdo\PdoDriver; use Joomla\Database\Pgsql\PgsqlDriver; use Joomla\Database\Sqlazure\SqlazureDriver; use Joomla\Database\Sqlite\SqliteDriver; use Joomla\Database\Sqlsrv\SqlsrvDriver; use ReflectionObject; use RuntimeException; class Joomla { /** @var Base The real database connection object */ private $dbo; /** * Database object constructor * * @param array $options List of options used to configure the connection */ public function __construct($options = []) { // Get the database driver *AND* make sure it's connected. /** @var DatabaseInterface|null $db */ $db = \Akeeba\Engine\Platform\Joomla::getDbDriver(); if (empty($db)) { throw new RuntimeException("Joomla does not return a database driver."); } $db->connect(); $options['connection'] = $db->getConnection(); $driver = $this->getDriverType($db); if (empty($driver)) { throw new RuntimeException("Unsupported database driver {$db->getName()}"); } $driver = '\\Akeeba\\Engine\\Driver\\' . ucfirst($driver); $this->dbo = new $driver($options); } public function close() { /** * We should not, in fact, try to close the connection by calling the parent method. * * If you close the connection we ask PHP's mysql / mysqli / pdomysql driver to disconnect the MySQL connection * resource from the database server inside our instance of Akeeba Engine's database driver. However, this * identical resource is also present in Joomla's database driver. Joomla will also try to close the connection * to a now invalid resource, causing a PHP notice to be recorded. * * By setting the connection resource to null in our own driver object we prevent closing the resource, * delegating that responsibility to Joomla. It will gladly do so at the very least automatically, through its * db driver's __destruct. */ $this->dbo->setConnection(null); } public function open() { if (method_exists($this->dbo, 'open')) { $this->dbo->open(); return; } if (method_exists($this->dbo, 'connect')) { $this->dbo->connect(); } } /** * Magic method to proxy all calls to the loaded database driver object * * @throws Exception */ public function __call($name, array $arguments) { if (is_null($this->dbo)) { throw new Exception('Akeeba Engine database driver is not loaded'); } if (method_exists($this->dbo, $name) || in_array($name, ['q', 'nq', 'qn'])) { return $this->dbo->{$name}(...$arguments); } throw new Exception('Method ' . $name . ' not found in Akeeba Platform'); } public function __get($name) { if (isset($this->dbo->$name) || property_exists($this->dbo, $name)) { return $this->dbo->$name; } $this->dbo->$name = null; user_error('Database driver does not support property ' . $name); return null; } public function __set($name, $value) { if (isset($this->dbo->name) || property_exists($this->dbo, $name)) { $this->dbo->$name = $value; return; } $this->dbo->$name = null; user_error('Database driver not support property ' . $name); } /** * Get the Akeeba Engine database driver type for the Joomla database object. * * Weak typing of the argument is deliberate. The class hierarchy of the database driver classes may change even * within the same major version of Joomla, as happened in the past with Joomla 3. Having weak typing we can amend * this method to straddle the change, i.e. make it compatible with Joomla versions before and after the change. In * simple terms, it's future–proofing. * * @param DatabaseInterface|DatabaseDriver $db * * @return string|null The driver type; null if unsupported */ private function getDriverType($db): ?string { // Make sure we got an object if (!is_object($db)) { return null; } // Get the Joomla database driver name — assuming the object passed is a DatabaseInterface instance if (method_exists($db, 'getName')) { $jDriverName = $db->getName(); } else { // On Joomla 4 this is supposed to raise an E_USER_DEPRECATED notice $jDriverName = $db->name ?? ''; } // Quick shortcuts to known core Joomla database drivers if (in_array($jDriverName, ['mysql', 'pdomysql'])) { return 'pdomysql'; } elseif ($jDriverName === 'mysqli') { return 'mysqli'; } elseif ( (stristr($jDriverName, 'postgre') !== false) || (stristr($jDriverName, 'pgsql') !== false) || (stristr($jDriverName, 'oracle') !== false) || (stristr($jDriverName, 'sqlite') !== false) || (stristr($jDriverName, 'sqlsrv') !== false) || (stristr($jDriverName, 'sqlazure') !== false) || (stristr($jDriverName, 'mssql') !== false) ) { return null; } /** * We do not have a driver name known to the core. This is a custom database driver, implemented by a Joomla * extension. This is typically used in two use cases: * - Transparent content translation (JoomFish, Falang, jDiction, ...) * - Support for primary / secondary database servers (primary is read only, secondary is write only) * The custom database drier will be extending one of the core drivers. We will use defensive code to detect * that, making no assumption that the core driver class exists because these classes are an implementation * detail in Joomla which may change over time, even though they are explicitly included in its SemVer promise. * We have been around long enough to know better than believing Joomla won't break SemVer by accident... */ if ( (class_exists(MysqlDriver::class) && ($db instanceof MysqlDriver)) || (class_exists(Pdomysql::class) && ($db instanceof Pdomysql)) ) { return 'pdomysql'; } elseif (class_exists(MysqliDriver::class) && ($db instanceof MysqliDriver)) { return 'mysqli'; } elseif ( (class_exists(PgsqlDriver::class) && ($db instanceof PgsqlDriver)) || (class_exists(SqliteDriver::class) && ($db instanceof SqliteDriver)) || (class_exists(SqlsrvDriver::class) && ($db instanceof SqlsrvDriver)) || (class_exists(SqlazureDriver::class) && ($db instanceof SqlazureDriver)) ) { return null; } // We still have no idea. We will need to use reflection. If it's unavailable we give up. if (!class_exists(ReflectionObject::class)) { return null; } $refDriver = new ReflectionObject($db); // Is this a generic PDO driver instance? if ((class_exists(PdoDriver::class) && ($db instanceof PdoDriver)) && $refDriver->hasProperty('options')) { $refOptions = $refDriver->getProperty('options'); $refOptions->setAccessible(true); $options = $refOptions->getValue($db); $options = is_array($options) ? $options : []; $pdoDriver = $options['driver'] ?? 'odbc'; switch ($pdoDriver) { // PDO MySQL. We support this! case 'mysql': return 'pdomysql'; // ODBC: I need to inspect the DSN case 'obdc': $dsn = $options['dsn'] ?? ''; // No DSN? No joy. if (empty($dsn)) { return null; } // That's MySQL over ODBC over PDO. OK, rather strained but we can do that. if (stripos($dsn, 'mysql:') === 0) { return 'pdomysql'; } // Anything else: tough luck. return null; // Anything else: tough luck. default: return null; } } // Let's get the class hierarchy and see if we have anything that looks like MySQL in its name. $classNames = class_parents($db); array_unshift($classNames, get_class($db)); $isMySQLi = array_reduce($classNames, function (bool $carry, string $className) { return $carry || (stripos($className, 'mysqli') !== false); }, false); if ($isMySQLi) { return 'mysqli'; } $isPdoMySQL = array_reduce($classNames, function (bool $carry, string $className) { return $carry || (stripos($className, 'pdomysql') !== false); }, false); if ($isPdoMySQL) { return 'pdomysql'; } // All possible checks failed. I have no idea what you're doing here, mate. return null; } }