Skip to content
Snippets Groups Projects
Database.php 8.98 KiB
Newer Older
declare(strict_types=1);

namespace SimpleSAML;
use Exception;
use PDOStatement;
Tim van Dijen's avatar
Tim van Dijen committed
use function count;
use function is_array;
use function rand;
use function serialize;
 * This file implements functions to read and write to a group of database servers.
Guy Halse's avatar
Guy Halse committed
 * This database class supports a single database, or a primary/secondary configuration with as many defined secondaries
 * as a user would like.
 * The goal of this class is to provide a single mechanism to connect to a database that can be reused by any component
 * within SimpleSAMLphp including modules. When using this class, the global configuration should be passed here, but in
 * the case of a module that has a good reason to use a different database, such as sqlauth, an alternative config file
 * can be provided.
 * @package SimpleSAMLphp
    /**
     * This variable holds the instance of the session - Singleton approach.
     * @var \SimpleSAML\Database[]
    private static array $instance = [];
Guy Halse's avatar
Guy Halse committed
     * PDO Object for the Primary database server
    private PDO $dbPrimary;
Guy Halse's avatar
Guy Halse committed
     * Array of PDO Objects for configured database secondaries
     * @var \PDO[]
    private array $dbSecondaries = [];

    /**
     * Prefix to apply to the tables
     */
    private string $tablePrefix;
    /**
     * Array with information on the last error occurred.
     */
    private array $lastError = [];

    /**
     * Retrieves the current database instance. Will create a new one if there isn't an existing connection.
     *
     * @param \SimpleSAML\Configuration $altConfig Optional: Instance of a \SimpleSAML\Configuration class
     *
     * @return \SimpleSAML\Database The shared database connection.
     */
