TikiWiki/tiki-manager

View on GitHub
src/Libs/Database/Database.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php
// Copyright (c) 2016, Avan.Tech, et. al.
// Copyright (c) 2008, Luis Argerich, Garland Foster, Eduardo Polidor, et. al.
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.

namespace TikiManager\Libs\Database;

use TikiManager\Config\App;
use TikiManager\Ext\Password;
use TikiManager\Libs\Host\Command;
use TikiManager\Application\Instance;
use TikiManager\Libs\Helpers\ApplicationHelper;
use TikiManager\Libs\Database\Exception\DatabaseErrorException;

class Database
{
    /** @var Instance */
    private $instance;
    private $access;
    private $extensions = array();
    private $mysqlArgs = array();

    public $dbname;
    public $host;
    public $pass;
    public $type;
    public $user;

    public $dbLocalContent = "";

    public function __construct(Instance $instance)
    {
        $this->setInstance($instance);
    }

    public function setInstance(Instance $instance)
    {
        $this->instance = $instance;
        $this->access = $instance->getBestAccess('scripting');
        $this->locateExtensions();

        return $this;
    }

    public function connect()
    {
        if (!($this->user && $this->host && $this->pass)) {
            throw new DatabaseErrorException("Invalid credentials", 2);
        }

        try {
            $result = (int) $this->query('SELECT 1;');
            return $result === 1;
        } catch (DatabaseErrorException $e) {
            if ($this->host === 'localhost') {
                $this->setMysqlArg('--protocol=TCP');
                $result = (int) $this->query('SELECT 1;');
                return $result === 1;
            }
            throw new DatabaseErrorException($e->getMessage(), $e->getCode());
        }
    }

    public function createAccess($user, $dbname, $pass = null)
    {
        $pass = $pass ?: Password::create(12, 'unpronounceable');

        $success = $this->createDatabase($dbname)
            && $this->createUser($user, $pass)
            && $this->grantRights($user, $dbname)
            && $this->finalize();

        $credentials = array(
            'dbname' => $dbname,
            'host'   => $this->host,
            'pass'   => $pass,
            'type'   => $this->type,
            'user'   => $user
        );

        $db = new self($this->instance);
        $db->setCredentials($credentials);
        if ($db->testConnection()) {
            return $db;
        }
        return null;
    }

    public function createDatabase($name)
    {
        $sql = sprintf('CREATE DATABASE IF NOT EXISTS `%s`;', $name);
        $result = $this->query($sql);
        return $this;
    }

    /**
     * Verify if the databases are equal
     * @param Database $database1
     * @param Database $database2
     * @return boolean
     */
    public static function compareDatabase(Database $database1, Database $database2)
    {
        return ($database1->host === $database2->host &&
            $database1->dbname === $database2->dbname &&
            $database1->user === $database2->user
        );
    }

    public static function createFromConfig($instance, $db_local_path)
    {
        if (! (file_exists($db_local_path) && filesize($db_local_path) > 0)) {
            return null;
        }

        $config = self::getInstanceDataBaseConfig($db_local_path);

        $db = new self($instance);
        $db->host = $config['host'];
        $db->user = $config['user'];
        $db->pass = $config['pass'];
        $db->dbname = $config['dbname'];
        $db->type = $config['type'];

        if ($db->testConnection()) {
            $db->dbLocalContent = file_get_contents($db_local_path);
            return $db;
        }

        return null;
    }

    public function getCurrentUserPermissions($refresh = false)
    {
        static $grants;

        if ($grants && !$refresh) {
            return $grants;
        }

        $sql = 'SHOW GRANTS FOR CURRENT_USER';
        return $grants = $this->query($sql);
    }

    /**
     * Ensure the the logged user has permission to create users and grants
     *
     * @return boolean
     */
    public function hasCreateUserPermissions()
    {
        $grants = $this->getCurrentUserPermissions();

        $regex = '/GRANT (ALL PRIVILEGES|.*CREATE USER).* ON \*\.\* .* WITH GRANT OPTION/m';

        return \preg_match($regex, $grants);
    }

    public function hasCreateDatabasePermissions()
    {
        $grants = $this->getCurrentUserPermissions();

        $regex = '/GRANT (ALL PRIVILEGES|.*CREATE(?! USER)).* ON \*\.\*/m';

        return \preg_match($regex, $grants);
    }

    public function createUser($username, $password)
    {
        if ($username == $this->user) { //DB user is the root user
            return $this;
        }
        $host = $this->host === 'localhost'
            ? $this->host
            : '%';

        $sql = sprintf(
            'CREATE USER `%s`@`%s` IDENTIFIED BY "%s";',
            $username,
            $host,
            $password
        );
        $result = $this->query($sql);
        return $this;
    }

    public function databaseExists($dbname)
    {
        $dbname = $dbname ?: $this->dbname;
        $sql = sprintf('SHOW DATABASES LIKE "%s"', $dbname);
        $result = $this->query($sql);
        return trim($result) === $dbname;
    }

    public function finalize()
    {
        $sql = 'FLUSH PRIVILEGES;';
        $result = $this->query($sql);
        return $this;
    }

