<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Magento\Setup\Validator;

use Magento\Framework\Config\ConfigOptionsListConstants;
use Magento\Setup\Model\Installer;
use Magento\Setup\Module\ConnectionFactory;

/**
 * Class DbValidator - validates DB related settings
 */
class DbValidator
{

    /**
     * Db prefix max length
     */
    const DB_PREFIX_LENGTH = 5;

    /**
     * DB connection factory
     *
     * @var ConnectionFactory
     */
    private $connectionFactory;

    /**
     * Constructor
     *
     * @param ConnectionFactory $connectionFactory
     */
    public function __construct(ConnectionFactory $connectionFactory)
    {
        $this->connectionFactory = $connectionFactory;
    }

    /**
     * Check if database table prefix is valid
     *
     * @param string $prefix
     * @return boolean
     * @throws \InvalidArgumentException
     */
    public function checkDatabaseTablePrefix($prefix)
    {
        //The table prefix should contain only letters (a-z), numbers (0-9) or underscores (_);
        // the first character should be a letter.
        if ($prefix !== '' && !preg_match('/^([a-zA-Z])([[:alnum:]_]+)$/', $prefix)) {
            throw new \InvalidArgumentException(
                'Please correct the table prefix format, should contain only numbers, letters or underscores.'
                .' The first character should be a letter.'
            );
        }

        if (strlen($prefix) > self::DB_PREFIX_LENGTH) {
            throw new \InvalidArgumentException(
                'Table prefix length can\'t be more than ' . self::DB_PREFIX_LENGTH . ' characters.'
            );
        }

        return true;
    }

    /**
     * Checks Database Connection
     *
     * @param string $dbName
     * @param string $dbHost
     * @param string $dbUser
     * @param string $dbPass
     * @return boolean
     * @throws \Magento\Setup\Exception
     */
    public function checkDatabaseConnection($dbName, $dbHost, $dbUser, $dbPass = '')
    {
        // establish connection to information_schema view to retrieve information about user and table privileges
        $connection = $this->connectionFactory->create([
            ConfigOptionsListConstants::KEY_NAME => 'information_schema',
            ConfigOptionsListConstants::KEY_HOST => $dbHost,
            ConfigOptionsListConstants::KEY_USER => $dbUser,
            ConfigOptionsListConstants::KEY_PASSWORD => $dbPass,
            ConfigOptionsListConstants::KEY_ACTIVE => true,
        ]);

        if (!$connection) {
            throw new \Magento\Setup\Exception('Database connection failure.');
        }

        $mysqlVersion = $connection->fetchOne('SELECT version()');
        if ($mysqlVersion) {
            if (preg_match('/^([0-9\.]+)/', $mysqlVersion, $matches)) {
                if (isset($matches[1]) && !empty($matches[1])) {
                    if (version_compare($matches[1], Installer::MYSQL_VERSION_REQUIRED) < 0) {
                        throw new \Magento\Setup\Exception(
                            'Sorry, but we support MySQL version ' . Installer::MYSQL_VERSION_REQUIRED . ' or later.'
                        );
                    }
                }
            }
        }

        return $this->checkDatabaseName($connection, $dbName) && $this->checkDatabasePrivileges($connection, $dbName);
    }

    /**
     * Checks if specified database exists and visible to current user
     *
     * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
     * @param string $dbName
     * @return bool
     * @throws \Magento\Setup\Exception
     */
    private function checkDatabaseName(\Magento\Framework\DB\Adapter\AdapterInterface $connection, $dbName)
    {
        $query = "SHOW DATABASES";
        $accessibleDbs = $connection->query($query)->fetchAll(\PDO::FETCH_COLUMN, 0);
        foreach ($accessibleDbs as $accessibleDbName) {
            if ($dbName == $accessibleDbName) {
                return true;
            }
        }
        throw new \Magento\Setup\Exception(
            "Database '{$dbName}' does not exist "
            ."or specified database server user does not have privileges to access this database."
        );
    }

    /**
     * Checks database privileges
     *
     * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
     * @param string $dbName
     * @return bool
     * @throws \Magento\Setup\Exception
     */
    private function checkDatabasePrivileges(\Magento\Framework\DB\Adapter\AdapterInterface $connection, $dbName)
    {
        $requiredPrivileges = [
            'SELECT',
            'INSERT',
            'UPDATE',
            'DELETE',
            'CREATE',
            'DROP',
            'INDEX',
            'ALTER',
            'CREATE TEMPORARY TABLES',
            'LOCK TABLES',
            'EXECUTE',
            'CREATE VIEW',
            'SHOW VIEW',
            'CREATE ROUTINE',
            'ALTER ROUTINE',
            'TRIGGER'
        ];

        // check global privileges
        $userPrivilegesQuery = "SELECT PRIVILEGE_TYPE FROM USER_PRIVILEGES "
            . "WHERE REPLACE(GRANTEE, '\'', '') = current_user()";
        $grantInfo = $connection->query($userPrivilegesQuery)->fetchAll(\PDO::FETCH_NUM);
        if (empty(array_diff($requiredPrivileges, $this->parseGrantInfo($grantInfo)))) {
            return true;
        }

        // check table privileges
        $schemaPrivilegesQuery = "SELECT PRIVILEGE_TYPE FROM SCHEMA_PRIVILEGES " .
            "WHERE '$dbName' LIKE TABLE_SCHEMA AND REPLACE(GRANTEE, '\'', '') = current_user()";
        $grantInfo = $connection->query($schemaPrivilegesQuery)->fetchAll(\PDO::FETCH_NUM);
        if (empty(array_diff($requiredPrivileges, $this->parseGrantInfo($grantInfo)))) {
            return true;
        }

        $errorMessage = 'Database user does not have enough privileges. Please make sure '
            . implode(', ', $requiredPrivileges) . " privileges are granted to table '{$dbName}'.";
        throw new \Magento\Setup\Exception($errorMessage);
    }

    /**
     * Parses query result
     *
     * @param array $grantInfo
     * @return array
     */
    private function parseGrantInfo(array $grantInfo)
    {
        $result = [];
        foreach ($grantInfo as $grantRow) {
            $result[] = $grantRow[0];
        }
        return $result;
    }
}
