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

ImportProjectCommand::execute()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 35
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 25
c 1
b 0
f 0
dl 0
loc 35
rs 9.2088
cc 5
nc 12
nop 2
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 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
    /**
43
     * Main command execution:
44
     *  - applications and users are loaded from current and imported project
45
     *  - applications api keys and users passwords are updated to be used in current environment
46
     *
47
     * {@inheritDoc}
48
     */
49
    public function execute(Arguments $args, ConsoleIo $io)
50
    {
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
        // review `applications`
57
        $this->loadModel('Applications');
58
        $current = $this->loadApplications(ConnectionManager::get('default'));
59
        $import = $this->loadApplications($importConnection);
60
        $missing = array_diff(array_keys($import), array_keys($current));
61
        if (!empty($missing)) {
62
            $io->error(sprintf('Some applications are missing on current project: %s', implode(' ', $missing)));
63
            $this->abort();
64
        }
65
        $update = array_intersect_key($current, $import);
66
        $this->updateApplications($importConnection, $update);
67
68
        // review `users`
69
        $this->loadModel('Users');
70
        $current = $this->loadUsers(ConnectionManager::get('default'));
71
        $import = $this->loadUsers($importConnection);
72
        $missing = array_diff(array_keys($import), array_keys($current));
73
        if (!empty($missing)) {
74
            $io->warning(sprintf('Some users are missing in current project [%d]', count($missing)));
75
76
            if ($io->ask('Do you want to proceed?', 'n', ['y', 'n']) === 'n') {
77
                $io->error('Aborting.');
78
                $this->abort();
79
            }
80
        }
81
        $update = array_intersect_key($current, $import);
82
        $this->updateUsers($importConnection, $update);
83
        $io->success('Import project done.');
84
    }
85
86
    /**
87
     * Load applications on a given connection
88
     * Return an array having `name` as key,
89
     *
90
     * @param \Cake\Database\Connection $connection The Connection
91
     * @return array
92
     */
93
    protected function loadApplications(Connection $connection): array
94
    {
95
        $this->Applications->setConnection($connection);
96
        $apps = $this->Applications->find()->select(['name', 'api_key', 'client_secret'])->toArray();
97
98
        return Hash::combine($apps, '{n}.name', '{n}');
99
    }
100
101
    /**
102
     * Load users on a given connection
103
     * Return an array having `username` as key,
104
     *
105
     * @param Connection $connection The Connection
106
     * @return array
107
     */
108
    protected function loadUsers(Connection $connection): array
109
    {
110
        $this->Users->setConnection($connection);
111
        $users = $this->Users->find()->select(['username', 'password_hash'])->toArray();
112
113
        return Hash::combine($users, '{n}.username', '{n}');
114
    }
115
116
    /**
117
     * Update applications api keys using api keys provided in input array
118
     *
119
     * @param \Cake\Database\Connection $connection The connection
120
     * @param array $applications Application data
121
     * @return void
122
     */
123
    protected function updateApplications(Connection $connection, array $applications): void
124
    {
125
        $this->Applications->setConnection($connection);
126
        foreach ($applications as $name => $application) {
127
            $entity = $this->Applications->find()->where(['name' => $name])->firstOrFail();
128
            $entity->api_key = $application->api_key;
129
            $entity->client_secret = $application->client_secret;
130
            $this->Applications->saveOrFail($entity);
131
        }
132
    }
133
134
    /**
135
     * Update applications api keys using api keys provided in input array
136
     *
137
     * @param Connection $connection The connection
138
     * @param array $users Users data
139
     * @return void
140
     */
141
    protected function updateUsers(Connection $connection, array $users): void
142
    {
143
        foreach ($users as $username => $user) {
144
            if (!empty($user->password_hash)) {
145
                $query = sprintf(
146
                    "UPDATE users SET password_hash = '%s' WHERE username = '%s'",
147
                    $user->password_hash,
148
                    $username
149
                );
150
                $connection->execute($query);
151
            }
152
        }
153
    }
154
}
155