Passed
Pull Request — main (#12)
by Dante
01:15
created

ImportProjectCommand::updateApplications()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 9
rs 10
cc 2
nc 2
nop 2
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2024 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\ImportTools\Command;
16
17
use Cake\Command\Command;
18
use Cake\Console\Arguments;
19
use Cake\Console\ConsoleIo;
20
use Cake\Database\Connection;
21
use Cake\Datasource\ConnectionManager;
22
use Cake\Utility\Hash;
23
24
/**
25
 * Command to prepare `applications` and `users` to import a project in a new environment.
26
 * Since applications api keys and users passwords will generally differ this command will try to
27
 * sync those values, when possible, in order to enable migration.
28
 *
29
 * The new project database you want to import must be reachable via an `import` key Datasource configuration.
30
 * This new project will be modified this way:
31
 *  - new project `applications` must be present as name on the current project DB => current api keys are saved in the new imported project
32
 *  - users password hashes are changed (if set) in the new imported project using current users password hashes on records with the same `username`
33
 *
34
 * After this command is finished the new imported project can be used in the current environment and replace current project/database.
35
 *
36
 * @property \BEdita\Core\Model\Table\ApplicationsTable $Applications
37
 * @property \BEdita\Core\Model\Table\UsersTable $Users
38
 */
39
class ImportProjectCommand extends Command
40
{
41
    /**
42
     * {@inheritDoc}
43
     *
44
     * Main command execution:
45
     * - applications and users are loaded from current and imported project
46
     * - applications api keys and users passwords are updated to be used in current environment
47
     */
48
    public function execute(Arguments $args, ConsoleIo $io)
49
    {
50
        $io->out('Start');
51
        if (!in_array('import', ConnectionManager::configured())) {
52
            $io->error('Unable to connect to `import` datasource, please review "Datasource" configuration');
53
            $this->abort();
54
        }
55
        $importConnection = ConnectionManager::get('import');
56
        $defaultConnection = ConnectionManager::get('default');
57
        if (!$importConnection instanceof Connection || !$defaultConnection instanceof Connection) {
58
            $io->error('Wrong connection type, please review "Datasource" configuration');
59
            $this->abort();
60
        }
61
62
        // review `applications`
63
        $this->Applications = $this->fetchTable('Applications');
64
        $current = $this->loadApplications($defaultConnection);
65
        $import = $this->loadApplications($importConnection);
66
        $missing = array_diff(array_keys($import), array_keys($current));
67
        if (!empty($missing)) {
68
            $io->error(sprintf('Some applications are missing on current project: %s', implode(' ', $missing)));
69
            $this->abort();
70
        }
71
        $update = array_intersect_key($current, $import);
72
        $this->updateApplications($importConnection, $update);
73
74
        // review `users`
75
        $this->Users = $this->fetchTable('Users');
76
        $current = $this->loadUsers($defaultConnection);
77
        $import = $this->loadUsers($importConnection);
78
        $missing = array_diff(array_keys($import), array_keys($current));
79
        if (!empty($missing)) {
80
            $io->warning(sprintf('Some users are missing in current project [%d]', count($missing)));
81
82
            if ($io->askChoice('Do you want to proceed?', ['y', 'n'], 'n') === 'n') {
83
                $io->error('Aborting.');
84
                $this->abort();
85
            }
86
        }
87
        $update = array_intersect_key($current, $import);
88
        $this->updateUsers($importConnection, $update);
89
        $io->success('Import project done.');
90
        $io->out('End');
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
                continue;
154
            }
155
            $query = sprintf(
156
                "UPDATE users SET password_hash = '%s' WHERE username = '%s'",
157
                $user->password_hash,
158
                $username
159
            );
160
            $connection->execute($query);
161
        }
162
    }
163
}
164