Tim van Dijen's avatar
Tim van Dijen committed
    public static function getInstance(Configuration $altConfig = null): Database
        $config = ($altConfig) ? $altConfig : Configuration::getInstance();
        $instanceId = self::generateInstanceId($config);

        // check if we already have initialized the session
        if (isset(self::$instance[$instanceId])) {
            return self::$instance[$instanceId];
        }

        // create a new session
        self::$instance[$instanceId] = new Database($config);
        return self::$instance[$instanceId];
    }


    /**
     * Private constructor that restricts instantiation to getInstance().
     *
     * @param \SimpleSAML\Configuration $config Instance of the \SimpleSAML\Configuration class
    private function __construct(Configuration $config)
        $driverOptions = $config->getOptionalArray('database.driver_options', []);
        if ($config->getOptionalBoolean('database.persistent', true)) {
            $driverOptions[PDO::ATTR_PERSISTENT] = true;
Guy Halse's avatar
Guy Halse committed
        // connect to the primary
        $this->dbPrimary = $this->connect(
            $config->getString('database.dsn'),
            $config->getOptionalString('database.username', null),
            $config->getOptionalString('database.password', null),
        // connect to any configured secondaries
        $secondaries = $config->getOptionalArray('database.secondaries', []);
Guy Halse's avatar
Guy Halse committed
        foreach ($secondaries as $secondary) {
            $this->dbSecondaries[] = $this->connect(
                $secondary['dsn'],
                $secondary['username'],
                $secondary['password'],
                $driverOptions
        $this->tablePrefix = $config->getOptionalString('database.prefix', '');
     * Generate an Instance ID based on the database configuration.
     * @param \SimpleSAML\Configuration $config Configuration class
     *
     * @return string $instanceId
     */
    private static function generateInstanceId(Configuration $config): string
        $assembledConfig = [
Guy Halse's avatar
Guy Halse committed
            'primary' => [
                'database.dsn'        => $config->getString('database.dsn'),
                'database.username'   => $config->getOptionalString('database.username', null),
                'database.password'   => $config->getOptionalString('database.password', null),
                'database.prefix'     => $config->getOptionalString('database.prefix', ''),
                'database.persistent' => $config->getOptionalBoolean('database.persistent', true),
            'secondaries' => $config->getOptionalArray('database.secondaries', []),

        return sha1(serialize($assembledConfig));
    }


    /**
     * This function connects to a database.
     *
     * @param string $dsn Database connection string
     * @param string|null $username SQL user
     * @param string|null $password SQL password
     * @param array  $options PDO options
     *
     * @throws \Exception If an error happens while trying to connect to the database.
     * @return \PDO object
     */
    private function connect(string $dsn, string $username = null, string $password = null, array $options): PDO
            $db = new PDO($dsn, $username, $password, $options);
            $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch (PDOException $e) {
            throw new Exception("Database error: " . $e->getMessage());
Guy Halse's avatar
Guy Halse committed
     * This function randomly selects a secondary database server to query. In the event no secondaries are configured,
     * it will return the primary.
Guy Halse's avatar
Guy Halse committed
    private function getSecondary(): PDO
Guy Halse's avatar
Guy Halse committed
        if (count($this->dbSecondaries) > 0) {
            $secondaryId = rand(0, count($this->dbSecondaries) - 1);
            return $this->dbSecondaries[$secondaryId];
Guy Halse's avatar
Guy Halse committed
            return $this->dbPrimary;
        }
    }


    /**
     * This function simply applies the table prefix to a supplied table name.
     *
     * @param string $table Table to apply prefix to, if configured
     *
     * @return string Table with configured prefix
     */
Tim van Dijen's avatar
Tim van Dijen committed
    public function applyPrefix(string $table): string
        return $this->tablePrefix . $table;
    }


    /**
     * This function queries the database
     *
     * @param \PDO   $db PDO object to use
     * @param string $stmt Prepared SQL statement
     * @param array  $params Parameters
     *
     * @throws \Exception If an error happens while trying to execute the query.
     * @return \PDOStatement object
    private function query(PDO $db, string $stmt, array $params): PDOStatement
    {
        try {
            $query = $db->prepare($stmt);

            foreach ($params as $param => $value) {
                if (is_array($value)) {
                    $query->bindValue(":$param", $value[0], ($value[1]) ? $value[1] : PDO::PARAM_STR);
                    $query->bindValue(":$param", $value, PDO::PARAM_STR);
        } catch (PDOException $e) {
            throw new Exception("Database error: " . $e->getMessage());
     * This function queries the database without using a prepared statement.
     *
     * @param \PDO   $db PDO object to use
     * @param string $stmt An SQL statement to execute, previously escaped.
     *
     * @throws \Exception If an error happens while trying to execute the query.
     * @return int The number of rows affected.
    private function exec(PDO $db, string $stmt): int
            return $db->exec($stmt);
        } catch (PDOException $e) {
            throw new Exception("Database error: " . $e->getMessage());
Guy Halse's avatar
Guy Halse committed
     * This executes queries directly on the primary.
     *
     * @param string $stmt Prepared SQL statement
     * @param array  $params Parameters
     *
     * @return int|false The number of rows affected by the query or false on error.
Tim van Dijen's avatar
Tim van Dijen committed
    public function write(string $stmt, array $params = []): int|bool
Guy Halse's avatar
Guy Halse committed
        return $this->query($this->dbPrimary, $stmt, $params)->rowCount();
Guy Halse's avatar
Guy Halse committed
     * This executes queries on a database server that is determined by this::getSecondary().
     *
     * @param string $stmt Prepared SQL statement
     * @param array  $params Parameters
     *
Tim van Dijen's avatar
Tim van Dijen committed
     * @return \PDOStatement object
Tim van Dijen's avatar
Tim van Dijen committed
    public function read(string $stmt, array $params = []): PDOStatement
Guy Halse's avatar
Guy Halse committed
        $db = $this->getSecondary();

        return $this->query($db, $stmt, $params);
    }


    /**
     * Return an array with information about the last operation performed in the database.
     *
     * @return array The array with error information.
     */
Tim van Dijen's avatar
Tim van Dijen committed
    public function getLastError(): array


    /**
     * Return the name of the PDO-driver
     *
     * @return string
     */
    public function getDriver(): string
    {
        return $this->dbPrimary->getAttribute(PDO::ATTR_DRIVER_NAME);
    }