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

namespace Magento\Setup\Model;

use Magento\Authorization\Model\Acl\Role\Group;
use Magento\Authorization\Model\Acl\Role\User;
use Magento\Authorization\Model\UserContextInterface;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\DB\Adapter\AdapterInterface;

class AdminAccount
{
    /**#@+
     * Data keys
     */
    const KEY_USER = 'admin-user';
    const KEY_PASSWORD = 'admin-password';
    const KEY_EMAIL = 'admin-email';
    const KEY_FIRST_NAME = 'admin-firstname';
    const KEY_LAST_NAME = 'admin-lastname';
    const KEY_PREFIX = 'db-prefix';
    /**#@- */

    /**
     * Db connection
     *
     * @var AdapterInterface
     */
    private $connection;

    /**
     * Configurations
     *
     * @var []
     */
    private $data;

    /**
     * @var EncryptorInterface
     */
    private $encryptor;

    /**
     * Default Constructor
     *
     * @param AdapterInterface $connection
     * @param EncryptorInterface $encryptor
     * @param array $data
     */
    public function __construct(
        AdapterInterface $connection,
        EncryptorInterface $encryptor,
        array $data
    ) {
        $this->connection = $connection;
        $this->encryptor = $encryptor;
        $this->data = $data;
    }

    /**
     * Generate password string
     *
     * @return string
     */
    protected function generatePassword()
    {
        return $this->encryptor->getHash($this->data[self::KEY_PASSWORD], true);
    }

    /**
     * Save administrator account and user role to DB.
     *
     * If the administrator account exists, update it.
     *
     * @return void
     */
    public function save()
    {
        $adminId = $this->saveAdminUser();
        $this->saveAdminUserRole($adminId);
    }

    /**
     * Uses the information in data[] to create the admin user.
     *
     * If the username already exists, it will update the record with information from data[]
     * and set the is_active flag.
     *
     * @return int The admin user id
     */
    private function saveAdminUser()
    {
        $passwordHash = $this->generatePassword();
        $adminData = [
            'firstname' => $this->data[self::KEY_FIRST_NAME],
            'lastname'  => $this->data[self::KEY_LAST_NAME],
            'password'  => $passwordHash,
            'is_active' => 1,
        ];
        $result = $this->connection->fetchRow(
            'SELECT user_id, username, email FROM ' . $this->getTableName('admin_user') . ' ' .
            'WHERE username = :username OR email = :email',
            ['username' => $this->data[self::KEY_USER], 'email' => $this->data[self::KEY_EMAIL]],
            null
        );

        if (!empty($result)) {
            // User exists, update
            $this->validateUserMatches();
            $adminId = $result['user_id'];
            $adminData['modified'] = date('Y-m-d H:i:s');

            $this->connection->update(
                $this->getTableName('admin_user'),
                $adminData,
                $this->connection->quoteInto('username = ?', $this->data[self::KEY_USER])
            );
        } else {
            // User does not exist, create it
            $adminData['username'] = $this->data[self::KEY_USER];
            $adminData['email'] = $this->data[self::KEY_EMAIL];
            $this->connection->insert(
                $this->getTableName('admin_user'),
                $adminData
            );
            $adminId = $this->connection->lastInsertId();
        }
        $this->trackPassword($adminId, $passwordHash);

        return $adminId;
    }

    /**
     * Remember a password hash for further usage.
     *
     * @param int $adminId
     * @param string $passwordHash
     * @return void
     */
    private function trackPassword($adminId, $passwordHash)
    {
        $this->connection->insert(
            $this->getTableName('admin_passwords'),
            [
                'user_id' => $adminId,
                'password_hash' => $passwordHash,
                'last_updated' => time()
            ]
        );
    }

