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

ImportProjectCommand   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 126
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 14
eloc 54
c 2
b 1
f 0
dl 0
loc 126
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A loadApplications() 0 6 1
A updateUsers() 0 12 3
A loadUsers() 0 6 1
A updateApplications() 0 9 2
B execute() 0 47 7
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
        /** @var \BEdita\Core\Model\Table\ApplicationsTable $applications */
64
        $applications = $this->fetchTable('Applications');
65
        $this->Applications = $applications;
66
        $current = $this->loadApplications($defaultConnection);
67
        $import = $this->loadApplications($importConnection);
68
        $missing = array_diff(array_keys($import), array_keys($current));
69
        if (!empty($missing)) {
70
            $io->error(sprintf('Some applications are missing on current project: %s', implode(' ', $missing)));
71
            $this->abort();
72
        }
73
        $update = array_intersect_key($current, $import);
74
        $this->updateApplications($importConnection, $update);
75
76
        // review `users`
77
        /** @var \BEdita\Core\Model\Table\UsersTable $users */
78
        $users = $this->fetchTable('Users');
79
        $this->Users = $users;
80
        $current = $this->loadUsers($defaultConnection);
81
        $import = $this->loadUsers($importConnection);
82
        $missing = array_diff(array_keys($import), array_keys($current));
83
        if (!empty($missing)) {
84
            $io->warning(sprintf('Some users are missing in current project [%d]', count($missing)));
85
86
            if ($io->askChoice('Do you want to proceed?', ['y', 'n'], 'n') === 'n') {
87
                $io->error('Aborting.');
88
                $this->abort();
89
            }
90
        }
91
        $update = array_intersect_key($current, $import);
92
        $this->updateUsers($importConnection, $update);
93
        $io->success('Import project done.');
94
        $io->out('End');
95
    }
96
97
    /**
98
     * Load applications on a given connection
99
     * Return an array having `name` as key,
100
     *
101
     * @param \Cake\Database\Connection $connection The Connection
102
     * @return array
103
     */
104
    protected function loadApplications(Connection $connection): array
105
    {
106
        $this->Applications->setConnection($connection);
107
        $apps = $this->Applications->find()->select(['name', 'api_key', 'client_secret'])->toArray();
108
109
        return Hash::combine($apps, '{n}.name', '{n}');
110
    }
111
112
    /**
113
     * Load users on a given connection
114
     * Return an array having `username` as key,
115
     *
116
     * @param \Cake\Database\Connection $connection The Connection
117
     * @return array
118
     */
119
    protected function loadUsers(Connection $connection): array
120
    {
121
        $this->Users->setConnection($connection);
122
        $users = $this->Users->find()->select(['username', 'password_hash'])->toArray();
123
124
        return Hash::combine($users, '{n}.username', '{n}');
125
    }
126
127
    /**
128
     * Update applications api keys using api keys provided in input array
129
     *
130
     * @param \Cake\Database\Connection $connection The connection
131
     * @param array $applications Application data
132
     * @return void
133
     */
134
    protected function updateApplications(Connection $connection, array $applications): void
135
    {
136
        $this->Applications->setConnection($connection);
137
        foreach ($applications as $name => $application) {
138
            /** @var \BEdita\Core\Model\Entity\Application $entity */
139
            $entity = $this->Applications->find()->where(['name' => $name])->firstOrFail();
140
            $entity->api_key = $application->api_key;
141
            $entity->client_secret = $application->client_secret;
142
            $this->Applications->saveOrFail($entity);
143
        }
144
    }
145
146
    /**
147
     * Update applications api keys using api keys provided in input array
148
     *
149
     * @param \Cake\Database\Connection $connection The connection
150
     * @param array $users Users data
151
     * @return void
152
     */
153
    protected function updateUsers(Connection $connection, array $users): void
154
    {
155
        foreach ($users as $username => $user) {
156
            if (empty($user->password_hash)) {
157
                continue;
158
            }
159
            $query = sprintf(
160
                "UPDATE users SET password_hash = '%s' WHERE username = '%s'",
161
                $user->password_hash,
162
                $username
163
            );
164
            $connection->execute($query);
165
        }
166
    }
167
}
168