Project::loadApplications()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
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 2024 Atlas 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
16
namespace BEdita\ImportTools\Utility;
17
18
use BEdita\Core\Model\Table\ApplicationsTable;
19
use BEdita\Core\Model\Table\UsersTable;
20
use Cake\Console\ConsoleIo;
21
use Cake\Database\Connection;
22
use Cake\Datasource\ConnectionManager;
23
use Cake\ORM\Locator\LocatorAwareTrait;
24
use Cake\Utility\Hash;
25
26
/**
27
 * Project utility
28
 */
29
class Project
30
{
31
    use LocatorAwareTrait;
32
33
    /**
34
     * @var \Cake\Database\Connection|null
35
     */
36
    protected ?Connection $defaultConnection = null;
37
38
    /**
39
     * @var \Cake\Database\Connection|null
40
     */
41
    protected ?Connection $importConnection = null;
42
43
    /**
44
     * @var \Cake\Console\ConsoleIo
45
     */
46
    protected ConsoleIo $io;
47
48
    /**
49
     * @var \BEdita\Core\Model\Table\ApplicationsTable
50
     */
51
    protected ApplicationsTable $Applications;
52
53
    /**
54
     * @var \BEdita\Core\Model\Table\UsersTable
55
     */
56
    protected UsersTable $Users;
57
58
    /**
59
     * Constructor
60
     *
61
     * @param \Cake\Console\ConsoleIo $io Console IO
62
     * @return void
63
     */
64
    public function __construct(ConsoleIo $io)
65
    {
66
        /** @var \BEdita\Core\Model\Table\ApplicationsTable $applications */
67
        $applications = $this->fetchTable('Applications');
68
        $this->Applications = $applications;
69
70
        /** @var \BEdita\Core\Model\Table\UsersTable $users */
71
        $users = $this->fetchTable('Users');
72
        $this->Users = $users;
73
74
        $this->io = $io;
75
76
        /** @var \Cake\Database\Connection|null $defaultConnection */
77
        $defaultConnection = ConnectionManager::get('default');
78
        $this->defaultConnection = $defaultConnection;
79
80
        /** @var \Cake\Database\Connection|null $importConnection */
81
        $importConnection = ConnectionManager::get('import');
82
        $this->importConnection = $importConnection;
83
    }
84
85
    /**
86
     * Check if `import` and `default` datasources are correctly configured
87
     *
88
     * @return bool
89
     */
90
    public function checkDatasourceConfig(): bool
91
    {
92
        if (!in_array('import', ConnectionManager::configured())) {
93
            $this->io->error('Unable to connect to `import` datasource, please review "Datasource" configuration');
94
95
            return false;
96
        }
97
        if (!$this->importConnection instanceof Connection || !$this->defaultConnection instanceof Connection) {
98
            $this->io->error('Wrong connection type, please review "Datasource" configuration');
99
100
            return false;
101
        }
102
103
        return true;
104
    }
105
106
    /**
107
     * Load applications on a given connection
108
     * Return an array having `name` as key,
109
     *
110
     * @param \Cake\Database\Connection $connection The Connection
111
     * @return array
112
     */
113
    public function loadApplications(Connection $connection): array
114
    {
115
        $this->Applications->setConnection($connection);
116
        $apps = $this->Applications->find()->select(['name', 'api_key', 'client_secret'])->toArray();
117
118
        return Hash::combine($apps, '{n}.name', '{n}');
119
    }
120
121
    /**
122
     * Load users on a given connection
123
     * Return an array having `username` as key,
124
     *
125
     * @param \Cake\Database\Connection $connection The Connection
126
     * @return array
127
     */
128
    public function loadUsers(Connection $connection): array
129
    {
130
        $this->Users->setConnection($connection);
131
        $users = $this->Users->find()->select(['username', 'password_hash'])->toArray();
132
133
        return Hash::combine($users, '{n}.username', '{n}');
134
    }
135
136
    /**
137
     * Update applications api keys using api keys provided in input array
138
     *
139
     * @param \Cake\Database\Connection $connection The connection
140
     * @param array $applications Application data
141
     * @return void
142
     */
143
    public function updateApplications(Connection $connection, array $applications): void
144
    {
145
        $this->Applications->setConnection($connection);
146
        foreach ($applications as $name => $application) {
147
            /** @var \BEdita\Core\Model\Entity\Application $entity */
148
            $entity = $this->Applications->find()->where(['name' => $name])->firstOrFail();
149
            $entity->api_key = $application->api_key;
150
            $entity->client_secret = $application->client_secret;
151
            $this->Applications->saveOrFail($entity);
152
        }
153
    }
154
155
    /**
156
     * Update users password hashes
157
     *
158
     * @param \Cake\Database\Connection $connection The connection
159
     * @param array $users Users data
160
     * @return void
161
     */
162
    public function updateUsers(Connection $connection, array $users): void
163
    {
164
        foreach ($users as $username => $user) {
165
            if (empty($user->password_hash)) {
166
                continue;
167
            }
168
            $query = sprintf(
169
                "UPDATE users SET password_hash = '%s' WHERE username = '%s'",
170
                $user->password_hash,
171
                $username,
172
            );
173
            $connection->execute($query);
174
        }
175
    }
176
177
    /**
178
     * Review applications and update api keys
179
     *
180
     * @return bool
181
     */
182
    public function reviewApplications(): bool
183
    {
184
        $current = $this->loadApplications($this->defaultConnection);
185
        $import = $this->loadApplications($this->importConnection);
186
        $missing = array_diff(array_keys($import), array_keys($current));
187
        if (!empty($missing)) {
188
            $this->io->error(sprintf('Some applications are missing on current project: %s', implode(' ', $missing)));
189
190
            return false;
191
        }
192
        $update = array_intersect_key($current, $import);
193
        $this->updateApplications($this->importConnection, $update);
194
195
        return true;
196
    }
197
198
    /**
199
     * Review users and update password hashes
200
     *
201
     * @return bool
202
     */
203
    public function reviewUsers(): bool
204
    {
205
        $current = $this->loadUsers($this->defaultConnection);
206
        $import = $this->loadUsers($this->importConnection);
207
        $missing = array_diff(array_keys($import), array_keys($current));
208
        if (!empty($missing)) {
209
            $this->io->warning(sprintf('Some users are missing in current project [%d]', count($missing)));
210
            if ($this->io->askChoice('Do you want to proceed?', ['y', 'n'], 'n') === 'n') {
211
                $this->io->error('Aborting.');
212
213
                return false;
214
            }
215
        }
216
        $update = array_intersect_key($current, $import);
217
        $this->updateUsers($this->importConnection, $update);
218
219
        return true;
220
    }
221
}
222