    /**
     * Validates that the username and email both match the user,
     * and that password exists and is different from user name.
     *
     * @return void
     * @throws \Exception If the username and email do not both match data provided to install
     * @throws \Exception If password is empty and if password is the same as the user name
     */
    public function validateUserMatches()
    {
        if (empty($this->data[self::KEY_PASSWORD])) {
            throw new \Exception(
                '"Password" is required. Enter and try again.'
            );
        }

        if (strcasecmp($this->data[self::KEY_PASSWORD], $this->data[self::KEY_USER]) == 0) {
            throw new \Exception(
                'Password cannot be the same as the user name.'
            );
        }

        try {
            $result = $this->connection->fetchRow(
                "SELECT user_id, username, email FROM {$this->getTableName('admin_user')} "
                . "WHERE username = :username OR email = :email",
                ['username' => $this->data[self::KEY_USER], 'email' => $this->data[self::KEY_EMAIL]]
            );
        } catch (\Exception $e) {
            return; // New installation, no need to validate existing users.
        }

        $email = $result['email'];
        $username = $result['username'];

        if ((strcasecmp($email, $this->data[self::KEY_EMAIL]) == 0) &&
            (strcasecmp($username, $this->data[self::KEY_USER]) != 0)) {
            // email matched but username did not
            throw new \Exception(
                'An existing user has the given email but different username. '
                . 'Username and email both need to match an existing user or both be new.'
            );
        }
        if ((strcasecmp($username, $this->data[self::KEY_USER]) == 0) &&
            (strcasecmp($email, $this->data[self::KEY_EMAIL]) != 0)) {
            // username matched but email did not
            throw new \Exception(
                'An existing user has the given username but different email. '
                . 'Username and email both need to match an existing user or both be new.'
            );
        }
    }

    /**
     * Creates the admin user role if one does not exist.
     *
     * Do nothing if a role already exists for this user
     *
     * @param int $adminId User id of administrator to set role for
     * @return void
     */
    private function saveAdminUserRole($adminId)
    {
        $result = $this->connection->fetchRow(
            'SELECT * FROM ' . $this->getTableName('authorization_role') . ' ' .
            'WHERE user_id = :user_id AND user_type = :user_type',
            ['user_id' => $adminId, 'user_type' => UserContextInterface::USER_TYPE_ADMIN]
        );
        if (empty($result)) {
            // No user role exists for this user id, create it
            $adminRoleData = [
                'parent_id'  => $this->retrieveAdministratorsRoleId(),
                'tree_level' => 2,
                'role_type'  => User::ROLE_TYPE,
                'user_id'    => $adminId,
                'user_type'  => UserContextInterface::USER_TYPE_ADMIN,
                'role_name'  => $this->data[self::KEY_USER],
            ];
            $this->connection->insert($this->getTableName('authorization_role'), $adminRoleData);
        }
    }

    /**
     * Gets the "Administrators" role id, the special role created by data fixture in Authorization module.
     *
     * @return int The id of the Administrators role
     * @throws \Exception If Administrators role not found or problem connecting with database.
     */
    private function retrieveAdministratorsRoleId()
    {
        // Get Administrators role id to use as parent_id
        $administratorsRoleData = [
            'parent_id'  => 0,
            'tree_level' => 1,
            'role_type' => Group::ROLE_TYPE,
            'user_id' => 0,
            'user_type' => UserContextInterface::USER_TYPE_ADMIN,
            'role_name' => 'Administrators',
        ];
        $result = $this->connection->fetchRow(
            'SELECT * FROM ' . $this->getTableName('authorization_role') . ' ' .
            'WHERE parent_id = :parent_id AND tree_level = :tree_level AND role_type = :role_type AND ' .
            'user_id = :user_id AND user_type = :user_type AND role_name = :role_name',
            $administratorsRoleData
        );
        if (empty($result)) {
            throw new \Exception('No Administrators role was found, data fixture needs to be run');
        } else {
            // Found at least one, use first
            return $result['role_id'];
        }
    }

    /**
     * Take table with prefix without loading modules
     *
     * @param string $table
     * @return string
     */
    private function getTableName($table)
    {
        if (!empty($this->data[self::KEY_PREFIX])) {
            return $this->connection->getTableName($this->data[self::KEY_PREFIX] . $table);
        }

        return $this->connection->getTableName($table);
    }
}