Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
2.92% covered (danger)
2.92%
43 / 1472
0.00% covered (danger)
0.00%
0 / 94
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiController
2.92% covered (danger)
2.92%
43 / 1472
0.00% covered (danger)
0.00%
0 / 94
84298.98
0.00% covered (danger)
0.00%
0 / 1
 apiLogin
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
20
 apiLogout
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 setUpServerMailHandler
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
42
 apiForgot
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 1
12
 apiResetPassword
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 1
42
 apiSettingsGet
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 apiAppConfigSet
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 apiSettingsSet
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
110
 apiSettingsCreate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 validateSettingsCreate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 createSettingFromRequest
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 apiSettingsAccountPasswordSet
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
56
 validatePasswordUpdate
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 apiSettingsAccountLocalizationSet
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 1
20
 apiSettingsDesignSet
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 apiApplicationCreate
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 createDefaultAppSettings
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 validateApplicationCreate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 createApplicationFromRequest
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 apiInstallApplication
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
72
 apiGroupGet
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 apiGroupUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 updateGroupFromRequest
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 validateGroupCreate
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 apiGroupCreate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 createGroupFromRequest
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 apiGroupDelete
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 validateGroupDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiGroupFind
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 apiAccountGet
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 apiAccountFind
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 apiAccountGroupFind
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 validateAccountCreate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
 apiAccountCreate
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
110
 apiAccountRegister