    public function getMaxUsernameLength()
    {
        $sql = 'SELECT CHARACTER_MAXIMUM_LENGTH'
            . ' FROM information_schema.COLUMNS'
            . ' WHERE TABLE_NAME="user"'
            .   ' AND TABLE_SCHEMA="mysql"'
            .   ' AND COLUMN_NAME="User"';

        $result = (int) $this->query($sql);
        return $result;
    }

    public function getUsableExtensions()
    {
        return array_intersect(
            $this->instance->getApplication()->getAcceptableExtensions(),
            $this->extensions
        );
    }

    public function grantRights($username, $database)
    {
        if ($username == $this->user) {
            return $this;
        }
        $host = $this->host === 'localhost'
            ? $this->host
            : '%';

        $sql = sprintf(
            'GRANT ALL ON `%s`.* TO `%s`@`%s`;',
            $database,
            $username,
            $host
        );
        $result = $this->query($sql);
        return $this;
    }

    public function locateExtensions()
    {
        if (empty($this->extensions)) {
            $modules = $this->instance->getExtensions();
            $expected = array('mysqli', 'mysql', 'pdo_mysql');
            $this->extensions = array_intersect($modules, $expected);
        }
        return $this->extensions;
    }

    public function query($sql)
    {
        $args = array(
            '-u', $this->user,
            '-p'. escapeshellarg($this->pass),
            '-h', $this->host,
            '-N',
            '-s'
        );
        $args = array_merge($args, $this->mysqlArgs);

        if (ApplicationHelper::isWindows()) {
            $args = implode(' ', $args);
            $command = 'echo ' . $sql . ' | mysql -f ' . $args;
            $command_result = $this->access->shellExec($command);
            return isset($command_result) ? rtrim($command_result, PHP_EOL) : 0;
        }

        $command = new Command('mysql', $args, $sql);
        $this->access->runCommand($command);

        if ($command->getReturn() !== 0) {
            throw new DatabaseErrorException($command->getStderrContent(), 1);
        }
        return $command->getStdoutContent();
    }

    public function setCredentials($credentials = array())
    {
        $credentials = array_filter($credentials, 'is_scalar');
        if (!empty($credentials['dbname'])) {
            $this->dbname = $credentials['dbname'];
        }
        if (!empty($credentials['host'])) {
            $this->host = $credentials['host'];
        }
        if (!empty($credentials['pass'])) {
            $this->pass = $credentials['pass'];
        }
        if (!empty($credentials['type'])) {
            $this->type = $credentials['type'];
        }
        if (!empty($credentials['user'])) {
            $this->user = $credentials['user'];
        }
        return $this;
    }

    private function setMysqlArg($arg)
    {
        $this->mysqlArgs[] = $arg;
    }

    public function testConnection()
    {
        try {
            return $this->connect();
        } catch (DatabaseErrorException $e) {
            App::get('io')->error($e->getMessage());
        }
        return false;
    }

    public function userExists($user)
    {
        $user = $user ?: $this->user;
        $host = $this->host === 'localhost' ? $this->host : '%';

        $sql = sprintf(
            'SELECT user FROM mysql.user'
            . ' WHERE user="%s" AND host="%s"',
            $user,
            $host
        );
        $result = $this->query($sql);
        return trim($result) === $user;
    }

    public static function getInstanceDataBaseConfig($db_local_path)
    {
        $getConfig = function ($db_local_path) {
            include($db_local_path);
            return array(
                'type' => $db_tiki ?? '',
                'host' => $host_tiki ?? '',
                'user' => $user_tiki ?? '',
                'pass' => $pass_tiki ?? '',
                'dbname' => $dbs_tiki ?? '',
            );
        };

        return $getConfig($db_local_path);
    }

    /**
     * @param Database|array $config
     * @return $this|Database|null
     */
    public function setupConnection()
    {
        $config = $this->instance->getDatabaseConfig();

        if ($config instanceof Database) {
            return $config;
        }

        list('host' => $dbHost, 'user' => $dbUser, 'pass' => $dbPass, 'database' => $dbName, 'prefix' => $dbPrefix) = $config;

        $this->host = $dbHost;
        $this->user = $dbUser;
        $this->pass = $dbPass;

        $types = $this->getUsableExtensions();
        $this->type = reset($types) ?: getenv('MYSQL_DRIVER');

        try {
            if ($dbPrefix) {
                $username = "{$dbPrefix}_user";
                $dbname = "{$dbPrefix}_db";
                $config = $this->createAccess($username, $dbname);
            }

            if (!$dbPrefix && !$this->databaseExists($dbName)) {
                $this->createDatabase($dbName);
            }

            if (!$dbPrefix) {
                $this->dbname = $dbName;
                $config = $this;
            }

            $this->instance->setDatabaseConfig($config);
        } catch (DatabaseErrorException $e) {
            throw new \RuntimeException("Can't setup database!\nError: " . $e->getMessage());
        }
    }
}
// vi: expandtab shiftwidth=4 softtabstop=4 tabstop=4