Passed
Pull Request — master (#5)
by Alberto
01:13
created

ImportProjectCommand::loadApplications()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 3
c 2
b 1
f 0
dl 0
loc 6
rs 10
cc 1
nc 1
nop 1
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2020 ChannelWeb Srl, Chialab Srl
7
 *
8
 * This file is part of BEdita: you can redistribute it and/or modify
9
 * it under the terms of the GNU Lesser General Public License as published
10
 * by the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14
 */
15
namespace BEdita\DevTools\Command;
16
17
use AllowDynamicProperties;
18
use Cake\Command\Command;
19
use Cake\Console\Arguments;
20
use Cake\Console\ConsoleIo;
21
use Cake\Database\Connection;
22
use Cake\Datasource\ConnectionManager;
23
use Cake\Utility\Hash;
24
25
/**
26
 * Command to prepare `applications` and `users` to import a project in a new environment.
27
 * Since applications api keys and users passwords will generally differ this command will try to
28
 * sync those values, when possible, in order to enable migration.
29
 *
30
 * The new project database you want to import must be reachable via an `import` key Datasource configuration.
31
 * This new project will be modified this way:
32
 *  - new project `applications` must be present as name on the current project DB => current api keys are saved in the new imported project
33
 *  - users password hashes are changed (if set) in the new imported project using current users password hashes on records with the same `username`
34
 *
35
 * After this command is finished the new imported project can be used in the current environment and replace current project/database.
36
 *
37
 * @property \BEdita\Core\Model\Table\ApplicationsTable $Applications
38
 * @property \BEdita\Core\Model\Table\UsersTable $Users
39
 */
40
#[AllowDynamicProperties]
41
class ImportProjectCommand extends Command
42
{
43
    /**
44
     * {@inheritDoc}
45
     *
46
     * Main command execution:
47
     * - applications and users are loaded from current and imported project
48
     * - applications api keys and users passwords are updated to be used in current environment
49
     */
50
    public function execute(Arguments $args, ConsoleIo $io)
51
    {
52
        if (!in_array('import', ConnectionManager::configured())) {
53
            $io->error('Unable to connect to `import` datasource, please review "Datasource" configuration');
54
            $this->abort();
55
        }
56
        $importConnection = ConnectionManager::get('import');
57
        $defaultConnection = ConnectionManager::get('default');
58
        if (!$importConnection instanceof Connection || !$defaultConnection instanceof Connection) {
59
            $io->error('Wrong connection type, please review "Datasource" configuration');
60
            $this->abort();
61
        }
62
63
        // review `applications`
64
        $this->loadModel('Applications');
65
        $current = $this->loadApplications($defaultConnection);
66
        $import = $this->loadApplications($importConnection);
67
        $missing = array_diff(array_keys($import), array_keys($current));
68
        if (!empty($missing)) {
69
            $io->error(sprintf('Some applications are missing on current project: %s', implode(' ', $missing)));
70
            $this->abort();
71
        }
72
        $update = array_intersect_key($current, $import);
73
        $this->updateApplications($importConnection, $update);
74
75
        // review `users`
76
        $this->loadModel('Users');
77
        $current = $this->loadUsers($defaultConnection);
78
        $import = $this->loadUsers($importConnection);
79
        $missing = array_diff(array_keys($import), array_keys($current));
80
        if (!empty($missing)) {
81
            $io->warning(sprintf('Some users are missing in current project [%d]', count($missing)));
82
83
            if ($io->askChoice('Do you want to proceed?', ['y', 'n'], 'n') === 'n') {
84
                $io->error('Aborting.');
85
                $this->abort();
86
            }
87
        }
88
        $update = array_intersect_key($current, $import);
89
        $this->updateUsers($importConnection, $update);
90
        $io->success('Import project done.');
91
    }
92
93
    /**
94
     * Load applications on a given connection
95
     * Return an array having `name` as key,
96
     *
97
     * @param \Cake\Database\Connection $connection The Connection
98
     * @return array
99
     */
100
    protected function loadApplications(Connection $connection): array
101
    {
102
        $this->Applications->setConnection($connection);
103
        $apps = $this->Applications->find()->select(['name', 'api_key', 'client_secret'])->toArray();
104
105
        return Hash::combine($apps, '{n}.name', '{n}');
106
    }
107
108
    /**
109
     * Load users on a given connection
110
     * Return an array having `username` as key,
111
     *
112
     * @param \Cake\Database\Connection $connection The Connection
113
     * @return array
114
     */
115
    protected function loadUsers(Connection $connection): array
116
    {
117
        $this->Users->setConnection($connection);
118
        $users = $this->Users->find()->select(['username', 'password_hash'])->toArray();
119
120
        return Hash::combine($users, '{n}.username', '{n}');
121
    }
122
123
    /**
124
     * Update applications api keys using api keys provided in input array
125
     *
126
     * @param \Cake\Database\Connection $connection The connection
127
     * @param array $applications Application data
128
     * @return void
129
     */
130
    protected function updateApplications(Connection $connection, array $applications): void
131
    {
132
        $this->Applications->setConnection($connection);
133
        foreach ($applications as $name => $application) {
134
            /** @var \BEdita\Core\Model\Entity\Application $entity */
135
            $entity = $this->Applications->find()->where(['name' => $name])->firstOrFail();
136
            $entity->api_key = $application->api_key;
137
            $entity->client_secret = $application->client_secret;
138
            $this->Applications->saveOrFail($entity);
139
        }
140
    }
141
142
    /**
143
     * Update applications api keys using api keys provided in input array
144
     *
145
     * @param \Cake\Database\Connection $connection The connection
146
     * @param array $users Users data
147
     * @return void
148
     */
149
    protected function updateUsers(Connection $connection, array $users): void
150
    {
151
        foreach ($users as $username => $user) {
152
            if (!empty($user->password_hash)) {
153
                $query = sprintf(
154
                    "UPDATE users SET password_hash = '%s' WHERE username = '%s'",
155
                    $user->password_hash,
156
                    $username
157
                );
158
                $connection->execute($query);
159
            }
160
        }
161
    }
162
}
163