0.00% covered (danger)
0.00%
0 / 240
0.00% covered (danger)
0.00%
0 / 1
1406
 validateRegistration
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 apiDataChange
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 validateDataChange
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 createMediaDirForAccount
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 createProfileForAccount
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 createAccountFromRequest
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 apiAccountDelete
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 apiAccountUpdate
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 updateAccountFromRequest
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 apiModuleStatusUpdate
33.86% covered (danger)
33.86%
43 / 127
0.00% covered (danger)
0.00%
0 / 1
221.60
 apiAccountPermissionGet
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 apiGroupPermissionGet
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 apiGroupPermissionDelete
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 apiAccountPermissionDelete
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 apiAddGroupPermission
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 apiAddAccountPermission
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 createAccountModelPermission
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validatePermissionCreate
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 createPermissionFromRequest
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 apiAccountPermissionUpdate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 apiGroupPermissionUpdate
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 updatePermissionFromRequest
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 apiAddGroupToAccount
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 validateAddGroupToAccount
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 apiAddAccountToGroup
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 validateAddAccountToGroup
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 apiDeleteGroupFromAccount
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 apiDeleteAccountFromGroup
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 apiReInit
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 apiCheckForUpdates
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 apiUpdateFile
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 apiUpdate
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 downloadUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 runUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 apiContactCreate
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 validateContactCreate
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 createContactFromRequest
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 apiSettingsDelete
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 validateSettingsDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiApplicationUpdate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 updateApplicationFromRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateApplicationUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiApplicationDelete
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 validateApplicationDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 validateGroupPermissionUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 validateGroupPermissionDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 updateAccountPermissionFromRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateAccountPermissionUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 validateAccountPermissionDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiContactUpdate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 updateContactFromRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateContactUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiContactDelete
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 validateContactDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiDataChangeCreate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 createDataChangeFromRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateDataChangeCreate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiDataChangeDelete
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 validateDataChangeDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   Modules\Admin
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace Modules\Admin\Controller;
16
17use Model\Setting;
18use Model\SettingMapper;
19use Modules\Admin\Models\Account;
20use Modules\Admin\Models\AccountCredentialMapper;
21use Modules\Admin\Models\AccountMapper;
22use Modules\Admin\Models\AccountPermission;
23use Modules\Admin\Models\AccountPermissionMapper;
24use Modules\Admin\Models\App;
25use Modules\Admin\Models\AppMapper;
26use Modules\Admin\Models\Contact;
27use Modules\Admin\Models\ContactMapper;
28use Modules\Admin\Models\DataChange;
29use Modules\Admin\Models\DataChangeMapper;
30use Modules\Admin\Models\Group;
31use Modules\Admin\Models\GroupMapper;
32use Modules\Admin\Models\GroupPermission;
33use Modules\Admin\Models\GroupPermissionMapper;
34use Modules\Admin\Models\LocalizationMapper;
35use Modules\Admin\Models\Module;
36use Modules\Admin\Models\ModuleMapper;
37use Modules\Admin\Models\ModuleStatusUpdateType;
38use Modules\Admin\Models\NullAccount;
39use Modules\Admin\Models\PermissionCategory;
40use Modules\Admin\Models\SettingsEnum;
41use Modules\Media\Models\Collection;
42use Modules\Media\Models\CollectionMapper;
43use Modules\Media\Models\UploadFile;
44use Modules\Messages\Models\EmailMapper;
45use phpOMS\Account\AccountStatus;
46use phpOMS\Account\AccountType;
47use phpOMS\Account\GroupStatus;
48use phpOMS\Account\PermissionAbstract;
49use phpOMS\Account\PermissionOwner;
50use phpOMS\Account\PermissionType;
51use phpOMS\Application\ApplicationInfo;
52use phpOMS\Application\ApplicationManager;
53use phpOMS\Application\ApplicationType;
54use phpOMS\Auth\LoginReturnType;
55use phpOMS\DataStorage\Database\Query\Builder;
56use phpOMS\Localization\Localization;
57use phpOMS\Message\Http\HttpRequest;
58use phpOMS\Message\Http\HttpResponse;
59use phpOMS\Message\Http\RequestMethod;
60use phpOMS\Message\Http\RequestStatusCode;
61use phpOMS\Message\Http\Rest;
62use phpOMS\Message\Mail\Email;
63use phpOMS\Message\Mail\MailHandler;
64use phpOMS\Message\Mail\Smtp;
65use phpOMS\Message\Mail\SubmitType;
66use phpOMS\Message\NotificationLevel;
67use phpOMS\Message\RequestAbstract;
68use phpOMS\Message\ResponseAbstract;
69use phpOMS\Model\Message\FormValidation;
70use phpOMS\Model\Message\Reload;
71use phpOMS\Module\ModuleInfo;
72use phpOMS\Module\ModuleStatus;
73use phpOMS\Security\EncryptionHelper;
74use phpOMS\System\File\Local\File;
75use phpOMS\System\MimeType;
76use phpOMS\Uri\HttpUri;
77use phpOMS\Uri\UriFactory;
78use phpOMS\Utils\ArrayUtils;
79use phpOMS\Utils\Parser\Markdown\Markdown;
80use phpOMS\Utils\Parser\Php\ArrayParser;
81use phpOMS\Utils\RnG\StringUtils as StringRng;
82use phpOMS\Utils\StringUtils;
83use phpOMS\Validation\Network\Email as EmailValidator;
84use phpOMS\Version\Version;
85
86/**
87 * Admin controller class.
88 *
89 * This class is responsible for the basic admin activities such as managing accounts, groups, permissions and modules.
90 *
91 * @package Modules\Admin
92 * @license OMS License 2.0
93 * @link    https://jingga.app
94 * @since   1.0.0
95 */
96final class ApiController extends Controller
97{
98    /**
99     * Api method to login
100     *
101     * @param RequestAbstract  $request  Request
102     * @param ResponseAbstract $response Response
103     * @param array            $data     Generic data
104     *
105     * @return void
106     *
107     * @api
108     *
109     * @since 1.0.0
110     */
111    public function apiLogin(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
112    {
113        $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true);
114
115        $login = AccountMapper::login(
116            $request->getDataString('user') ?? '',
117            $request->getDataString('pass') ?? ''
118        );
119
120        if ($login > LoginReturnType::OK) {
121            $this->app->sessionManager->set('UID', $login, true);
122            $response->set($request->uri->__toString(), new Reload());
123        } elseif ($login === LoginReturnType::NOT_ACTIVATED) {
124            $response->header->status = RequestStatusCode::R_401;
125            $this->fillJsonResponse(
126                $request,
127                $response,
128                NotificationLevel::WARNING,
129                '',
130                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'NOT_ACTIVATED'),
131                null
132            );
133        } elseif ($login === LoginReturnType::WRONG_INPUT_EXCEEDED) {
134            $response->header->status = RequestStatusCode::R_401;
135            $this->fillJsonResponse(
136                $request,
137                $response,
138                NotificationLevel::WARNING,
139                '',
140                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'WRONG_INPUT_EXCEEDED'),
141                null
142            );
143        } else {
144            $response->header->status = RequestStatusCode::R_401;
145            $this->fillJsonResponse(
146                $request,
147                $response,
148                NotificationLevel::WARNING,
149                '',
150                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'LOGIN_ERROR'),
151                null
152            );
153        }
154    }
155
156    /**
157     * Api method to login
158     *
159     * @param RequestAbstract  $request  Request
160     * @param ResponseAbstract $response Response
161     * @param array            $data     Generic data
162     *
163     * @return void
164     *
165     * @api
166     *
167     * @since 1.0.0
168     */
169    public function apiLogout(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
170    {
171        $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true);
172
173        $this->app->sessionManager->remove('UID');
174        $this->app->sessionManager->save();
175
176        $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true);
177        $response->set($request->uri->__toString(), [
178            'status'   => NotificationLevel::OK,
179            'title'    => $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'LogoutSuccessfulTitle'),
180            'message'  => $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'LogoutSuccessfulMsg'),
181            'response' => null,
182        ]);
183    }
184
185    /**
186     * Create basic server mail handler
187     *
188     * @return MailHandler
189     *
190     * @since 1.0.0
191     **/
192    public function setUpServerMailHandler() : MailHandler
193    {
194        /** @var \Model\Setting[] $emailSettings */
195        $emailSettings = $this->app->appSettings->get(
196            names: [
197                SettingsEnum::MAIL_SERVER_OUT,
198                SettingsEnum::MAIL_SERVER_PORT_OUT,
199                SettingsEnum::MAIL_SERVER_TYPE,
200                SettingsEnum::MAIL_SERVER_USER,
201                SettingsEnum::MAIL_SERVER_PASS,
202                SettingsEnum::MAIL_SERVER_TLS,
203            ],
204            module: 'Admin'
205        );
206
207        if (empty($emailSettings)) {
208            /** @var \Model\Setting[] $emailSettings */
209            $emailSettings = $this->app->appSettings->get(
210                names: [
211                    SettingsEnum::MAIL_SERVER_OUT,
212                    SettingsEnum::MAIL_SERVER_PORT_OUT,
213                    SettingsEnum::MAIL_SERVER_TYPE,
214                    SettingsEnum::MAIL_SERVER_USER,
215                    SettingsEnum::MAIL_SERVER_PASS,
216                    SettingsEnum::MAIL_SERVER_TLS,
217                ],
218                module: 'Admin'
219            );
220        }
221
222        $handler = new MailHandler();
223        $handler->setMailer($emailSettings[SettingsEnum::MAIL_SERVER_TYPE]->content ?? SubmitType::MAIL);
224        $handler->useAutoTLS = (bool) ($emailSettings[SettingsEnum::MAIL_SERVER_TLS]->content ?? false);
225
226        if (($emailSettings[SettingsEnum::MAIL_SERVER_TYPE]->content ?? SubmitType::MAIL) === SubmitType::SMTP) {
227            $smtp          = new Smtp();
228            $handler->smtp = $smtp;
229        }
230
231        if (!empty($port = $emailSettings[SettingsEnum::MAIL_SERVER_PORT_OUT]->content)) {
232            $handler->port = (int) $port;
233        }
234
235        $handler->host     = $emailSettings[SettingsEnum::MAIL_SERVER_OUT]->content ?? 'localhost';
236        $handler->hostname = $emailSettings[SettingsEnum::MAIL_SERVER_OUT]->content ?? '';
237        $handler->username = $emailSettings[SettingsEnum::MAIL_SERVER_USER]->content ?? '';
238        $handler->password = $emailSettings[SettingsEnum::MAIL_SERVER_PASS]->isEncrypted && !empty($_SERVER['OMS_PRIVATE_KEY_I'] ?? '')
239            ? EncryptionHelper::decryptShared($emailSettings[SettingsEnum::MAIL_SERVER_PASS]->content ?? '', $_SERVER['OMS_PRIVATE_KEY_I'])
240            : $emailSettings[SettingsEnum::MAIL_SERVER_PASS]->content ?? '';
241
242        return $handler;
243    }
244
245    /**
246     * Api method to send forgotten password email
247     *
248     * @param RequestAbstract  $request  Request
249     * @param ResponseAbstract $response Response
250     * @param array            $data     Generic data
251     *
252     * @return void
253     *
254     * @api
255     *
256     * @since 1.0.0
257     */
258    public function apiForgot(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
259    {
260        /** @var \Modules\Admin\Models\Account $account */
261        $account = $request->hasData('user')
262            ? AccountMapper::get()->where('login', (string) $request->getData('user'))->execute()
263            : AccountMapper::get()->where('email', (string) $request->getData('email'))->execute();
264
265        /** @var \Model\Setting[] $forgotten */
266        $forgotten = $this->app->appSettings->get(
267            names: [SettingsEnum::LOGIN_FORGOTTEN_DATE, SettingsEnum::LOGIN_FORGOTTEN_COUNT],
268            module: 'Admin',
269            account: $account->id
270        );
271
272        if ((int) $forgotten[SettingsEnum::LOGIN_FORGOTTEN_COUNT]->content > 3) {
273            $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true);
274            $response->set($request->uri->__toString(), [
275                'status'   => NotificationLevel::ERROR,
276                'title'    => $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'PasswordResetTitle'),
277            'message'      => $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'PasswordResetMsg'),
278                'response' => null,
279            ]);
280        }
281
282        $token     = (string) \random_bytes(64);
283        $handler   = $this->setUpServerMailHandler();
284        $resetLink = UriFactory::build('{/base}/reset?user=' . $account->id . '&token=' . $token);
285
286        /** @var \Model\Setting[] $emailSettings */
287        $emailSettings = $this->app->appSettings->get(
288            names: [SettingsEnum::MAIL_SERVER_ADDR, SettingsEnum::LOGIN_MAIL_FORGOT_PASSWORD_TEMPLATE],
289            module: 'Admin'
290        );
291
292        /** @var \Modules\Messages\Models\Email $mail */
293        $mail = EmailMapper::get()
294            ->with('l11n')
295            ->where('id', (int) $emailSettings[SettingsEnum::LOGIN_MAIL_FORGOT_PASSWORD_TEMPLATE]->content)
296            ->where('l11n/language', $response->header->l11n->language)
297            ->execute();
298
299        $mail->setFrom($emailSettings[SettingsEnum::MAIL_SERVER_ADDR]->content);
300        $mail->addTo($account->email);
301
302        // @todo: load default l11n if no translation is available
303        $mailL11n = $mail->getL11nByLanguage($response->header->l11n->language);
304
305        $mail->subject = $mailL11n->subject;
306
307        // @todo: improve, the /tld link could be api.myurl.com which of course is not the url of the respective app.
308        // Maybe store the uri in the $app model? or store all urls in the config file
309        $mail->body = \str_replace(
310            [
311                '{reset_link}',
312                '{user_name}',
313            ],
314            [
315                $resetLink,
316                $account->login,
317            ],
318            $mailL11n->body
319        );
320
321        $mail->bodyAlt = \str_replace(
322            [
323                '{reset_link}',
324                '{user_name}',
325            ],
326            [
327                $resetLink,
328                $account->login,
329            ],
330            $mailL11n->bodyAlt
331        );
332
333        $handler->send($mail);
334
335        $this->app->appSettings->set([
336            [
337                'name'    => SettingsEnum::LOGIN_FORGOTTEN_DATE,
338                'module'  => self::NAME,
339                'account' => $account->id,
340                'content' => (string) \time(),
341            ],
342            [
343                'name'    => SettingsEnum::LOGIN_FORGOTTEN_COUNT,
344                'module'  => self::NAME,
345                'account' => $account->id,
346                'content' => (string) (((int) $forgotten[SettingsEnum::LOGIN_FORGOTTEN_COUNT]->content) + 1),
347            ],
348            [
349                'name'    => SettingsEnum::LOGIN_FORGOTTEN_TOKEN,
350                'module'  => self::NAME,
351                'account' => $account->id,
352                'content' => $token,
353            ],
354        ], true);
355
356        /*
357        if (!empty($emailSettings[SettingsEnum::MAIL_SERVER_CERT]->content)
358            && !empty($emailSettings[SettingsEnum::MAIL_SERVER_KEY]->content)
359        ) {
360            $mail->sign(
361                $emailSettings[SettingsEnum::MAIL_SERVER_CERT]->content,
362                $emailSettings[SettingsEnum::MAIL_SERVER_KEY]->content,
363                $emailSettings[SettingsEnum::MAIL_SERVER_KEYPASS]->content
364            );
365        }
366        */
367
368        // $handler->send($mail);
369
370        $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true);
371        $response->set($request->uri->__toString(), [
372            'status'   => NotificationLevel::OK,
373            'title'    => $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'PasswordResetTitle'),
374            'message'  => $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'PasswordResetEmailMsg'),
375            'response' => null,
376        ]);
377    }
378
379    /**
380     * Api method to reset the password
381     *
382     * @param RequestAbstract  $request  Request
383     * @param ResponseAbstract $response Response
384     * @param array            $data     Generic data
385     *
386     * @return void
387     *
388     * @api
389     *
390     * @since 1.0.0
391     */
392    public function apiResetPassword(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
393    {
394        /** @var \Model\Setting[] $forgotten */
395        $forgotten = $this->app->appSettings->get(
396            names: [SettingsEnum::LOGIN_FORGOTTEN_DATE, SettingsEnum::LOGIN_FORGOTTEN_TOKEN],
397            module: self::NAME,
398            account: (int) $request->getData('user')
399        );
400
401        $date  = new \DateTime($forgotten[SettingsEnum::LOGIN_FORGOTTEN_DATE]->content);
402        $token = $forgotten[SettingsEnum::LOGIN_FORGOTTEN_TOKEN]->content;
403
404        if ($date->getTimestamp() < \time() - 60 * 10
405            || !$request->hasData('token')
406            || $request->getData('token') !== $token
407        ) {
408            $response->header->status = RequestStatusCode::R_405;
409            $response->set($request->uri->__toString(), [
410                'status'   => NotificationLevel::OK,
411                'title'    => $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'PasswordResetTitle'),
412                'message'  => $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'PasswordResetInvalidMsg'),
413                'response' => null,
414            ]);
415
416            return;
417        }
418
419        /** @var \Modules\Admin\Models\Account $account */
420        $account = AccountMapper::get()->where('id', (int) $request->getData('user'))->execute();
421
422        $account->generatePassword($pass = StringRng::generateString(10, 14, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_-+=/\\{}<>?'));
423
424        AccountMapper::update()->execute($account);
425
426        $handler = $this->setUpServerMailHandler();
427
428        /** @var \Model\Setting[] $emailSettings */
429        $emailSettings = $this->app->appSettings->get(
430            names: [SettingsEnum::MAIL_SERVER_ADDR, SettingsEnum::LOGIN_MAIL_FORGOT_PASSWORD_TEMPLATE],
431            module: 'Admin'
432        );
433
434        /** @var \Modules\Messages\Models\Email $mail */
435        $mail = EmailMapper::get()
436            ->with('l11n')
437            ->where('id', (int) $emailSettings[SettingsEnum::LOGIN_MAIL_FORGOT_PASSWORD_TEMPLATE]->content)
438            ->where('l11n/language', $response->header->l11n->language)
439            ->execute();
440
441        $mail->setFrom($emailSettings[SettingsEnum::MAIL_SERVER_ADDR]->content);
442        $mail->addTo($account->email);
443
444        // @todo: load default l11n if no translation is available
445        $mailL11n = $mail->getL11nByLanguage($response->header->l11n->language);
446
447        $mail->subject = $mailL11n->subject;
448
449        // @todo: improve, the /tld link could be api.myurl.com which of course is not the url of the respective app.
450        // Maybe store the uri in the $app model? or store all urls in the config file
451        $mail->body = \str_replace(
452            [
453                '{new_password}',
454                '{user_name}',
455            ],
456            [
457                $pass,
458                $account->login,
459            ],
460            $mailL11n->body
461        );
462
463        $mail->bodyAlt = \str_replace(
464            [
465                '{new_password}',
466                '{user_name}',
467            ],
468            [
469                $pass,
470                $account->login,
471            ],
472            $mailL11n->bodyAlt
473        );
474
475        $handler->send($mail);
476
477        $this->app->appSettings->set([
478            [
479                'name'    => SettingsEnum::LOGIN_FORGOTTEN_COUNT,
480                'module'  => self::NAME,
481                'account' => $account->id,
482                'content' => '0',
483            ],
484            [
485                'name'    => SettingsEnum::LOGIN_FORGOTTEN_TOKEN,
486                'module'  => self::NAME,
487                'account' => $account->id,
488                'content' => '',
489            ],
490        ], true);
491
492        if (!empty($emailSettings[SettingsEnum::MAIL_SERVER_CERT]->content)
493            && !empty($emailSettings[SettingsEnum::MAIL_SERVER_KEY]->content)
494        ) {
495            $mail->sign(
496                $emailSettings[SettingsEnum::MAIL_SERVER_CERT]->content,
497                $emailSettings[SettingsEnum::MAIL_SERVER_KEY]->content,
498                $emailSettings[SettingsEnum::MAIL_SERVER_KEYPASS]->content
499            );
500        }
501
502        $handler->send($mail);
503
504        $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true);
505        $response->set($request->uri->__toString(), [
506            'status'   => NotificationLevel::OK,
507            'title'    => 'Password Reset',
508            'message'  => 'You received a new password.',
509            'response' => null,
510        ]);
511    }
512
513    /**
514     * Api method to get settings
515     *
516     * @param RequestAbstract  $request  Request
517     * @param ResponseAbstract $response Response
518     * @param array            $data     Generic data
519     *
520     * @return void
521     *
522     * @api
523     *
524     * @since 1.0.0
525     */
526    public function apiSettingsGet(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
527    {
528        $response->set(
529            $request->uri->__toString(),
530            [
531                'response' => $this->app->appSettings->get(
532                    $request->getDataInt('id'),
533                    $request->getDataString('name'),
534                    $request->getDataInt('unit'),
535                    $request->getDataInt('app'),
536                    $request->getDataString('module'),
537                    $request->getDataInt('group'),
538                    $request->getDataInt('account')
539                ),
540            ]
541        );
542    }
543
544    /**
545     * Set app config
546     *
547     * @param RequestAbstract  $request  Request
548     * @param ResponseAbstract $response Response
549     * @param array            $data     Generic data
550     *
551     * @return void
552     *
553     * @api
554     *
555     * @since 1.0.0
556     */
557    public function apiAppConfigSet(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
558    {
559        $dataSettings = $request->getDataJson('settings');
560
561        $config = include __DIR__ . '/../../../config.php';
562
563        foreach ($dataSettings as $data) {
564            $config = ArrayUtils::setArray($data['path'], $config, $data['value'], '/', true);
565        }
566
567        \file_put_contents(__DIR__ . '/../../../config.php', "<?php\ndeclare(strict_types=1);\nreturn " . ArrayParser::serializeArray($config) . ';');
568
569        $this->createStandardUpdateResponse($request, $response, $dataSettings);
570    }
571
572    /**
573     * Api method for modifying settings
574     *
575     * @param RequestAbstract  $request  Request
576     * @param ResponseAbstract $response Response
577     * @param array            $data     Generic data
578     *
579     * @return void
580     *
581     * @api
582     *
583     * @since 1.0.0
584     */
585    public function apiSettingsSet(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
586    {
587        $dataSettings = $request->getDataJson('settings');
588
589        foreach ($dataSettings as $data) {
590            $id        = isset($data['id']) && !empty($data['id']) ? (int) $data['id'] : null;
591            $name      = $data['name'] ?? null;
592            $content   = $data['content'] ?? null;
593            $unit      = $data['unit'] ?? null;
594            $app       = $data['app'] ?? null;
595            $module    = $data['module'] ?? null;
596            $pattern   = $data['pattern'] ?? '';
597            $encrypted = $data['encrypted'] ?? null;
598            $group     = isset($data['group']) ? (int) $data['group'] : null;
599            $account   = isset($data['account']) ? (int) $data['account'] : null;
600
601            /** @var \Model\Setting $old */
602            $old = $this->app->appSettings->get($id, $name, $unit, $app, $module, $group, $account);
603            if ($old->id === 0) {
604                $internalResponse = new HttpResponse();
605                $internalRequest  = new HttpRequest($request->uri);
606
607                $internalRequest->header->account = $request->header->account;
608                $internalRequest->setData('id', $id);
609                $internalRequest->setData('name', $name);
610                $internalRequest->setData('content', $content);
611                $internalRequest->setData('pattern', $pattern);
612                $internalRequest->setData('unit', $unit);
613                $internalRequest->setData('app', $app);
614                $internalRequest->setData('module', $module);
615                $internalRequest->setData('group', $group);
616                $internalRequest->setData('account', $account);
617                $internalRequest->setData('encrypted', $encrypted);
618
619                $this->apiSettingsCreate($internalRequest, $internalResponse, $data);
620
621                continue;
622            }
623
624            $new = clone $old;
625
626            $new->name        = $name ?? $new->name;
627            $new->isEncrypted = $encrypted ?? $new->isEncrypted;
628            $new->content     = $new->isEncrypted && !empty($content) && !empty($_SERVER['OMS_PRIVATE_KEY_I'] ?? '')
629                ? EncryptionHelper::encryptShared($content, $_SERVER['OMS_PRIVATE_KEY_I'])
630                : $content ?? $new->content;
631            $new->unit        = $unit ?? $new->unit;
632            $new->app         = $app ?? $new->app;
633            $new->module      = $module ?? $new->module;
634            $new->group       = $group ?? $new->group;
635            $new->account     = $account ?? $new->account;
636
637            // @todo: this function call seems stupid, it should just pass the $new object.
638            $this->app->appSettings->set([
639                [
640                    'id'          => $new->id,
641                    'name'        => $new->name,
642                    'content'     => $new->content,
643                    'unit'        => $new->unit,
644                    'app'         => $new->app,
645                    'module'      => $new->module,
646                    'group'       => $new->group,
647                    'account'     => $new->account,
648                    'isEncrypted' => $new->isEncrypted,
649                ],
650            ], false);
651
652            $this->updateModel($request->header->account, $old, $new, SettingMapper::class, 'settings', $request->getOrigin());
653        }
654
655        $this->createStandardUpdateResponse($request, $response, $dataSettings);
656    }
657
658    /**
659     * Api method for modifying settings
660     *
661     * @param RequestAbstract  $request  Request
662     * @param ResponseAbstract $response Response
663     * @param array            $data     Generic data
664     *
665     * @return void
666     *
667     * @api
668     *
669     * @since 1.0.0
670     */
671    public function apiSettingsCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
672    {
673        if (!empty($val = $this->validateSettingsCreate($request))) {
674            $response->header->status = RequestStatusCode::R_400;
675            $this->createInvalidCreateResponse($request, $response, $val);
676
677            return;
678        }
679
680        $setting = $this->createSettingFromRequest($request);
681        $this->createModel($request->header->account, $setting, SettingMapper::class, 'setting', $request->getOrigin());
682        $this->createStandardCreateResponse($request, $response, $setting);
683    }
684
685    /**
686     * Validate password update request
687     *
688     * @param RequestAbstract $request Request
689     *
690     * @return array<string, bool>
691     *
692     * @since 1.0.0
693     */
694    private function validateSettingsCreate(RequestAbstract $request) : array
695    {
696        $val = [];
697        if (($val['name'] = !$request->hasData('name'))) {
698            return $val;
699        }
700
701        return [];
702    }
703
704    /**
705     * Method to create group from request.
706     *
707     * @param RequestAbstract $request Request
708     *
709     * @return Setting
710     *
711     * @since 1.0.0
712     */
713    private function createSettingFromRequest(RequestAbstract $request) : Setting
714    {
715        return new Setting(
716            id: $request->getDataInt('id') ?? 0,
717            name: $request->getDataString('name') ?? '',
718            content: $request->getDataString('content') ?? '',
719            pattern: $request->getDataString('pattern') ?? '',
720            unit: $request->getDataInt('unit'),
721            app: $request->getDataInt('app'),
722            module: $request->getDataString('module'),
723            group: $request->getDataInt('group'),
724            account: $request->getDataInt('account'),
725            isEncrypted: $request->getDataBool('encrypted') ?? false
726        );
727    }
728
729    /**
730     * Api method for modifying account password
731     *
732     * @param RequestAbstract  $request  Request
733     * @param ResponseAbstract $response Response
734     * @param array            $data     Generic data
735     *
736     * @return void
737     *
738     * @api
739     *
740     * @since 1.0.0
741     */
742    public function apiSettingsAccountPasswordSet(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
743    {
744        // has required data
745        if (!empty($val = $this->validatePasswordUpdate($request))) {
746            $response->data['password_update'] = new FormValidation($val);
747            $response->header->status          = RequestStatusCode::R_400;
748
749            return;
750        }
751
752        $requestAccount = $request->header->account;
753
754        // request account is valid
755        if ($requestAccount <= 0) {
756            $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []);
757            $response->header->status = RequestStatusCode::R_403;
758
759            return;
760        }
761
762        /** @var Account $account */
763        $account = AccountMapper::get()
764            ->where('id', $requestAccount)
765            ->execute();
766
767        // test old password is correct
768        if ($account->login === null
769            || AccountMapper::login($account->login, (string) $request->getData('oldpass')) !== $requestAccount
770        ) {
771            $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []);
772            $response->header->status = RequestStatusCode::R_403;
773
774            return;
775        }
776
777        // test password repetition
778        if (((string) $request->getData('newpass')) !== ((string) $request->getData('reppass'))) {
779            $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []);
780            $response->header->status = RequestStatusCode::R_403;
781
782            return;
783        }
784
785        // test password complexity
786        /** @var \Model\Setting $complexity */
787        $complexity = $this->app->appSettings->get(names: SettingsEnum::PASSWORD_PATTERN, module: 'Admin');
788        if (\preg_match($complexity->content, (string) $request->getData('newpass')) !== 1) {
789            $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []);
790            $response->header->status = RequestStatusCode::R_403;
791
792            return;
793        }
794
795        $account->generatePassword((string) $request->getData('newpass'));
796
797        AccountMapper::update()->execute($account);
798
799        $this->createStandardUpdateResponse($request, $response, $account);
800    }
801
802    /**
803     * Validate password update request
804     *
805     * @param RequestAbstract $request Request
806     *
807     * @return array<string, bool>
808     *
809     * @since 1.0.0
810     */
811    private function validatePasswordUpdate(RequestAbstract $request) : array
812    {
813        $val = [];
814        if (($val['oldpass'] = !$request->hasData('oldpass'))
815            || ($val['newpass'] = !$request->hasData('newpass'))
816            || ($val['reppass'] = !$request->hasData('reppass'))
817        ) {
818            return $val;
819        }
820
821        return [];
822    }
823
824    /**
825     * Api method for modifying account localization
826     *
827     * @param RequestAbstract  $request  Request
828     * @param ResponseAbstract $response Response
829     * @param array            $data     Generic data
830     *
831     * @return void
832     *
833     * @api
834     *
835     * @since 1.0.0
836     */
837    public function apiSettingsAccountLocalizationSet(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
838    {
839        $requestAccount = $request->header->account;
840        $accountId      = (int) $request->getData('account_id');
841
842        if ($requestAccount !== $accountId
843            && !$this->app->accountManager->get($accountId)->hasPermission(
844                PermissionType::MODIFY,
845                $this->app->unitId,
846                $this->app->appId,
847                self::NAME,
848                PermissionCategory::ACCOUNT_SETTINGS,
849                $accountId
850            )
851        ) {
852            $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []);
853            $response->header->status = RequestStatusCode::R_403;
854
855            return;
856        }
857
858        /** @var \Modules\Admin\Models\Account $account */
859        $account = AccountMapper::get()
860            ->with('l11n')
861            ->where('id', $accountId)
862            ->execute();
863
864        if (($request->getDataString('localization_load') ?? '-1') !== '-1') {
865            $locale = \explode('_', $request->getDataString('localization_load') ?? '');
866            $old    = clone $account->l11n;
867
868            $account->l11n->loadFromLanguage($locale[0], $locale[1]);
869
870            $this->updateModel($request->header->account, $old, $account->l11n, LocalizationMapper::class, 'l11n', $request->getOrigin());
871            $this->createStandardUpdateResponse($request, $response, $account->l11n);
872
873            return;
874        }
875
876        $old = clone $account->l11n;
877
878        $dataSettings = $request->getLike('settings_(.*)');
879
880        $account->l11n->setCountry($dataSettings['settings_country']);
881        $account->l11n->setLanguage($dataSettings['settings_language']);
882        $account->l11n->setTemperature($dataSettings['settings_temperature']);
883
884        $account->l11n->setTimezone($dataSettings['settings_timezone']);
885        $account->l11n->setDatetime(
886            [
887                'very_short' => $dataSettings['settings_timeformat_vs'],
888                'short'      => $dataSettings['settings_timeformat_s'],
889                'medium'     => $dataSettings['settings_timeformat_m'],
890                'long'       => $dataSettings['settings_timeformat_l'],
891                'very_long'  => $dataSettings['settings_timeformat_vl'],
892            ]
893        );
894
895        $account->l11n->setCurrency($dataSettings['settings_currency']);
896        $account->l11n->setCurrencyFormat($dataSettings['settings_currencyformat']);
897
898        $account->l11n->setDecimal($dataSettings['settings_decimal']);
899        $account->l11n->setThousands($dataSettings['settings_thousands']);
900
901        $account->l11n->setPrecision(
902            [
903                'very_short' => $dataSettings['settings_precision_vs'],
904                'short'      => $dataSettings['settings_precision_s'],
905                'medium'     => $dataSettings['settings_precision_m'],
906                'long'       => $dataSettings['settings_precision_l'],
907                'very_long'  => $dataSettings['settings_precision_vl'],
908            ]
909        );
910
911        $account->l11n->setWeight(
912            [
913                'very_light' => $dataSettings['settings_weight_vl'],
914                'light'      => $dataSettings['settings_weight_l'],
915                'medium'     => $dataSettings['settings_weight_m'],
916                'heavy'      => $dataSettings['settings_weight_h'],
917                'very_heavy' => $dataSettings['settings_weight_vh'],
918            ]
919        );
920
921        $account->l11n->setSpeed(
922            [
923                'very_slow' => $dataSettings['settings_speed_vs'],
924                'slow'      => $dataSettings['settings_speed_s'],
925                'medium'    => $dataSettings['settings_speed_m'],
926                'fast'      => $dataSettings['settings_speed_f'],
927                'very_fast' => $dataSettings['settings_speed_vf'],
928                'sea'       => $dataSettings['settings_speed_sea'],
929            ]
930        );
931
932        $account->l11n->setLength(
933            [
934                'very_short' => $dataSettings['settings_length_vs'],
935                'short'      => $dataSettings['settings_length_s'],
936                'medium'     => $dataSettings['settings_length_m'],
937                'long'       => $dataSettings['settings_length_l'],
938                'very_long'  => $dataSettings['settings_length_vl'],
939                'sea'        => $dataSettings['settings_length_sea'],
940            ]
941        );
942
943        $account->l11n->setArea(
944            [
945                'very_small' => $dataSettings['settings_area_vs'],
946                'small'      => $dataSettings['settings_area_s'],
947                'medium'     => $dataSettings['settings_area_m'],
948                'large'      => $dataSettings['settings_area_l'],
949                'very_large' => $dataSettings['settings_area_vl'],
950            ]
951        );
952
953        $account->l11n->setVolume(
954            [
955                'very_small' => $dataSettings['settings_volume_vs'],
956                'small'      => $dataSettings['settings_volume_s'],
957                'medium'     => $dataSettings['settings_volume_m'],
958                'large'      => $dataSettings['settings_volume_l'],
959                'very_large' => $dataSettings['settings_volume_vl'],
960                'tablespoon' => $dataSettings['settings_volume_tablespoon'],
961                'teaspoon'   => $dataSettings['settings_volume_teaspoon'],
962                'glass'      => $dataSettings['settings_volume_glass'],
963            ]
964        );
965
966        $this->updateModel($request->header->account, $old, $account->l11n, LocalizationMapper::class, 'l11n', $request->getOrigin());
967        $this->createStandardUpdateResponse($request, $response, $account->l11n);
968    }
969
970    /**
971     * Routing end-point for application behaviour.
972     *
973     * @param RequestAbstract  $request  Request
974     * @param ResponseAbstract $response Response
975     * @param array            $data     Generic data
976     *
977     * @return void
978     *
979     * @api
980     *
981     * @since 1.0.0
982     */
983    public function apiSettingsDesignSet(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
984    {
985        $uploadedFiles = $request->files;
986
987        if (!empty($uploadedFiles)) {
988            $upload                   = new UploadFile();
989            $upload->preserveFileName = false;
990            $upload->outputDir        = __DIR__ . '/../../../Web/Backend/img';
991
992            $status = $upload->upload($uploadedFiles, ['logo.png'], true);
993        }
994
995        $this->createStandardUpdateResponse($request, $response, []);
996    }
997
998    /**
999     * Api method to install a application
1000     *
1001     * @param RequestAbstract  $request  Request
1002     * @param ResponseAbstract $response Response
1003     * @param array            $data     Generic data
1004     *
1005     * @return void
1006     *
1007     * @api
1008     *
1009     * @since 1.0.0
1010     */
1011    public function apiApplicationCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1012    {
1013        if (!empty($val = $this->validateApplicationCreate($request))) {
1014            $response->header->status = RequestStatusCode::R_400;
1015            $this->createInvalidCreateResponse($request, $response, $val);
1016
1017            return;
1018        }
1019
1020        $app = $this->createApplicationFromRequest($request);
1021        $this->createModel($request->header->account, $app, AppMapper::class, 'application', $request->getOrigin());
1022
1023        $this->createDefaultAppSettings($app, $request);
1024
1025        /** @var \Model\Setting $setting */
1026        $setting = $this->app->appSettings->get(null, SettingsEnum::GROUP_GENERATE_AUTOMATICALLY_APP);
1027        if ($setting->content === '1') {
1028            $newRequest                  = new HttpRequest();
1029            $newRequest->header->account = $request->header->account;
1030            $newRequest->setData('name', 'app:' . \strtolower($app->name));
1031            $newRequest->setData('status', GroupStatus::ACTIVE);
1032            $this->apiGroupCreate($newRequest, $response, $data);
1033        }
1034
1035        $this->createStandardCreateResponse($request, $response, $app);
1036    }
1037
1038    /**
1039     * Create default settings for new app
1040     *
1041     * @param App             $app     Application to create new settings for
1042     * @param RequestAbstract $request Request used to create the app
1043     *
1044     * @return void
1045     *
1046     * @since 1.0.0
1047     */
1048    private function createDefaultAppSettings(App $app, RequestAbstract $request) : void
1049    {
1050        $settings   = [];
1051        $settings[] = new Setting(0, SettingsEnum::REGISTRATION_ALLOWED, '0', '\\d+', app: $app->id, module: 'Admin');
1052        $settings[] = new Setting(0, SettingsEnum::APP_DEFAULT_GROUPS, '[]', app: $app->id, module: 'Admin');
1053
1054        foreach ($settings as $setting) {
1055            $this->createModel($request->header->account, $setting, SettingMapper::class, 'setting', $request->getOrigin());
1056        }
1057    }
1058
1059    /**
1060     * Validate app create request
1061     *
1062     * @param RequestAbstract $request Request
1063     *
1064     * @return array<string, bool>
1065     *
1066     * @since 1.0.0
1067     */
1068    private function validateApplicationCreate(RequestAbstract $request) : array
1069    {
1070        $val = [];
1071        if (($val['name'] = !$request->hasData('name'))) {
1072            return $val;
1073        }
1074
1075        return [];
1076    }
1077
1078    /**
1079     * Method to create task from request.
1080     *
1081     * @param RequestAbstract $request Request
1082     *
1083     * @return App Returns the created application from the request
1084     *
1085     * @since 1.0.0
1086     */
1087    private function createApplicationFromRequest(RequestAbstract $request) : App
1088    {
1089        $app              = new App();
1090        $app->name        = $request->getDataString('name') ?? '';
1091        $app->type        = $request->getDataInt('type') ?? ApplicationType::WEB;
1092        $app->defaultUnit = $request->getDataInt('default_unit');
1093
1094        return $app;
1095    }
1096
1097    /**
1098     * Api method to install a application
1099     *
1100     * @param RequestAbstract  $request  Request
1101     * @param ResponseAbstract $response Response
1102     * @param array            $data     Generic data
1103     *
1104     * @return void
1105     *
1106     * @api
1107     *
1108     * @since 1.0.0
1109     */
1110    public function apiInstallApplication(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1111    {
1112        $appManager = new ApplicationManager($this->app);
1113
1114        $app = \rtrim($request->getDataString('appSrc') ?? '', '/\\ ');
1115        if (!\is_dir(__DIR__ . '/../../../' . $app)) {
1116            $response->header->status = RequestStatusCode::R_400;
1117            return;
1118        }
1119
1120        $appInfo = new ApplicationInfo(__DIR__ . '/../../../' . $app . '/info.json');
1121        $appInfo->load();
1122
1123        // handle dependencies
1124        $dependencies = $appInfo->getDependencies();
1125        $installed    = $this->app->moduleManager->getInstalledModules();
1126
1127        foreach ($dependencies as $key => $version) {
1128            if (!isset($installed[$key])) {
1129                $this->app->moduleManager->install($key);
1130            }
1131        }
1132
1133        // handle app installation
1134        $result = $appManager->install(
1135            __DIR__ . '/../../../' . $app,
1136            __DIR__ . '/../../../' . ($request->getDataString('appDest') ?? ''),
1137            $request->getDataString('theme') ?? 'Default'
1138        );
1139
1140        // handle providing
1141        if ($result) {
1142            $providing = $appInfo->getProviding();
1143
1144            foreach ($providing as $key => $version) {
1145                if (isset($installed[$key])) {
1146                    $this->app->moduleManager->installProviding($app, $key);
1147                }
1148            }
1149        }
1150
1151        // handle Routes of already installed modules
1152        foreach ($installed as $module => $data) {
1153            $class = '\Modules\\' . $module . '\Admin\Status';
1154
1155            $moduleInfo = new ModuleInfo(__DIR__ . '/../../../Modules/' . $module . '/info.json');
1156            $moduleInfo->load();
1157
1158            $class::activateRoutes($moduleInfo, $appInfo);
1159            $class::activateHooks($moduleInfo, $appInfo);
1160        }
1161    }
1162
1163    /**
1164     * Api method to get a group
1165     *
1166     * @param RequestAbstract  $request  Request
1167     * @param ResponseAbstract $response Response
1168     * @param array            $data     Generic data
1169     *
1170     * @return void
1171     *
1172     * @api
1173     *
1174     * @since 1.0.0
1175     */
1176    public function apiGroupGet(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1177    {
1178        /** @var \Modules\Admin\Models\Group $group */
1179        $group = GroupMapper::get()->where('id', (int) $request->getData('id'))->execute();
1180        $this->createStandardReturnResponse($request, $response, $group);
1181    }
1182
1183    /**
1184     * Api method for modifying a group
1185     *
1186     * @param RequestAbstract  $request  Request
1187     * @param ResponseAbstract $response Response
1188     * @param array            $data     Generic data
1189     *
1190     * @return void
1191     *
1192     * @api
1193     *
1194     * @since 1.0.0
1195     */
1196    public function apiGroupUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1197    {
1198        /** @var \Modules\Admin\Models\Group $old */
1199        $old = GroupMapper::get()->where('id', (int) $request->getData('id'))->execute();
1200        $new = $this->updateGroupFromRequest($request, clone $old);
1201
1202        $this->updateModel($request->header->account, $old, $new, GroupMapper::class, 'group', $request->getOrigin());
1203        $this->createStandardUpdateResponse($request, $response, $new);
1204    }
1205
1206    /**
1207     * Method to update group from request.
1208     *
1209     * @param RequestAbstract $request Request
1210     *
1211     * @return Group
1212     *
1213     * @since 1.0.0
1214     */
1215    private function updateGroupFromRequest(RequestAbstract $request, Group $group) : Group
1216    {
1217        $group->name = $request->getDataString('name') ?? $group->name;
1218        $group->setStatus($request->getDataInt('status') ?? $group->getStatus());
1219        $group->description    = Markdown::parse($request->getDataString('description') ?? $group->descriptionRaw);
1220        $group->descriptionRaw = $request->getDataString('description') ?? $group->descriptionRaw;
1221
1222        return $group;
1223    }
1224
1225    /**
1226     * Validate group create request
1227     *
1228     * @param RequestAbstract $request Request
1229     *
1230     * @return array<string, bool>
1231     *
1232     * @since 1.0.0
1233     */
1234    private function validateGroupCreate(RequestAbstract $request) : array
1235    {
1236        $val = [];
1237        if (($val['name'] = !$request->hasData('name'))
1238            || ($val['status'] = !GroupStatus::isValidValue((int) $request->getData('status')))
1239        ) {
1240            return $val;
1241        }
1242
1243        return [];
1244    }
1245
1246    /**
1247     * Api method to create a group
1248     *
1249     * @param RequestAbstract  $request  Request
1250     * @param ResponseAbstract $response Response
1251     * @param array            $data     Generic data
1252     *
1253     * @return void
1254     *
1255     * @api
1256     *
1257     * @since 1.0.0
1258     */
1259    public function apiGroupCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1260    {
1261        if (!empty($val = $this->validateGroupCreate($request))) {
1262            $response->header->status = RequestStatusCode::R_400;
1263            $this->createInvalidCreateResponse($request, $response, $val);
1264
1265            return;
1266        }
1267
1268        $group = $this->createGroupFromRequest($request);
1269        $this->createModel($request->header->account, $group, GroupMapper::class, 'group', $request->getOrigin());
1270        $this->createStandardCreateResponse($request, $response, $group);
1271    }
1272
1273    /**
1274     * Method to create group from request.
1275     *
1276     * @param RequestAbstract $request Request
1277     *
1278     * @return Group
1279     *
1280     * @since 1.0.0
1281     */
1282    private function createGroupFromRequest(RequestAbstract $request) : Group
1283    {
1284        $group            = new Group();
1285        $group->createdBy = new NullAccount($request->header->account);
1286        $group->name      = $request->getDataString('name') ?? '';
1287        $group->setStatus($request->getDataInt('status') ?? GroupStatus::INACTIVE);
1288        $group->description    = Markdown::parse($request->getDataString('description') ?? '');
1289        $group->descriptionRaw = $request->getDataString('description') ?? '';
1290
1291        return $group;
1292    }
1293
1294    /**
1295     * Api method to delete a group
1296     *
1297     * @param RequestAbstract  $request  Request
1298     * @param ResponseAbstract $response Response
1299     * @param array            $data     Generic data
1300     *
1301     * @return void
1302     *
1303     * @api
1304     *
1305     * @since 1.0.0
1306     */
1307    public function apiGroupDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1308    {
1309        if (!empty($val = $this->validateGroupDelete($request))) {
1310            $response->header->status = RequestStatusCode::R_400;
1311            $this->createInvalidDeleteResponse($request, $response, $val);
1312
1313            return;
1314        }
1315
1316        if (((int) $request->getData('id')) === 3) {
1317            // admin group cannot be deleted
1318            $this->createInvalidDeleteResponse($request, $response, []);
1319
1320            return;
1321        }
1322
1323        /** @var \Modules\Admin\Models\Group $group */
1324        $group = GroupMapper::get()->where('id', (int) $request->getData('id'))->execute();
1325        $this->deleteModel($request->header->account, $group, GroupMapper::class, 'group', $request->getOrigin());
1326        $this->createStandardDeleteResponse($request, $response, $group);
1327    }
1328
1329    /**
1330     * Validate Group delete request
1331     *
1332     * @param RequestAbstract $request Request
1333     *
1334     * @return array<string, bool>
1335     *
1336     * @todo: implement
1337     *
1338     * @since 1.0.0
1339     */
1340    private function validateGroupDelete(RequestAbstract $request) : array
1341    {
1342        $val = [];
1343        if (($val['id'] = !$request->hasData('id'))) {
1344            return $val;
1345        }
1346
1347        return [];
1348    }
1349
1350    /**
1351     * Api method to find groups
1352     *
1353     * @param RequestAbstract  $request  Request
1354     * @param ResponseAbstract $response Response
1355     * @param array            $data     Generic data
1356     *
1357     * @return void
1358     *
1359     * @api
1360     *
1361     * @since 1.0.0
1362     */
1363    public function apiGroupFind(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1364    {
1365        /** @var \Modules\Admin\Models\Group[] $groups */
1366        $groups = GroupMapper::getAll()
1367            ->where('name', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE')
1368            ->execute();
1369
1370        $response->header->set('Content-Type', MimeType::M_JSON, true);
1371        $response->set(
1372            $request->uri->__toString(),
1373            \array_values($groups)
1374        );
1375    }
1376
1377    /**
1378     * Api method to get an accoung
1379     *
1380     * @param RequestAbstract  $request  Request
1381     * @param ResponseAbstract $response Response
1382     * @param array            $data     Generic data
1383     *
1384     * @return void
1385     *
1386     * @api
1387     *
1388     * @since 1.0.0
1389     */
1390    public function apiAccountGet(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1391    {
1392        /** @var Account $account */
1393        $account = AccountMapper::get()->where('id', (int) $request->getData('id'))->execute();
1394        $this->createStandardReturnResponse($request, $response, $account);
1395    }
1396
1397    /**
1398     * Api method to find accounts
1399     *
1400     * @param RequestAbstract  $request  Request
1401     * @param ResponseAbstract $response Response
1402     * @param array            $data     Generic data
1403     *
1404     * @return void
1405     *
1406     * @api
1407     *
1408     * @since 1.0.0
1409     */
1410    public function apiAccountFind(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1411    {
1412        /** @var \Modules\Admin\Models\Account[] $accounts */
1413        $accounts =  AccountMapper::getAll()
1414            ->where('login', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE')
1415            ->where('email', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE', 'OR')
1416            ->where('name1', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE', 'OR')
1417            ->where('name2', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE', 'OR')
1418            ->where('name3', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE', 'OR')
1419            ->execute();
1420
1421        $response->header->set('Content-Type', MimeType::M_JSON, true);
1422        $response->set(
1423            $request->uri->__toString(),
1424            \array_values($accounts)
1425        );
1426    }
1427
1428    /**
1429     * Api method to find accounts and or groups
1430     *
1431     * @param RequestAbstract  $request  Request
1432     * @param ResponseAbstract $response Response
1433     * @param array            $data     Generic data
1434     *
1435     * @return void
1436     *
1437     * @api
1438     *
1439     * @since 1.0.0
1440     */
1441    public function apiAccountGroupFind(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1442    {
1443        /** @var Account[] $accounts */
1444        $accounts = AccountMapper::getAll()
1445            ->where('login', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE')
1446            ->where('email', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE', 'OR')
1447            ->where('name1', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE', 'OR')
1448            ->where('name2', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE', 'OR')
1449            ->where('name3', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE', 'OR')
1450            ->execute();
1451
1452        $data = [];
1453
1454        foreach ($accounts as $account) {
1455            /** @var array $temp */
1456            $temp                = $account->jsonSerialize();
1457            $temp['type_prefix'] = 'a';
1458            $temp['type_name']   = 'Account';
1459
1460            $data[] = $temp;
1461        }
1462
1463        /** @var Group[] $groups */
1464        $groups = GroupMapper::getAll()
1465            ->where('name', '%' . ($request->getDataString('search') ?? '') . '%', 'LIKE')
1466            ->execute();
1467
1468        foreach ($groups as $group) {
1469            /** @var array $temp */
1470            $temp                = $group->jsonSerialize();
1471            $temp['name']        = [$temp['name']];
1472            $temp['email']       = '---';
1473            $temp['type_prefix'] = 'g';
1474            $temp['type_name']   = 'Group';
1475
1476            $data[] = $temp;
1477        }
1478
1479        $response->header->set('Content-Type', MimeType::M_JSON, true);
1480        $response->set($request->uri->__toString(), $data);
1481    }
1482
1483    /**
1484     * Method to validate account creation from request
1485     *
1486     * @param RequestAbstract $request Request
1487     *
1488     * @return array<string, bool>
1489     *
1490     * @since 1.0.0
1491     */
1492    private function validateAccountCreate(RequestAbstract $request) : array
1493    {
1494        $val = [];
1495        if (($val['name1'] = !$request->hasData('name1'))
1496            || ($val['type'] = !AccountType::isValidValue((int) $request->getData('type')))
1497            || ($val['status'] = !AccountStatus::isValidValue((int) $request->getData('status')))
1498            || ($val['email'] = $request->hasData('email') && !EmailValidator::isValid((string) $request->getData('email')))
1499        ) {
1500            return $val;
1501        }
1502
1503        return [];
1504    }
1505
1506    /**
1507     * Api method to create an account
1508     *
1509     * @param RequestAbstract  $request  Request
1510     * @param ResponseAbstract $response Response
1511     * @param array            $data     Generic data
1512     *
1513     * @return void
1514     *
1515     * @api
1516     *
1517     * @since 1.0.0
1518     */
1519    public function apiAccountCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1520    {
1521        if (!empty($val = $this->validateAccountCreate($request))) {
1522            $response->header->status = RequestStatusCode::R_400;
1523            $this->createInvalidCreateResponse($request, $response, $val);
1524
1525            return;
1526        }
1527
1528        $account = $this->createAccountFromRequest($request);
1529        $this->createModel($request->header->account, $account, AccountCredentialMapper::class, 'account', $request->getOrigin());
1530
1531        if ($request->hasData('create_profile')) {
1532            $this->createProfileForAccount($account, $request);
1533        }
1534
1535        $collection = $this->createMediaDirForAccount($account->id, $account->login ?? '', $request->header->account);
1536        $this->createModel($request->header->account, $collection, CollectionMapper::class, 'collection', $request->getOrigin());
1537
1538        // find default groups and create them
1539        $defaultGroupIds = [];
1540
1541        if ($request->hasData('app')) {
1542            /** @var \Model\Setting $defaultGroupSettings */
1543            $defaultGroupSettings = $this->app->appSettings->get(
1544                names: SettingsEnum::APP_DEFAULT_GROUPS,
1545                app:  (int) $request->getData('app'),
1546                module: 'Admin'
1547            );
1548
1549            if (!empty($defaultGroupSettings)) {
1550                $temp = \json_decode($defaultGroupSettings->content, true);
1551                if (!\is_array($temp)) {
1552                    $temp = [];
1553                }
1554
1555                $defaultGroupIds = \array_merge($defaultGroupIds, $temp);
1556            }
1557        }
1558
1559        if ($request->hasData('unit')) {
1560            /** @var \Model\Setting $defaultGroupSettings */
1561            $defaultGroupSettings = $this->app->appSettings->get(
1562                names: SettingsEnum::UNIT_DEFAULT_GROUPS,
1563                unit: (int) $request->getData('unit'),
1564                module: 'Admin'
1565            );
1566
1567            if (!empty($defaultGroupSettings)) {
1568                $temp = \json_decode($defaultGroupSettings->content, true);
1569                if (!\is_array($temp)) {
1570                    $temp = [];
1571                }
1572
1573                $defaultGroupIds = \array_merge($defaultGroupIds, $temp);
1574            }
1575        }
1576
1577        if (!empty($defaultGroupIds)) {
1578            $this->createModelRelation(
1579                $account->id,
1580                $account->id,
1581                $defaultGroupIds,
1582                AccountMapper::class,
1583                'groups',
1584                'account',
1585                $request->getOrigin()
1586            );
1587        }
1588
1589        $this->fillJsonResponse(
1590            $request,
1591            $response,
1592            NotificationLevel::OK,
1593            '',
1594            \str_replace(
1595                '{url}',
1596                UriFactory::build('{/base}/admin/account/settings?{?}&id=' . $account->id),
1597                $this->app->l11nManager->getText($response->header->l11n->language, '0', '0', 'SuccessfulCreate'
1598            )),
1599            $account
1600        );
1601    }
1602
1603    /**
1604     * Api method to register an account
1605     *
1606     * @param RequestAbstract  $request  Request
1607     * @param ResponseAbstract $response Response
1608     * @param array            $data     Generic data
1609     *
1610     * @return void
1611     *
1612     * @api
1613     *
1614     * @since 1.0.0
1615     */
1616    public function apiAccountRegister(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1617    {
1618        if ($request->header->account === 0) {
1619            $request->header->account = 1;
1620        }
1621
1622        if (!empty($val = $this->validateRegistration($request))) {
1623            $response->header->status = RequestStatusCode::R_400;
1624
1625            $this->fillJsonResponse(
1626                $request,
1627                $response,
1628                NotificationLevel::ERROR,
1629                '',
1630                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'FormDataInvalid'),
1631                $val
1632            );
1633
1634            return;
1635        }
1636
1637        /** @var \Modules\Admin\Models\App */
1638        $app = AppMapper::get()
1639            ->where('id', (int) $request->getData('app'))
1640            ->execute();
1641
1642        /** @var \Model\Setting $allowed */
1643        $allowed = $this->app->appSettings->get(
1644            names: SettingsEnum::REGISTRATION_ALLOWED,
1645            app: (int) $request->getData('app'),
1646            module: 'Admin'
1647        );
1648
1649        if ($allowed->content !== '1') {
1650            $response->header->status = RequestStatusCode::R_400;
1651
1652            $this->fillJsonResponse(
1653                $request,
1654                $response,
1655                NotificationLevel::ERROR,
1656                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationTitle'),
1657                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationNotAllowed'),
1658                []
1659            );
1660
1661            $response->header->status = RequestStatusCode::R_400;
1662
1663            return;
1664        }
1665
1666        /** @var \Model\Setting $complexity */
1667        $complexity = $this->app->appSettings->get(names: SettingsEnum::PASSWORD_PATTERN, module: 'Admin');
1668        if ($request->hasData('password')
1669            && \preg_match($complexity->content, (string) $request->getData('password')) !== 1
1670        ) {
1671            $response->header->status = RequestStatusCode::R_400;
1672
1673            $this->fillJsonResponse(
1674                $request,
1675                $response,
1676                NotificationLevel::ERROR,
1677                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationTitle'),
1678                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationInvalidPasswordFormat'),
1679                []
1680            );
1681
1682            $response->header->status = RequestStatusCode::R_403;
1683
1684            return;
1685        }
1686
1687        // Check if account already exists
1688        /** @var Account $emailAccount */
1689        $emailAccount = AccountMapper::get()
1690            ->where('email', (string) $request->getData('email'))
1691            ->execute();
1692
1693        /** @var Account $loginAccount */
1694        $loginAccount = AccountMapper::get()
1695            ->where('login', (string) ($request->getData('user') ?? $request->getData('email')))
1696            ->execute();
1697
1698        /** @var null|Account $account */
1699        $account = null;
1700
1701        // email already in use
1702        if ($emailAccount->id > 0
1703            && $emailAccount->login !== null
1704            && AccountMapper::login($emailAccount->login, (string) $request->getData('password')) !== LoginReturnType::OK
1705        ) {
1706            $response->header->status = RequestStatusCode::R_400;
1707
1708            $this->fillJsonResponse(
1709                $request,
1710                $response,
1711                NotificationLevel::OK,
1712                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationTitle'),
1713                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationEmailInUse'),
1714                []
1715            );
1716
1717            $response->header->status = RequestStatusCode::R_400;
1718
1719            return;
1720        } elseif ($emailAccount->id > 0) {
1721            $account = $emailAccount;
1722        }
1723
1724        // login already in use by different email
1725        if ($account === null
1726            && $loginAccount->id > 0
1727            && $loginAccount->getEmail() !== $request->getData('email')
1728        ) {
1729            $response->header->status = RequestStatusCode::R_400;
1730
1731            $this->fillJsonResponse(
1732                $request,
1733                $response,
1734                NotificationLevel::ERROR,
1735                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationTitle'),
1736                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationLoginInUse'),
1737                []
1738            );
1739
1740            $response->header->status = RequestStatusCode::R_400;
1741
1742            return;
1743        } elseif ($account === null
1744            && $loginAccount->id > 0
1745            && $loginAccount->login !== null
1746            && AccountMapper::login($loginAccount->login, (string) $request->getData('password')) !== LoginReturnType::OK
1747        ) {
1748            $account = $loginAccount;
1749        }
1750
1751        // Already registered
1752        if ($account !== null) {
1753            /** @var Account $account */
1754            $account = AccountMapper::get()
1755                ->with('groups')
1756                ->where('id', $account->id)
1757                ->execute();
1758
1759            $defaultGroupIds = [];
1760
1761            if ($request->hasData('app')) {
1762                /** @var \Model\Setting $defaultGroupSettings */
1763                $defaultGroupSettings = $this->app->appSettings->get(
1764                    names: SettingsEnum::APP_DEFAULT_GROUPS,
1765                    app:  (int) $request->getDataInt('app'),
1766                    module: 'Admin'
1767                );
1768
1769                if (!empty($defaultGroupSettings)) {
1770                    $temp = \json_decode($defaultGroupSettings->content, true);
1771                    if (!\is_array($temp)) {
1772                        $temp = [];
1773                    }
1774
1775                    $defaultGroupIds = \array_merge(
1776                        $defaultGroupIds,
1777                        $temp
1778                    );
1779                }
1780            }
1781
1782            if ($request->hasData('unit')) {
1783                /** @var \Model\Setting $defaultGroupSettings */
1784                $defaultGroupSettings = $this->app->appSettings->get(
1785                    names: SettingsEnum::UNIT_DEFAULT_GROUPS,
1786                    app:  (int) $request->getDataInt('unit'),
1787                    module: 'Admin'
1788                );
1789
1790                if (!empty($defaultGroupSettings)) {
1791                    $temp = \json_decode($defaultGroupSettings->content, true);
1792                    if (!\is_array($temp)) {
1793                        $temp = [];
1794                    }
1795
1796                    $defaultGroupIds = \array_merge(
1797                        $defaultGroupIds,
1798                        $temp
1799                    );
1800                }
1801            }
1802
1803            foreach ($defaultGroupIds as $index => $id) {
1804                if ($account->hasGroup($id)) {
1805                    unset($defaultGroupIds[$index]);
1806                }
1807            }
1808
1809            if (empty($defaultGroupIds)
1810                && $account->getStatus() === AccountStatus::INACTIVE
1811            ) {
1812                $response->header->status = RequestStatusCode::R_400;
1813
1814                // Account not active
1815                $this->fillJsonResponse(
1816                    $request,
1817                    $response,
1818                    NotificationLevel::ERROR,
1819                    $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationTitle'),
1820                    $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationNotActivated'),
1821                    []
1822                );
1823
1824                $response->header->status = RequestStatusCode::R_403;
1825
1826                return;
1827            }
1828
1829            if (!empty($defaultGroupIds)) {
1830                // Create missing account / group relationships
1831                $this->createModelRelation(
1832                    $account->id,
1833                    $account->id,
1834                    $defaultGroupIds,
1835                    AccountMapper::class,
1836                    'groups',
1837                    'registration',
1838                    $request->getOrigin()
1839                );
1840            }
1841        } else {
1842            // New account
1843            $request->setData('status', AccountStatus::INACTIVE, true);
1844            $request->setData('type', AccountType::USER, true);
1845            $request->setData('create_profile', (string) true);
1846            $request->setData('name1', $request->hasData('name1')
1847                ? ($request->getDataString('name1')
1848                )
1849                : ($request->hasData('user')
1850                    ? $request->getDataString('user')
1851                    : \explode('@', $request->getDataString('email') ?? '')[0])
1852                , true);
1853
1854            $request->setData('user', $request->hasData('user')
1855                ? $request->getDataString('user')
1856                : $request->getDataString('email')
1857                , true);
1858
1859            $this->apiAccountCreate($request, $response, $data);
1860
1861            /** @var Account $account */
1862            $account = $response->getDataArray($request->uri->__toString())['response'];
1863
1864            // Create confirmation pending entry
1865            $dataChange            = new DataChange();
1866            $dataChange->type      = 'account';
1867            $dataChange->createdBy = $account->id;
1868            $dataChange->data      = '{"status": ' . AccountStatus::ACTIVE . '}';
1869
1870            $tries = 0;
1871            do {
1872                $dataChange->reHash();
1873                $this->createModel($account->id, $dataChange, DataChangeMapper::class, 'datachange', $request->getOrigin());
1874
1875                ++$tries;
1876            } while ($dataChange->id === 0 && $tries < 5);
1877
1878            $handler = $this->setUpServerMailHandler();
1879
1880            /** @var \Model\Setting[] $emailSettings */
1881            $emailSettings = $this->app->appSettings->get(
1882                names: [SettingsEnum::MAIL_SERVER_ADDR, SettingsEnum::LOGIN_MAIL_REGISTRATION_TEMPLATE],
1883                module: 'Admin'
1884            );
1885
1886            /** @var \Modules\Messages\Models\Email $mail */
1887            $mail = EmailMapper::get()
1888                ->with('l11n')
1889                ->where('id', (int) $emailSettings[SettingsEnum::LOGIN_MAIL_REGISTRATION_TEMPLATE]->content)
1890                ->where('l11n/language', $response->header->l11n->language)
1891                ->execute();
1892
1893            $mail->setFrom($emailSettings[SettingsEnum::MAIL_SERVER_ADDR]->content);
1894            $mail->addTo((string) $request->getData('email'));
1895
1896            // @todo: load default l11n if no translation is available
1897            $mailL11n = $mail->getL11nByLanguage($response->header->l11n->language);
1898
1899            $mail->subject = $mailL11n->subject;
1900
1901            // @todo: improve, the /tld link could be api.myurl.com which of course is not the url of the respective app.
1902            // Maybe store the uri in the $app model? or store all urls in the config file
1903            $mail->body = \str_replace(
1904                [
1905                    '{confirmation_link}',
1906                    '{user_name}',
1907                ],
1908                [
1909                    UriFactory::hasQuery('/' . \strtolower($app->name))
1910                        ? UriFactory::build('{/' . \strtolower($app->name) . '}/' . \strtolower($app->name) . '/signup/confirmation?hash=' . $dataChange->getHash())
1911                        : UriFactory::build('{/tld}/{/lang}/' . \strtolower($app->name) . '/signup/confirmation?hash=' . $dataChange->getHash()),
1912                    $account->login,
1913                ],
1914                $mailL11n->body
1915            );
1916
1917            $mail->bodyAlt = \str_replace(
1918                [
1919                    '{confirmation_link}',
1920                    '{user_name}',
1921                ],
1922                [
1923                    UriFactory::hasQuery('/' . \strtolower($app->name))
1924                        ? UriFactory::build('{/' . \strtolower($app->name) . '}/' . \strtolower($app->name) . '/signup/confirmation?hash=' . $dataChange->getHash())
1925                        : UriFactory::build('{/tld}/{/lang}/' . \strtolower($app->name) . '/signup/confirmation?hash=' . $dataChange->getHash()),
1926                    $account->login,
1927                ],
1928                $mailL11n->bodyAlt
1929            );
1930
1931            $handler->send($mail);
1932        }
1933
1934        // Create client
1935        if ($request->hasData('client')) {
1936            $client = $this->app->moduleManager->get('ClientManagement', 'Api')
1937                ->findClientForAccount($account->id, $request->getDataInt('unit'));
1938
1939            if ($client === null) {
1940                $internalRequest  = new HttpRequest();
1941                $internalResponse = new HttpResponse();
1942
1943                $internalRequest->header->account = $account->id;
1944                $internalRequest->setData('account', $account->id);
1945                $internalRequest->setData('number', 100000 + $account->id);
1946                $internalRequest->setData('address', $request->getDataString('address') ?? '');
1947                $internalRequest->setData('postal', $request->getDataString('postal') ?? '');
1948                $internalRequest->setData('city', $request->getDataString('city') ?? '');
1949                $internalRequest->setData('country', $request->getDataString('country'));
1950                $internalRequest->setData('state', $request->getDataString('state') ?? '');
1951                $internalRequest->setData('vat_id', $request->getDataString('vat_id') ?? '');
1952                $internalRequest->setData('unit', $request->getDataInt('unit'));
1953
1954                $this->app->moduleManager->get('ClientManagement', 'Api')->apiClientCreate($internalRequest, $internalResponse);
1955            }
1956        }
1957
1958        $this->fillJsonResponse(
1959            $request,
1960            $response,
1961            NotificationLevel::OK,
1962            $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationTitle'),
1963            $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'RegistrationSuccessful'),
1964            $account
1965        );
1966    }
1967
1968    /**
1969     * Method to validate account registration from request
1970     *
1971     * @param RequestAbstract $request Request
1972     *
1973     * @return array<string, bool>
1974     *
1975     * @since 1.0.0
1976     */
1977    private function validateRegistration(RequestAbstract $request) : array
1978    {
1979        $val = [];
1980        if (($val['email'] = $request->hasData('email')
1981                && !EmailValidator::isValid((string) $request->getData('email')))
1982            || ($val['unit'] = !$request->hasData('unit'))
1983            || ($val['app'] = !$request->hasData('app'))
1984            || ($val['password'] = !$request->hasData('password'))
1985        ) {
1986            return $val;
1987        }
1988
1989        return [];
1990    }
1991
1992    /**
1993     * Api method to perform a pending model change
1994     *
1995     * @param RequestAbstract  $request  Request
1996     * @param ResponseAbstract $response Response
1997     * @param array            $data     Generic data
1998     *
1999     * @return void
2000     *
2001     * @api
2002     * @todo: maybe move to job/workflow??? This feels very much like a job/event especially if we make the 'type' an event-trigger
2003     *
2004     * @since 1.0.0
2005     */
2006    public function apiDataChange(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2007    {
2008        if (!empty($val = $this->validateDataChange($request))) {
2009            $response->data['data_change'] = new FormValidation($val);
2010            $response->header->status      = RequestStatusCode::R_400;
2011
2012            return;
2013        }
2014
2015        /** @var DataChange $dataChange */
2016        $dataChange = DataChangeMapper::get()->where('hash', (string) $request->getData('hash'))->execute();
2017        if ($dataChange->id === 0) {
2018            $response->header->status = RequestStatusCode::R_400;
2019
2020            return;
2021        }
2022
2023        switch ($dataChange->type) {
2024            case 'account':
2025                /** @var Account $old */
2026                $old = AccountMapper::get()->where('id', $dataChange->createdBy)->execute();
2027                $new = clone $old;
2028
2029                $data = \json_decode($dataChange->data, true);
2030                if (!\is_array($data)) {
2031                    break;
2032                }
2033
2034                $new->setStatus((int) ($data['status'] ?? -1));
2035
2036                $this->updateModel($dataChange->createdBy, $old, $new, AccountMapper::class, 'datachange', $request->getOrigin());
2037                $this->deleteModel($dataChange->createdBy, $dataChange, DataChangeMapper::class, 'datachange', $request->getOrigin());
2038
2039                break;
2040        }
2041    }
2042
2043    /**
2044     * Method to validate account registration from request
2045     *
2046     * @param RequestAbstract $request Request
2047     *
2048     * @return array<string, bool>
2049     *
2050     * @since 1.0.0
2051     */
2052    private function validateDataChange(RequestAbstract $request) : array
2053    {
2054        $val = [];
2055        if (($val['hash'] = !$request->hasData('hash'))) {
2056            return $val;
2057        }
2058
2059        return [];
2060    }
2061
2062    /**
2063     * Create directory for an account
2064     *
2065     * @param int    $id        Account id
2066     * @param string $name      Name of the directory/account
2067     * @param int    $createdBy Creator of the directory
2068     *
2069     * @return Collection
2070     *
2071     * @since 1.0.0
2072     */
2073    private function createMediaDirForAccount(int $id, string $name, int $createdBy) : Collection
2074    {
2075        $collection       = new Collection();
2076        $collection->name = ((string) $id) . ' ' . $name;
2077        $collection->setVirtualPath('/Accounts');
2078        $collection->setPath('/Modules/Media/Files/Accounts/' . ((string) $id));
2079        $collection->createdBy = new NullAccount($createdBy);
2080
2081        return $collection;
2082    }
2083
2084    /**
2085     * Create profile for account
2086     *
2087     * @param Account         $account Account to create profile for
2088     * @param RequestAbstract $request Request
2089     *
2090     * @return void
2091     *
2092     * @api
2093     *
2094     * @since 1.0.0
2095     */
2096    private function createProfileForAccount(Account $account, RequestAbstract $request) : void
2097    {
2098        // @todo: why do we need the following lines?
2099        if (($request->getDataString('password') ?? '') === ''
2100            || ($request->getDataString('user') ?? '') === ''
2101        ) {
2102            return;
2103        }
2104
2105        $request->setData('iaccount-idlist', $account->id);
2106
2107        $internalResponse = new HttpResponse();
2108
2109        $this->app->moduleManager->get('Profile', 'Api')->apiProfileCreate($request, $internalResponse);
2110    }
2111
2112    /**
2113     * Method to create an account from a request
2114     *
2115     * @param RequestAbstract $request Request
2116     *
2117     * @return Account
2118     *
2119     * @since 1.0.0
2120     */
2121    private function createAccountFromRequest(RequestAbstract $request) : Account
2122    {
2123        $account        = new Account();
2124        $account->login = $request->getDataString('user') ?? '';
2125        $account->name1 = $request->getDataString('name1') ?? '';
2126        $account->name2 = $request->getDataString('name2') ?? '';
2127        $account->name3 = $request->getDataString('name3') ?? '';
2128        $account->setStatus($request->getDataInt('status') ?? AccountStatus::INACTIVE);
2129        $account->setType($request->getDataInt('type') ?? AccountType::USER);
2130        $account->setEmail($request->getDataString('email') ?? '');
2131        $account->generatePassword($request->getDataString('password') ?? '');
2132
2133        if (!$request->hasData('locale')) {
2134            $account->l11n = Localization::fromJson(
2135                    $this->app->l11nServer === null
2136                        ? $request->header->l11n->toArray()
2137                        : $this->app->l11nServer->toArray()
2138                );
2139        } else {
2140            $locale = \explode('_', $request->getdataString('locale') ?? '');
2141
2142            $account->l11n
2143                ->loadFromLanguage(
2144                    $locale[0] ?? $this->app->l11nServer->language,
2145                    $locale[1] ?? $this->app->l11nServer->country
2146                );
2147        }
2148
2149        return $account;
2150    }
2151
2152    /**
2153     * Api method to delete an account
2154     *
2155     * @param RequestAbstract  $request  Request
2156     * @param ResponseAbstract $response Response
2157     * @param array            $data     Generic data
2158     *
2159     * @return void
2160     *
2161     * @api
2162     *
2163     * @since 1.0.0
2164     */
2165    public function apiAccountDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2166    {
2167        /** @var Account $account */
2168        $account = AccountMapper::get()->where('id', (int) $request->getData('id'))->execute();
2169        $this->deleteModel($request->header->account, $account, AccountMapper::class, 'account', $request->getOrigin());
2170        $this->createStandardDeleteResponse($request, $response, $account);
2171    }
2172
2173    /**
2174     * Api method to update an account
2175     *
2176     * @param RequestAbstract  $request  Request
2177     * @param ResponseAbstract $response Response
2178     * @param array            $data     Generic data
2179     *
2180     * @return void
2181     *
2182     * @api
2183     *
2184     * @since 1.0.0
2185     */
2186    public function apiAccountUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2187    {
2188        /** @var Account $old */
2189        $old = AccountMapper::get()
2190            ->where('id', (int) $request->getData('id'))
2191            ->execute();
2192
2193        $new = $this->updateAccountFromRequest($request, clone $old);
2194        $this->updateModel($request->header->account, $old, $new, AccountMapper::class, 'account', $request->getOrigin());
2195        $this->createStandardUpdateResponse($request, $response, $new);
2196    }
2197
2198    /**
2199     * Method to update an account from a request
2200     *
2201     * @param RequestAbstract $request       Request
2202     * @param Account         $account       Account
2203     * @param bool            $allowPassword Allow to change password
2204     *
2205     * @return Account
2206     *
2207     * @since 1.0.0
2208     */
2209    private function updateAccountFromRequest(RequestAbstract $request, Account $account, bool $allowPassword = false) : Account
2210    {
2211        $account->login = $request->getDataString('user') ?? $account->login;
2212        $account->name1 = $request->getDataString('name1') ?? $account->name1;
2213        $account->name2 = $request->getDataString('name2') ?? $account->name2;
2214        $account->name3 = $request->getDataString('name3') ?? $account->name3;
2215        $account->setEmail($request->getDataString('email') ?? $account->getEmail());
2216        $account->setStatus($request->getDataInt('status') ?? $account->getStatus());
2217        $account->setType($request->getDataInt('type') ?? $account->getType());
2218
2219        if ($allowPassword && $request->hasData('password')) {
2220            $account->generatePassword((string) $request->getData('password'));
2221        }
2222
2223        return $account;
2224    }
2225
2226    /**
2227     * Api method to update the module settigns
2228     *
2229     * @param RequestAbstract  $request  Request
2230     * @param ResponseAbstract $response Response
2231     * @param array            $data     Generic data
2232     *
2233     * @return void
2234     *
2235     * @api
2236     *
2237     * @since 1.0.0
2238     */
2239    public function apiModuleStatusUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2240    {
2241        $module = $request->getDataString('module') ?? '';
2242        $status = (int) $request->getData('status');
2243
2244        if (empty($module) || empty($status)) {
2245            $this->fillJsonResponse(
2246                $request,
2247                $response,
2248                NotificationLevel::WARNING,
2249                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleStatusTitle'),
2250                $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'UnknownModuleOrStatusChange'),
2251                []
2252            );
2253
2254            $response->header->status = RequestStatusCode::R_403;
2255            return;
2256        }
2257
2258        /** @var \Modules\Admin\Models\Module $old */
2259        $old = ModuleMapper::get()->where('id', $module)->execute();
2260
2261        $this->app->eventManager->triggerSimilar(
2262            'PRE:Module:Admin-module-status-update', '',
2263            [
2264                $request->header->account,
2265                ['status' => $status, 'module' => $module],
2266            ]
2267        );
2268        switch ($status) {
2269            case ModuleStatusUpdateType::ACTIVATE:
2270                $done = $module === 'Admin' ? false : $this->app->moduleManager->activate($module);
2271                $msg  = $done
2272                    ? $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleActivatedSuccessful')
2273                    : $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleActivatedFailure');
2274
2275                $new = clone $old;
2276                $new->setStatus(ModuleStatusUpdateType::ACTIVATE);
2277                ModuleMapper::update()->execute($new);
2278
2279                break;
2280            case ModuleStatusUpdateType::DEACTIVATE:
2281                $done = $module === 'Admin' ? false : $this->app->moduleManager->deactivate($module);
2282                $msg  = $done
2283                    ? $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleDeactivatedSuccessful')
2284                    : $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleDeactivatedFailure');
2285
2286                $new = clone $old;
2287                $new->setStatus(ModuleStatusUpdateType::DEACTIVATE);
2288                ModuleMapper::update()->execute($new);
2289
2290                break;
2291            case ModuleStatusUpdateType::INSTALL:
2292                $done = $this->app->moduleManager->isInstalled($module);
2293                $msg  = $done
2294                    ? $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleInstalledSuccessful')
2295                    : $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleInstalledFailure');
2296
2297                if ($done) {
2298                    break;
2299                }
2300
2301                if (!\is_file(__DIR__ . '/../../../Modules/' . $module . '/info.json')) {
2302                    $msg  = $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'UnknownModuleChange');
2303                    $done = false;
2304                    break;
2305                }
2306
2307                $moduleInfo = new ModuleInfo(__DIR__ . '/../../../Modules/' . $module . '/info.json');
2308                $moduleInfo->load();
2309
2310                // install dependencies
2311                $dependencies = $moduleInfo->getDependencies();
2312                foreach ($dependencies as $key => $_) {
2313                    $iResponse                 = new HttpResponse();
2314                    $iRequest                  = new HttpRequest(new HttpUri(''));
2315                    $iRequest->header->account = 1;
2316                    $iRequest->setData('status', ModuleStatusUpdateType::INSTALL);
2317                    $iRequest->setData('module', $key);
2318
2319                    $this->apiModuleStatusUpdate($iRequest, $iResponse);
2320                }
2321
2322                // install module
2323                $moduleObj          = new Module();
2324                $moduleObj->id      = $module;
2325                $moduleObj->theme   = 'Default';
2326                $moduleObj->path    = $moduleInfo->getDirectory();
2327                $moduleObj->version = $moduleInfo->getVersion();
2328                $moduleObj->name    = $moduleInfo->getExternalName();
2329
2330                $moduleObj->setStatus(ModuleStatus::AVAILABLE);
2331
2332                $this->createModel($request->header->account, $moduleObj, ModuleMapper::class, 'module', $request->getOrigin());
2333
2334                $done = $this->app->moduleManager->install($module);
2335                $msg  = $done
2336                    ? $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleInstalledSuccessful')
2337                    : $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleInstalledFailure');
2338
2339                $old = clone $moduleObj;
2340                $moduleObj->setStatus(ModuleStatus::ACTIVE);
2341
2342                $this->updateModel($request->header->account, $old, $moduleObj, ModuleMapper::class, 'module', $request->getOrigin());
2343
2344                $queryLoad = new Builder($this->app->dbPool->get('insert'));
2345                $queryLoad->insert('module_load_pid', 'module_load_type', 'module_load_from', 'module_load_for', 'module_load_file')
2346                    ->into('module_load');
2347
2348                $load = $moduleInfo->getLoad();
2349                foreach ($load as $val) {
2350                    foreach ($val['pid'] as $pid) {
2351                        $queryLoad->values(
2352                            \sha1(\str_replace('/', '', $pid)),
2353                            (int) $val['type'],
2354                            $val['from'],
2355                            $val['for'],
2356                            $val['file']
2357                        );
2358                    }
2359                }
2360
2361                if (!empty($queryLoad->getValues())) {
2362                    $queryLoad->execute();
2363                }
2364
2365                // install receiving from application (receiving from module is already installed during the module installation)
2366                $appManager = new ApplicationManager($this->app);
2367                $receiving  = $appManager->getProvidingForModule($module);
2368                foreach ($receiving as $app => $modules) {
2369                    foreach ($modules as $module) {
2370                        $this->app->moduleManager->installProviding('/Web/' . $app, $module);
2371                    }
2372                }
2373
2374                break;
2375            case ModuleStatusUpdateType::UNINSTALL:
2376                $done = $module === 'Admin' ? false : $this->app->moduleManager->uninstall($module);
2377                $msg  = $done
2378                    ? $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleUninstalledSuccessful')
2379                    : $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleUninstalledFailure');
2380
2381                $new = clone $old;
2382                $new->setStatus(ModuleStatusUpdateType::DELETE);
2383                ModuleMapper::delete()->execute($new);
2384
2385                break;
2386            default:
2387                $done                     = false;
2388                $msg                      = $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'UnknownModuleStatusChange');
2389                $response->header->status = RequestStatusCode::R_400;
2390        }
2391
2392        if ($done) {
2393            $new = ModuleMapper::get()->where('id', $module)->execute();
2394
2395            $this->app->eventManager->triggerSimilar(
2396                'POST:Module:Admin-module-status-update', '',
2397                [
2398                    $request->header->account,
2399                    $old, $new,
2400                    StringUtils::intHash(ModuleMapper::class), 'module-status',
2401                    self::NAME,
2402                    $module,
2403                    '',
2404                    $request->getOrigin(),
2405                ]
2406            );
2407        } else {
2408            $response->header->status = RequestStatusCode::R_400;
2409        }
2410
2411        $this->fillJsonResponse(
2412            $request,
2413            $response,
2414            $done ? NotificationLevel::OK : NotificationLevel::WARNING,
2415            $this->app->l11nManager->getText($response->header->l11n->language, 'Admin', 'Api', 'ModuleStatusTitle'),
2416            $msg,
2417            []
2418        );
2419    }
2420
2421    /**
2422     * Api method to get a user permission
2423     *
2424     * @param RequestAbstract  $request  Request
2425     * @param ResponseAbstract $response Response
2426     * @param array            $data     Generic data
2427     *
2428     * @return void
2429     *
2430     * @api
2431     *
2432     * @since 1.0.0
2433     */
2434    public function apiAccountPermissionGet(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2435    {
2436        /** @var AccountPermission $account */
2437        $account = AccountPermissionMapper::get()->where('id', (int) $request->getData('id'))->execute();
2438        $this->createStandardReturnResponse($request, $response, $account);
2439    }
2440
2441    /**
2442     * Api method to get a group permission
2443     *
2444     * @param RequestAbstract  $request  Request
2445     * @param ResponseAbstract $response Response
2446     * @param array            $data     Generic data
2447     *
2448     * @return void
2449     *
2450     * @api
2451     *
2452     * @since 1.0.0
2453     */
2454    public function apiGroupPermissionGet(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2455    {
2456        /** @var GroupPermission $group */
2457        $group = GroupPermissionMapper::get()->where('id', (int) $request->getData('id'))->execute();
2458        $this->createStandardReturnResponse($request, $response, $group);
2459    }
2460
2461    /**
2462     * Api method to delete a group permission
2463     *
2464     * @param RequestAbstract  $request  Request
2465     * @param ResponseAbstract $response Response
2466     * @param array            $data     Generic data
2467     *
2468     * @return void
2469     *
2470     * @api
2471     *
2472     * @since 1.0.0
2473     */
2474    public function apiGroupPermissionDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2475    {
2476        if (!empty($val = $this->validateGroupPermissionDelete($request))) {
2477            $response->header->status = RequestStatusCode::R_400;
2478            $this->createInvalidDeleteResponse($request, $response, $val);
2479
2480            return;
2481        }
2482
2483        /** @var GroupPermission $permission */
2484        $permission = GroupPermissionMapper::get()->where('id', (int) $request->getData('id'))->execute();
2485
2486        if ($permission->getGroup() === 3) {
2487            // admin group cannot be deleted
2488            $this->createInvalidDeleteResponse($request, $response, []);
2489
2490            return;
2491        }
2492
2493        $this->deleteModel($request->header->account, $permission, GroupPermissionMapper::class, 'group-permission', $request->getOrigin());
2494        $this->createStandardDeleteResponse($request, $response, $permission);
2495    }
2496
2497    /**
2498     * Api method to delete a user permission
2499     *
2500     * @param RequestAbstract  $request  Request
2501     * @param ResponseAbstract $response Response
2502     * @param array            $data     Generic data
2503     *
2504     * @return void
2505     *
2506     * @api
2507     *
2508     * @since 1.0.0
2509     */
2510    public function apiAccountPermissionDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2511    {
2512        if (!empty($val = $this->validateAccountPermissionDelete($request))) {
2513            $response->header->status = RequestStatusCode::R_400;
2514            $this->createInvalidDeleteResponse($request, $response, $val);
2515
2516            return;
2517        }
2518
2519        /** @var AccountPermission $permission */
2520        $permission = AccountPermissionMapper::get()->where('id', (int) $request->getData('id'))->execute();
2521        $this->deleteModel($request->header->account, $permission, AccountPermissionMapper::class, 'user-permission', $request->getOrigin());
2522        $this->createStandardDeleteResponse($request, $response, $permission);
2523    }
2524
2525    /**
2526     * Api method to add a permission to a group
2527     *
2528     * @param RequestAbstract  $request  Request
2529     * @param ResponseAbstract $response Response
2530     * @param array            $data     Generic data
2531     *
2532     * @return void
2533     *
2534     * @api
2535     *
2536     * @since 1.0.0
2537     */
2538    public function apiAddGroupPermission(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2539    {
2540        if (!empty($val = $this->validatePermissionCreate($request))) {
2541            $response->header->status = RequestStatusCode::R_400;
2542            $this->createInvalidCreateResponse($request, $response, $val);
2543
2544            return;
2545        }
2546
2547        if (((int) $request->getData('permissionref')) === 3) {
2548            // admin group cannot be deleted
2549            $this->createInvalidUpdateResponse($request, $response, []);
2550
2551            return;
2552        }
2553
2554        $permission = $this->createPermissionFromRequest($request);
2555
2556        if (!($permission instanceof GroupPermission)) {
2557            $response->data['permission_create'] = new FormValidation($val);
2558            $response->header->status            = RequestStatusCode::R_400;
2559
2560            return;
2561        }
2562
2563        $this->createModel($request->header->account, $permission, GroupPermissionMapper::class, 'group-permission', $request->getOrigin());
2564        $this->createStandardCreateResponse($request, $response, $permission);
2565    }
2566
2567    /**
2568     * Api method to add a permission to a account
2569     *
2570     * @param RequestAbstract  $request  Request
2571     * @param ResponseAbstract $response Response
2572     * @param array            $data     Generic data
2573     *
2574     * @return void
2575     *
2576     * @api
2577     *
2578     * @since 1.0.0
2579     */
2580    public function apiAddAccountPermission(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2581    {
2582        if (!empty($val = $this->validatePermissionCreate($request))) {
2583            $response->header->status = RequestStatusCode::R_400;
2584            $this->createInvalidCreateResponse($request, $response, $val);
2585
2586            return;
2587        }
2588
2589        $permission = $this->createPermissionFromRequest($request);
2590
2591        if (!($permission instanceof AccountPermission)) {
2592            $response->data['permission_create'] = new FormValidation($val);
2593            $response->header->status            = RequestStatusCode::R_400;
2594
2595            return;
2596        }
2597
2598        $this->createModel($request->header->account, $permission, AccountPermissionMapper::class, 'account-permission', $request->getOrigin());
2599        $this->createStandardCreateResponse($request, $response, $permission);
2600    }
2601
2602    /**
2603     * Api method to add a permission to a account-model combination
2604     *
2605     * @param PermissionAbstract $permission Permission to create for account-model combination
2606     * @param int                $account    Account creating this model
2607     * @param string             $ip         Ip
2608     *
2609     * @return void
2610     *
2611     * @api
2612     *
2613     * @since 1.0.0
2614     */
2615    public function createAccountModelPermission(PermissionAbstract $permission, int $account, string $ip) : void
2616    {
2617        $this->createModel($account, $permission, AccountPermissionMapper::class, 'account-permission', $ip);
2618    }
2619
2620    /**
2621     * Validate permission create request
2622     *
2623     * @param RequestAbstract $request Request
2624     *
2625     * @return array<string, bool>
2626     *
2627     * @since 1.0.0
2628     */
2629    private function validatePermissionCreate(RequestAbstract $request) : array
2630    {
2631        $val = [];
2632        if (($val['permissionowner'] = !PermissionOwner::isValidValue((int) $request->getData('permissionowner')))
2633            || ($val['permissionref'] = !\is_numeric($request->getData('permissionref')))
2634        ) {
2635            return $val;
2636        }
2637
2638        return [];
2639    }
2640
2641    /**
2642     * Method to create a permission from request.
2643     *
2644     * @param RequestAbstract $request Request
2645     *
2646     * @return AccountPermission|GroupPermission
2647     *
2648     * @since 1.0.0
2649     */
2650    public function createPermissionFromRequest(RequestAbstract $request) : PermissionAbstract
2651    {
2652        /** @var AccountPermission|GroupPermission $permission */
2653        $permission = ($request->getDataInt('permissionowner')) === PermissionOwner::GROUP
2654            ? new GroupPermission((int) $request->getData('permissionref'))
2655            : new AccountPermission((int) $request->getData('permissionref'));
2656
2657        $permission->unit      = $request->getDataInt('permissionunit');
2658        $permission->app       = $request->getDataInt('permissionapp');
2659        $permission->module    = $request->getDataString('permissionmodule');
2660        $permission->category  = $request->getDataInt('permissioncategory');
2661        $permission->element   = $request->getDataInt('permissionelement');
2662        $permission->component = $request->getDataInt('permissioncomponent');
2663        $permission->setPermission(
2664            ($request->getDataInt('permissionread') ?? 0)
2665                | ($request->getDataInt('permissioncreate') ?? 0)
2666                | ($request->getDataInt('permissionupdate') ?? 0)
2667                | ($request->getDataInt('permissiondelete') ?? 0)
2668                | ($request->getDataInt('permissionpermission') ?? 0)
2669        );
2670
2671        return $permission;
2672    }
2673
2674    /**
2675     * Api method to update a account permission
2676     *
2677     * @param RequestAbstract  $request  Request
2678     * @param ResponseAbstract $response Response
2679     * @param array            $data     Generic data
2680     *
2681     * @return void
2682     *
2683     * @api
2684     *
2685     * @since 1.0.0
2686     */
2687    public function apiAccountPermissionUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2688    {
2689        if (!empty($val = $this->validateAccountPermissionUpdate($request))) {
2690            $response->header->status = RequestStatusCode::R_400;
2691            $this->createInvalidUpdateResponse($request, $response, $val);
2692
2693            return;
2694        }
2695
2696        /** @var AccountPermission $old */
2697        $old = AccountPermissionMapper::get()->where('id', (int) $request->getData('id'))->execute();
2698
2699        /** @var AccountPermission $new */
2700        $new = $this->updatePermissionFromRequest($request, clone $old);
2701
2702        $this->updateModel($request->header->account, $old, $new, AccountPermissionMapper::class, 'account-permission', $request->getOrigin());
2703        $this->createStandardUpdateResponse($request, $response, $new);
2704    }
2705
2706    /**
2707     * Api method to update a group permission
2708     *
2709     * @param RequestAbstract  $request  Request
2710     * @param ResponseAbstract $response Response
2711     * @param array            $data     Generic data
2712     *
2713     * @return void
2714     *
2715     * @api
2716     *
2717     * @since 1.0.0
2718     */
2719    public function apiGroupPermissionUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2720    {
2721        if (!empty($val = $this->validateGroupPermissionUpdate($request))) {
2722            $response->header->status = RequestStatusCode::R_400;
2723            $this->createInvalidUpdateResponse($request, $response, $val);
2724
2725            return;
2726        }
2727
2728        /** @var GroupPermission $old */
2729        $old = GroupPermissionMapper::get()->where('id', (int) $request->getData('id'))->execute();
2730
2731        if ($old->getGroup() === 3) {
2732            // admin group cannot be deleted
2733            $this->createInvalidUpdateResponse($request, $response, []);
2734
2735            return;
2736        }
2737
2738        /** @var GroupPermission $new */
2739        $new = $this->updatePermissionFromRequest($request, clone $old);
2740
2741        $this->updateModel($request->header->account, $old, $new, GroupPermissionMapper::class, 'group-permission', $request->getOrigin());
2742        $this->createStandardUpdateResponse($request, $response, $new);
2743    }
2744
2745    /**
2746     * Method to update a group permission from a request
2747     *
2748     * @param RequestAbstract    $request    Request
2749     * @param PermissionAbstract $permission Permission model
2750     *
2751     * @return PermissionAbstract
2752     *
2753     * @since 1.0.0
2754     */
2755    private function updatePermissionFromRequest(RequestAbstract $request, PermissionAbstract $permission) : PermissionAbstract
2756    {
2757        $permission->unit      = $request->getDataInt('permissionunit') ?? $permission->unit;
2758        $permission->app       = $request->getDataInt('permissionapp') ?? $permission->app;
2759        $permission->module    = $request->getDataString('permissionmodule') ?? $permission->module;
2760        $permission->category  = $request->getDataInt('permissioncategory') ?? $permission->category;
2761        $permission->element   = $request->getDataInt('permissionelement') ?? $permission->element;
2762        $permission->component = $request->getDataInt('permissioncomponent') ?? $permission->component;
2763        $permission->setPermission(($request->getDataInt('permissioncreate') ?? 0)
2764            | ($request->getDataInt('permissionread') ?? 0)
2765            | ($request->getDataInt('permissionupdate') ?? 0)
2766            | ($request->getDataInt('permissiondelete') ?? 0)
2767            | ($request->getDataInt('permissionpermission') ?? 0));
2768
2769        return $permission;
2770    }
2771
2772    /**
2773     * Api method to add a group to an account
2774     *
2775     * @param RequestAbstract  $request  Request
2776     * @param ResponseAbstract $response Response
2777     * @param array            $data     Generic data
2778     *
2779     * @return void
2780     *
2781     * @api
2782     *
2783     * @since 1.0.0
2784     */
2785    public function apiAddGroupToAccount(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2786    {
2787        if (!empty($val = $this->validateAddGroupToAccount($request))) {
2788            $response->header->status = RequestStatusCode::R_400;
2789            $this->createInvalidAddResponse($request, $response, $val);
2790
2791            return;
2792        }
2793
2794        $account = (int) $request->getData('account');
2795        $groups  = [$request->getDataInt('account-list') ?? 0];
2796
2797        $this->createModelRelation($request->header->account, $account, $groups, AccountMapper::class, 'groups', 'account-group', $request->getOrigin());
2798        $this->createStandardAddResponse($request, $response, $groups);
2799    }
2800
2801    /**
2802     * Validate adding a group to an account request
2803     *
2804     * @param RequestAbstract $request Request
2805     *
2806     * @return array<string, bool>
2807     *
2808     * @since 1.0.0
2809     */
2810    private function validateAddGroupToAccount(RequestAbstract $request) : array
2811    {
2812        $val = [];
2813        if (($val['account'] = !$request->hasData('account'))
2814            || ($val['accountlist'] = !$request->hasData('account-list'))
2815        ) {
2816            return $val;
2817        }
2818
2819        return [];
2820    }
2821
2822    /**
2823     * Api method to add an account to a group
2824     *
2825     * @param RequestAbstract  $request  Request
2826     * @param ResponseAbstract $response Response
2827     * @param array            $data     Generic data
2828     *
2829     * @return void
2830     *
2831     * @api
2832     *
2833     * @since 1.0.0
2834     */
2835    public function apiAddAccountToGroup(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2836    {
2837        if (!empty($val = $this->validateAddAccountToGroup($request))) {
2838            $response->header->status = RequestStatusCode::R_400;
2839            $this->createInvalidAddResponse($request, $response, $val);
2840
2841            return;
2842        }
2843
2844        $group    = (int) $request->getData('group');
2845        $accounts = [$request->getDataInt('group-list') ?? 0];
2846
2847        $this->createModelRelation($request->header->account, $group, $accounts, GroupMapper::class, 'accounts', 'group-account', $request->getOrigin());
2848        $this->createStandardAddResponse($request, $response, $accounts);
2849    }
2850
2851    /**
2852     * Validate adding an account to a group request
2853     *
2854     * @param RequestAbstract $request Request
2855     *
2856     * @return array<string, bool>
2857     *
2858     * @since 1.0.0
2859     */
2860    private function validateAddAccountToGroup(RequestAbstract $request) : array
2861    {
2862        $val = [];
2863        if (($val['group'] = !$request->hasData('group'))
2864            || ($val['grouplist'] = !$request->hasData('group-list'))
2865        ) {
2866            return $val;
2867        }
2868
2869        return [];
2870    }
2871
2872    /**
2873     * Api method to add a group to an account
2874     *
2875     * @param RequestAbstract  $request  Request
2876     * @param ResponseAbstract $response Response
2877     * @param array            $data     Generic data
2878     *
2879     * @return void
2880     *
2881     * @api
2882     *
2883     * @since 1.0.0
2884     */
2885    public function apiDeleteGroupFromAccount(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2886    {
2887        $account = (int) $request->getData('account');
2888        $groups  = \array_map('intval', $request->getDataList('igroup-idlist'));
2889
2890        if (\in_array(3, $groups) && $account === $request->header->account) {
2891            // admin group cannot be deleted
2892            $this->createInvalidRemoveResponse($request, $response, []);
2893
2894            return;
2895        }
2896
2897        $this->deleteModelRelation($request->header->account, $account, $groups, AccountMapper::class, 'groups', 'account-group', $request->getOrigin());
2898        $this->createStandardRemoveResponse($request, $response, $groups);
2899    }
2900
2901    /**
2902     * Api method to add an account to a group
2903     *
2904     * @param RequestAbstract  $request  Request
2905     * @param ResponseAbstract $response Response
2906     * @param array            $data     Generic data
2907     *
2908     * @return void
2909     *
2910     * @api
2911     *
2912     * @since 1.0.0
2913     */
2914    public function apiDeleteAccountFromGroup(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2915    {
2916        $group    = (int) $request->getData('group');
2917        $accounts = \array_map('intval', $request->getDataList('iaccount-idlist'));
2918
2919        if (\in_array($request->header->account, $accounts) && $group === 3) {
2920            // admin group cannot be deleted
2921            $this->createInvalidRemoveResponse($request, $response, []);
2922
2923            return;
2924        }
2925
2926        $this->deleteModelRelation($request->header->account, $group, $accounts, GroupMapper::class, 'accounts', 'group-account', $request->getOrigin());
2927        $this->createStandardRemoveResponse($request, $response, $accounts);
2928    }
2929
2930    /**
2931     * Api re-init routes
2932     *
2933     * @param RequestAbstract  $request  Request
2934     * @param ResponseAbstract $response Response
2935     * @param array            $data     Generic data
2936     *
2937     * @return void
2938     *
2939     * @api
2940     *
2941     * @since 1.0.0
2942     */
2943    public function apiReInit(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2944    {
2945        $directories = \glob(__DIR__ . '/../../../Web/*', \GLOB_ONLYDIR);
2946
2947        if ($directories !== false) {
2948            foreach ($directories as $directory) {
2949                if (\is_file($path = $directory . '/Routes.php')) {
2950                    \file_put_contents($path, '<?php return [];');
2951                }
2952
2953                if (\is_file($path = $directory . '/Hooks.php')) {
2954                    \file_put_contents($path, '<?php return [];');
2955                }
2956            }
2957        }
2958
2959        if (\is_file($path = __DIR__ . '/../../../Cli/Routes.php')) {
2960            \file_put_contents($path, '<?php return [];');
2961        }
2962
2963        if (\is_file($path = __DIR__ . '/../../../Socket/Routes.php')) {
2964            \file_put_contents($path, '<?php return [];');
2965        }
2966
2967        $installedModules = $this->app->moduleManager->getActiveModules();
2968        foreach ($installedModules as $name => $module) {
2969            $this->app->moduleManager->reInit($name);
2970        }
2971    }
2972
2973    /**
2974     * Api check for updates
2975     *
2976     * @param RequestAbstract  $request  Request
2977     * @param ResponseAbstract $response Response
2978     * @param array            $data     Generic data
2979     *
2980     * @return void
2981     *
2982     * @api
2983     *
2984     * @since 1.0.0
2985     */
2986    public function apiCheckForUpdates(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
2987    {
2988        // this is only a temp... in the future this logic will change but for current purposes this is the easiest way to implement updates
2989        $request = new HttpRequest(new HttpUri('https://api.github.com/repos/Karaka/Updates/contents'));
2990        $request->setMethod(RequestMethod::GET);
2991        $request->header->set('User-Agent', 'spl1nes');
2992
2993        $updateFilesJson = Rest::request($request)->getJsonData();
2994
2995        /** @var array<string, array<string, mixed>> */
2996        $toUpdate = [];
2997
2998        foreach ($updateFilesJson as $file) {
2999            $name = \explode('_', $file['name']);
3000            $path = '';
3001
3002            if (\is_dir(__DIR__ . '/../../../' . $name[0])) {
3003                $path = __DIR__ . '/../../../' . $name[0];
3004            } elseif (\is_dir(__DIR__ . '/../../' . $name[0])) {
3005                $path = __DIR__ . '/../../' . $name[0];
3006            }
3007
3008            if ($path === '') {
3009                return;
3010            }
3011
3012            $currentVersion = '';
3013            $remoteVersion  = \substr($file[1], 0, -5);
3014
3015            if (Version::compare($currentVersion, $remoteVersion) < 0) {
3016                $toUpdate[$name[0]][$remoteVersion] = $file;
3017
3018                \uksort($toUpdate[$name[0]], [Version::class, 'compare']);
3019            }
3020        }
3021
3022        $this->apiUpdate($toUpdate);
3023    }
3024
3025    /**
3026     * Api update file
3027     *
3028     * @param RequestAbstract  $request  Request
3029     * @param ResponseAbstract $response Response
3030     * @param array            $data     Generic data
3031     *
3032     * @return void
3033     *
3034     * @api
3035     *
3036     * @since 1.0.0
3037     */
3038    public function apiUpdateFile(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
3039    {
3040        $this->apiUpdate([[
3041            'name'         => 'temp.json',
3042            'download_url' => 'https://raw.githubusercontent.com/Karaka-Management/' . ($request->getDataString('url') ?? ''),
3043        ]]);
3044    }
3045
3046    /**
3047     * Update the system or a module
3048     *
3049     * @param array $toUpdate Array of updte resources
3050     *
3051     * @return void
3052     *
3053     * @since 1.0.0
3054     */
3055    private function apiUpdate(array $toUpdate) : void
3056    {
3057        // this is only a temp... in the future this logic will change but for current purposes this is the easiest way to implement updates
3058
3059        foreach ($toUpdate as $update) {
3060            $dest = __DIR__ . '/../Updates/' . \explode('.', $update['name'])[0];
3061            \mkdir($dest);
3062            $this->downloadUpdate($update['download_url'], $dest . '/' . $update['name']);
3063            $this->runUpdate($dest . '/' . $update['name']);
3064        }
3065    }
3066
3067    /**
3068     * Package to download
3069     *
3070     * @param string $url  Url to download from
3071     * @param string $dest Local destination of the download
3072     *
3073     * @return void
3074     *
3075     * @since 1.0.0
3076     */
3077    private function downloadUpdate(string $url, string $dest) : void
3078    {
3079        // this is only a temp... in the future this logic will change but for current purposes this is the easiest way to implement updates
3080        $request = new HttpRequest(new HttpUri($url));
3081        $request->setMethod(RequestMethod::GET);
3082
3083        $updateFile = Rest::request($request)->getBody();
3084        File::put($dest, $updateFile);
3085    }
3086
3087    /**
3088     * Run the update
3089     *
3090     * @param string $updateFile Update file/package
3091     *
3092     * @return void
3093     *
3094     * @since 1.0.0
3095     */
3096    private function runUpdate(string $updateFile) : void
3097    {
3098    }
3099
3100    /**
3101     * Routing end-point for application behaviour.
3102     *
3103     * @param RequestAbstract  $request  Request
3104     * @param ResponseAbstract $response Response
3105     * @param array            $data     Generic data
3106     *
3107     * @return void
3108     *
3109     * @api
3110     *
3111     * @since 1.0.0
3112     */
3113    public function apiContactCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
3114    {
3115        if (!empty($val = $this->validateContactCreate($request))) {
3116            $response->header->status = RequestStatusCode::R_400;
3117            $this->createInvalidCreateResponse($request, $response, $val);
3118
3119            return;
3120        }
3121
3122        $contact = $this->createContactFromRequest($request);
3123
3124        $this->createModel($request->header->account, $contact, ContactMapper::class, 'account_contact', $request->getOrigin());
3125
3126        $this->createModelRelation(
3127            $request->header->account,
3128            (int) $request->getData('account'),
3129            $contact->id,
3130            AccountMapper::class, 'contacts', '', $request->getOrigin()
3131        );
3132
3133        $this->createStandardCreateResponse($request, $response, $contact);
3134    }
3135
3136    /**
3137     * Validate contact element create request
3138     *
3139     * @param RequestAbstract $request Request
3140     *
3141     * @return array<string, bool>
3142     *
3143     * @since 1.0.0
3144     */
3145    public function validateContactCreate(RequestAbstract $request) : array
3146    {
3147        $val = [];
3148        if (($val['account'] = !$request->hasData('account'))
3149            || ($val['type'] = !\is_numeric($request->getData('type')))
3150            || ($val['content'] = !$request->hasData('content'))
3151        ) {
3152            return $val;
3153        }
3154
3155        return [];
3156    }
3157
3158    /**
3159     * Method to create a account element from request.
3160     *
3161     * @param RequestAbstract $request Request
3162     *
3163     * @return Contact
3164     *
3165     * @since 1.0.0
3166     */
3167    public function createContactFromRequest(RequestAbstract $request) : Contact
3168    {
3169        /** @var Contact $element */
3170        $element = new Contact();
3171        $element->setType($request->getDataInt('type') ?? 0);
3172        $element->setSubtype($request->getDataInt('subtype') ?? 0);
3173        $element->content = $request->getDataString('content') ?? '';
3174        $element->account = $request->getDataInt('account') ?? 0;
3175
3176        return $element;
3177    }
3178
3179    /**
3180     * Api method to delete Settings
3181     *
3182     * @param RequestAbstract  $request  Request
3183     * @param ResponseAbstract $response Response
3184     * @param array            $data     Generic data
3185     *
3186     * @return void
3187     *
3188     * @api
3189     *
3190     * @since 1.0.0
3191     */
3192    public function apiSettingsDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
3193    {
3194        if (!empty($val = $this->validateSettingsDelete($request))) {
3195            $response->header->status = RequestStatusCode::R_400;
3196            $this->createInvalidDeleteResponse($request, $response, $val);
3197
3198            return;
3199        }
3200
3201        $settings = SettingMapper::get()->where('id', (int) $request->getData('id'))->execute();
3202        $this->deleteModel($request->header->account, $settings, SettingMapper::class, 'settings', $request->getOrigin());
3203        $this->createStandardDeleteResponse($request, $response, $settings);
3204    }
3205
3206    /**
3207     * Validate Settings delete request
3208     *
3209     * @param RequestAbstract $request Request
3210     *
3211     * @return array<string, bool>
3212     *
3213     * @todo: implement
3214     *
3215     * @since 1.0.0
3216     */
3217    private function validateSettingsDelete(RequestAbstract $request) : array
3218    {
3219        $val = [];
3220        if (($val['id'] = !$request->hasData('id'))) {
3221            return $val;
3222        }
3223
3224        return [];
3225    }
3226
3227    /**
3228     * Api method to update Application
3229     *
3230     * @param RequestAbstract  $request  Request
3231     * @param ResponseAbstract $response Response
3232     * @param array            $data     Generic data
3233     *
3234     * @return void
3235     *
3236     * @api
3237     *
3238     * @since 1.0.0
3239     */
3240    public function apiApplicationUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
3241    {
3242        if (!empty($val = $this->validateApplicationUpdate($request))) {
3243            $response->header->status = RequestStatusCode::R_400;
3244            $this->createInvalidUpdateResponse($request, $response, $val);
3245
3246            return;
3247        }
3248
3249        /** @var App $old */
3250        $old = AppMapper::get()->where('id', (int) $request->getData('id'))->execute();
3251        $new = $this->updateApplicationFromRequest($request, clone $old);
3252
3253        $this->updateModel($request->header->account, $old, $new, AppMapper::class, 'application', $request->getOrigin());
3254        $this->createStandardUpdateResponse($request, $response, $new);
3255    }
3256
3257    /**
3258     * Method to update Application from request.
3259     *
3260     * @param RequestAbstract $request Request
3261     * @param App             $new     Model to modify
3262     *
3263     * @return App
3264     *
3265     * @todo: implement
3266     *
3267     * @since 1.0.0
3268     */
3269    public function updateApplicationFromRequest(RequestAbstract $request, App $new) : App
3270    {
3271        return $new;
3272    }
3273
3274    /**
3275     * Validate Application update request
3276     *
3277     * @param RequestAbstract $request Request
3278     *
3279     * @return array<string, bool>
3280     *
3281     * @todo: implement
3282     *
3283     * @since 1.0.0
3284     */
3285    private function validateApplicationUpdate(RequestAbstract $request) : array
3286    {
3287        $val = [];
3288        if (($val['id'] = !$request->hasData('id'))) {
3289            return $val;
3290        }
3291
3292        return [];
3293    }
3294
3295    /**
3296     * Api method to delete Application
3297     *
3298     * @param RequestAbstract  $request  Request
3299     * @param ResponseAbstract $response Response
3300     * @param array            $data     Generic data
3301     *
3302     * @return void
3303     *
3304     * @api
3305     *
3306     * @since 1.0.0
3307     */
3308    public function apiApplicationDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
3309    {
3310        if (!empty($val = $this->validateApplicationDelete($request))) {
3311            $response->header->status = RequestStatusCode::R_400;
3312            $this->createInvalidDeleteResponse($request, $response, $val);
3313
3314            return;
3315        }
3316
3317        /** @var \Modules\Admin\Models\App $application */
3318        $application = AppMapper::get()->where('id', (int) $request->getData('id'))->execute();
3319        $this->deleteModel($request->header->account, $application, AppMapper::class, 'application', $request->getOrigin());
3320        $this->createStandardDeleteResponse($request, $response, $application);
3321    }
3322
3323    /**
3324     * Validate Application delete request
3325     *
3326     * @param RequestAbstract $request Request
3327     *
3328     * @return array<string, bool>
3329     *
3330     * @todo: implement
3331     *
3332     * @since 1.0.0
3333     */
3334    private function validateApplicationDelete(RequestAbstract $request) : array
3335    {
3336        $val = [];
3337        if (($val['id'] = !$request->hasData('id'))) {
3338            return $val;
3339        }
3340
3341        return [];
3342    }
3343
3344    /**
3345     * Validate GroupPermission update request
3346     *
3347     * @param RequestAbstract $request Request
3348     *
3349     * @return array<string, bool>
3350     *
3351     * @todo: implement
3352     *
3353     * @since 1.0.0
3354     */
3355    private function validateGroupPermissionUpdate(RequestAbstract $request) : array
3356    {
3357        $val = [];
3358        if (($val['id'] = !$request->hasData('id'))) {
3359            return $val;
3360        }
3361
3362        return [];
3363    }
3364
3365    /**
3366     * Validate GroupPermission delete request
3367     *
3368     * @param RequestAbstract $request Request
3369     *
3370     * @return array<string, bool>
3371     *
3372     * @todo: implement
3373     *
3374     * @since 1.0.0
3375     */
3376    private function validateGroupPermissionDelete(RequestAbstract $request) : array
3377    {
3378        $val = [];
3379        if (($val['id'] = !$request->hasData('id'))) {
3380            return $val;
3381        }
3382
3383        return [];
3384    }
3385
3386    /**
3387     * Method to update AccountPermission from request.
3388     *
3389     * @param RequestAbstract   $request Request
3390     * @param AccountPermission $new     Model to modify
3391     *
3392     * @return AccountPermission
3393     *
3394     * @todo: implement
3395     *
3396     * @since 1.0.0
3397     */
3398    public function updateAccountPermissionFromRequest(RequestAbstract $request, AccountPermission $new) : AccountPermission
3399    {
3400        return $new;
3401    }
3402
3403    /**
3404     * Validate AccountPermission update request
3405     *
3406     * @param RequestAbstract $request Request
3407     *
3408     * @return array<string, bool>
3409     *
3410     * @todo: implement
3411     *
3412     * @since 1.0.0
3413     */
3414    private function validateAccountPermissionUpdate(RequestAbstract $request) : array
3415    {
3416        $val = [];
3417        if (($val['id'] = !$request->hasData('id'))) {
3418            return $val;
3419        }
3420
3421        return [];
3422    }
3423
3424    /**
3425     * Validate AccountPermission delete request
3426     *
3427     * @param RequestAbstract $request Request
3428     *
3429     * @return array<string, bool>
3430     *
3431     * @todo: implement
3432     *
3433     * @since 1.0.0
3434     */
3435    private function validateAccountPermissionDelete(RequestAbstract $request) : array
3436    {
3437        $val = [];
3438        if (($val['id'] = !$request->hasData('id'))) {
3439            return $val;
3440        }
3441
3442        return [];
3443    }
3444
3445    /**
3446     * Api method to update Contact
3447     *
3448     * @param RequestAbstract  $request  Request
3449     * @param ResponseAbstract $response Response
3450     * @param array            $data     Generic data
3451     *
3452     * @return void
3453     *
3454     * @api
3455     *
3456     * @since 1.0.0
3457     */
3458    public function apiContactUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
3459    {
3460        if (!empty($val = $this->validateContactUpdate($request))) {
3461            $response->header->status = RequestStatusCode::R_400;
3462            $this->createInvalidUpdateResponse($request, $response, $val);
3463
3464            return;
3465        }
3466
3467        /** @var Contact $old */
3468        $old = ContactMapper::get()->where('id', (int) $request->getData('id'))->execute();
3469        $new = $this->updateContactFromRequest($request, clone $old);
3470
3471        $this->updateModel($request->header->account, $old, $new, ContactMapper::class, 'contact', $request->getOrigin());
3472        $this->createStandardUpdateResponse($request, $response, $new);
3473    }
3474
3475    /**
3476     * Method to update Contact from request.
3477     *
3478     * @param RequestAbstract $request Request
3479     * @param Contact         $new     Model to modify
3480     *
3481     * @return Contact
3482     *
3483     * @todo: implement
3484     *
3485     * @since 1.0.0
3486     */
3487    public function updateContactFromRequest(RequestAbstract $request, Contact $new) : Contact
3488    {
3489        return $new;
3490    }
3491
3492    /**
3493     * Validate Contact update request
3494     *
3495     * @param RequestAbstract $request Request
3496     *
3497     * @return array<string, bool>
3498     *
3499     * @todo: implement
3500     *
3501     * @since 1.0.0
3502     */
3503    private function validateContactUpdate(RequestAbstract $request) : array
3504    {
3505        $val = [];
3506        if (($val['id'] = !$request->hasData('id'))) {
3507            return $val;
3508        }
3509
3510        return [];
3511    }
3512
3513    /**
3514     * Api method to delete Contact
3515     *
3516     * @param RequestAbstract  $request  Request
3517     * @param ResponseAbstract $response Response
3518     * @param array            $data     Generic data
3519     *
3520     * @return void
3521     *
3522     * @api
3523     *
3524     * @since 1.0.0
3525     */
3526    public function apiContactDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
3527    {
3528        if (!empty($val = $this->validateContactDelete($request))) {
3529            $response->header->status = RequestStatusCode::R_400;
3530            $this->createInvalidDeleteResponse($request, $response, $val);
3531
3532            return;
3533        }
3534
3535        /** @var \Modules\Admin\Models\Contact $contact */
3536        $contact = ContactMapper::get()->where('id', (int) $request->getData('id'))->execute();
3537        $this->deleteModel($request->header->account, $contact, ContactMapper::class, 'contact', $request->getOrigin());
3538        $this->createStandardDeleteResponse($request, $response, $contact);
3539    }
3540
3541    /**
3542     * Validate Contact delete request
3543     *
3544     * @param RequestAbstract $request Request
3545     *
3546     * @return array<string, bool>
3547     *
3548     * @todo: implement
3549     *
3550     * @since 1.0.0
3551     */
3552    private function validateContactDelete(RequestAbstract $request) : array
3553    {
3554        $val = [];
3555        if (($val['id'] = !$request->hasData('id'))) {
3556            return $val;
3557        }
3558
3559        return [];
3560    }
3561
3562    /**
3563     * Api method to create Data
3564     *
3565     * @param RequestAbstract  $request  Request
3566     * @param ResponseAbstract $response Response
3567     * @param array            $data     Generic data
3568     *
3569     * @return void
3570     *
3571     * @api
3572     *
3573     * @since 1.0.0
3574     */
3575    public function apiDataChangeCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
3576    {
3577        if (!empty($val = $this->validateDataChangeCreate($request))) {
3578            $response->header->status = RequestStatusCode::R_400;
3579            $this->createInvalidCreateResponse($request, $response, $val);
3580
3581            return;
3582        }
3583
3584        $data = $this->createDataChangeFromRequest();
3585        $this->createModel($request->header->account, $data, DataChangeMapper::class, 'data', $request->getOrigin());
3586        $this->createStandardCreateResponse($request, $response, $data);
3587    }
3588
3589    /**
3590     * Method to create DataChange from request.
3591     *
3592     * @return DataChange
3593     *
3594     * @todo: implement
3595     *
3596     * @since 1.0.0
3597     */
3598    private function createDataChangeFromRequest() : DataChange
3599    {
3600        return new DataChange();
3601    }
3602
3603    /**
3604     * Validate Data create request
3605     *
3606     * @param RequestAbstract $request Request
3607     *
3608     * @return array<string, bool>
3609     *
3610     * @todo: implement
3611     *
3612     * @since 1.0.0
3613     */
3614    private function validateDataChangeCreate(RequestAbstract $request) : array
3615    {
3616        $val = [];
3617        if (($val['id'] = ($request->header->account < 1))) {
3618            return $val;
3619        }
3620
3621        return [];
3622    }
3623
3624    /**
3625     * Api method to delete DataChange
3626     *
3627     * @param RequestAbstract  $request  Request
3628     * @param ResponseAbstract $response Response
3629     * @param array            $data     Generic data
3630     *
3631     * @return void
3632     *
3633     * @api
3634     *
3635     * @since 1.0.0
3636     */
3637    public function apiDataChangeDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
3638    {
3639        if (!empty($val = $this->validateDataChangeDelete($request))) {
3640            $response->header->status = RequestStatusCode::R_400;
3641            $this->createInvalidDeleteResponse($request, $response, $val);
3642
3643            return;
3644        }
3645
3646        /** @var \Modules\Admin\Models\DataChange $data */
3647        $data = DataChangeMapper::get()->where('id', (int) $request->getData('id'))->execute();
3648        $this->deleteModel($request->header->account, $data, DataChangeMapper::class, 'data', $request->getOrigin());
3649        $this->createStandardDeleteResponse($request, $response, $data);
3650    }
3651
3652    /**
3653     * Validate DataChange delete request
3654     *
3655     * @param RequestAbstract $request Request
3656     *
3657     * @return array<string, bool>
3658     *
3659     * @todo: implement
3660     *
3661     * @since 1.0.0
3662     */
3663    private function validateDataChangeDelete(RequestAbstract $request) : array
3664    {
3665        $val = [];
3666        if (($val['id'] = !$request->hasData('id'))) {
3667            return $val;
3668        }
3669
3670        return [];
3671    }
3672}