Passed
Push — master ( 5bbfbb...247434 )
by Greg
06:13
created

UsersController::cleanup()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 39
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 24
nc 1
nop 1
dl 0
loc 39
rs 9.2248
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\Controllers\Admin;
21
22
use Fisharebest\Localization\Locale\LocaleInterface;
23
use Fisharebest\Webtrees\Auth;
24
use Fisharebest\Webtrees\Carbon;
25
use Fisharebest\Webtrees\FlashMessages;
26
use Fisharebest\Webtrees\Functions\FunctionsEdit;
27
use Fisharebest\Webtrees\I18N;
28
use Fisharebest\Webtrees\Log;
29
use Fisharebest\Webtrees\Module\ModuleThemeInterface;
30
use Fisharebest\Webtrees\Services\DatatablesService;
31
use Fisharebest\Webtrees\Services\EmailService;
32
use Fisharebest\Webtrees\Services\ModuleService;
33
use Fisharebest\Webtrees\Services\TreeService;
34
use Fisharebest\Webtrees\Services\UserService;
35
use Fisharebest\Webtrees\Site;
36
use Fisharebest\Webtrees\SiteUser;
37
use Fisharebest\Webtrees\User;
38
use Illuminate\Database\Capsule\Manager as DB;
39
use Illuminate\Database\Query\JoinClause;
40
use Illuminate\Support\Collection;
41
use Psr\Http\Message\ResponseInterface;
42
use Psr\Http\Message\ServerRequestInterface;
43
use stdClass;
44
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
45
46
use function assert;
47
use function e;
48
use function route;
49
50
/**
51
 * Controller for user administration.
52
 */
53
class UsersController extends AbstractAdminController
54
{
55
    /** @var DatatablesService */
56
    private $datatables_service;
57
58
    /** @var EmailService */
59
    private $email_service;
60
61
    /** @var ModuleService */
62
    private $module_service;
63
64
    /** @var UserService */
65
    private $user_service;
66
67
    /** @var TreeService */
68
    private $tree_service;
69
70
    /**
71
     * UsersController constructor.
72
     *
73
     * @param DatatablesService $datatables_service
74
     * @param EmailService      $email_service
75
     * @param ModuleService     $module_service
76
     * @param TreeService       $tree_service
77
     * @param UserService       $user_service
78
     */
79
    public function __construct(
80
        DatatablesService $datatables_service,
81
        EmailService $email_service,
82
        ModuleService $module_service,
83
        TreeService $tree_service,
84
        UserService $user_service
85
    ) {
86
        $this->datatables_service = $datatables_service;
87
        $this->email_service      = $email_service;
88
        $this->module_service     = $module_service;
89
        $this->tree_service       = $tree_service;
90
        $this->user_service       = $user_service;
91
    }
92
93
    /**
94
     * @param ServerRequestInterface $request
95
     *
96
     * @return ResponseInterface
97
     */
98
    public function index(ServerRequestInterface $request): ResponseInterface
99
    {
100
        $user = $request->getAttribute('user');
101
102
        $filter = $request->getQueryParams()['filter'] ?? '';
103
104
        $all_users = $this->user_service->all();
105
106
        $page_size = (int) $user->getPreference(' admin_users_page_size', '10');
107
108
        $title = I18N::translate('User administration');
109
110
        return $this->viewResponse('admin/users', [
111
            'all_users' => $all_users,
112
            'filter'    => $filter,
113
            'page_size' => $page_size,
114
            'title'     => $title,
115
        ]);
116
    }
117
118
    /**
119
     * @param ServerRequestInterface $request
120
     *
121
     * @return ResponseInterface
122
     */
123
    public function data(ServerRequestInterface $request): ResponseInterface
124
    {
125
        $user = $request->getAttribute('user');
126
127
        $installed_languages = [];
128
        foreach (I18N::installedLocales() as $installed_locale) {
129
            $installed_languages[$installed_locale->languageTag()] = $installed_locale->endonym();
130
        }
131
132
        $query = DB::table('user')
133
            ->leftJoin('user_setting AS us1', static function (JoinClause $join): void {
134
                $join
135
                    ->on('us1.user_id', '=', 'user.user_id')
136
                    ->where('us1.setting_name', '=', 'language');
137
            })
138
            ->leftJoin('user_setting AS us2', static function (JoinClause $join): void {
139
                $join
140
                    ->on('us2.user_id', '=', 'user.user_id')
141
                    ->where('us2.setting_name', '=', 'reg_timestamp');
142
            })
143
            ->leftJoin('user_setting AS us3', static function (JoinClause $join): void {
144
                $join
145
                    ->on('us3.user_id', '=', 'user.user_id')
146
                    ->where('us3.setting_name', '=', 'sessiontime');
147
            })
148
            ->leftJoin('user_setting AS us4', static function (JoinClause $join): void {
149
                $join
150
                    ->on('us4.user_id', '=', 'user.user_id')
151
                    ->where('us4.setting_name', '=', 'verified');
152
            })
153
            ->leftJoin('user_setting AS us5', static function (JoinClause $join): void {
154
                $join
155
                    ->on('us5.user_id', '=', 'user.user_id')
156
                    ->where('us5.setting_name', '=', 'verified_by_admin');
157
            })
158
            ->where('user.user_id', '>', '0')
159
            ->select([
160
                'user.user_id AS edit_menu', // Hidden column
161
                'user.user_id',
162
                'user_name',
163
                'real_name',
164
                'email',
165
                'us1.setting_value AS language',
166
                'us2.setting_value AS registered_at_sort', // Hidden column
167
                'us2.setting_value AS registered_at',
168
                'us3.setting_value AS active_at_sort', // Hidden column
169
                'us3.setting_value AS active_at',
170
                'us4.setting_value AS verified',
171
                'us5.setting_value AS verified_by_admin',
172
            ]);
173
174
        $search_columns = ['user_name', 'real_name', 'email'];
175
        $sort_columns   = [];
176
177
        $callback = static function (stdClass $row) use ($installed_languages, $user): array {
178
            $datum = [
179
                view('admin/users-table-options', ['row' => $row, 'user' => $user]),
180
                $row->user_id,
181
                '<span dir="auto">' . e($row->user_name) . '</span>',
182
                '<span dir="auto">' . e($row->real_name) . '</span>',
183
                '<a href="mailto:' . e($row->email) . '">' . e($row->email) . '</a>',
184
                $installed_languages[$row->language] ?? $row->language,
185
                $row->registered_at,
186
                $row->registered_at ? view('components/datetime-diff', ['timestamp' => Carbon::createFromTimestamp((int) $row->registered_at)]) : '',
187
                $row->active_at,
188
                $row->active_at ? view('components/datetime-diff', ['timestamp' => Carbon::createFromTimestamp((int) $row->active_at)]) : I18N::translate('Never'),
189
                $row->verified ? I18N::translate('yes') : I18N::translate('no'),
190
                $row->verified_by_admin ? I18N::translate('yes') : I18N::translate('no'),
191
            ];
192
193
            // Highlight old registrations.
194
            if (!$datum[10] && date('U') - $datum[6] > 604800) {
195
                $datum[7] = '<span class="text-danger">' . $datum[7] . '</span>';
196
            }
197
198
            return $datum;
199
        };
200
201
        return $this->datatables_service->handle($request, $query, $search_columns, $sort_columns, $callback);
202
    }
203
204
    /**
205
     * @param ServerRequestInterface $request
206
     *
207
     * @return ResponseInterface
208
     */
209
    public function create(ServerRequestInterface $request): ResponseInterface
210
    {
211
        $email     = $request->getQueryParams()['email'] ?? '';
212
        $real_name = $request->getQueryParams()['real_name'] ?? '';
213
        $username  = $request->getQueryParams()['username'] ?? '';
214
        $title     = I18N::translate('Add a user');
215
216
        return $this->viewResponse('admin/users-create', [
217
            'email'     => $email,
218
            'real_name' => $real_name,
219
            'title'     => $title,
220
            'username'  => $username,
221
        ]);
222
    }
223
224
    /**
225
     * @param ServerRequestInterface $request
226
     *
227
     * @return ResponseInterface
228
     */
229
    public function edit(ServerRequestInterface $request): ResponseInterface
230
    {
231
        $locale = $request->getAttribute('locale');
232
        assert($locale instanceof LocaleInterface);
233
234
        $user_id = (int) $request->getQueryParams()['user_id'];
235
        $user    = $this->user_service->find($user_id);
236
237
        if ($user === null) {
238
            throw new NotFoundHttpException(I18N::translate('%1$s does not exist.', 'user_id:' . $user_id));
239
        }
240
241
        return $this->viewResponse('admin/users-edit', [
242
            'contact_methods' => FunctionsEdit::optionsContactMethods(),
243
            'default_locale'  => $locale->languageTag(),
244
            'locales'         => I18N::installedLocales(),
245
            'roles'           => $this->roles(),
246
            'trees'           => $this->tree_service->all(),
247
            'theme_options'   => $this->themeOptions(),
248
            'title'           => I18N::translate('Edit the user'),
249
            'user'            => $user,
250
        ]);
251
    }
252
253
    /**
254
     * @param ServerRequestInterface $request
255
     *
256
     * @return ResponseInterface
257
     */
258
    public function save(ServerRequestInterface $request): ResponseInterface
259
    {
260
        $locale = $request->getAttribute('locale');
261
        assert($locale instanceof LocaleInterface);
262
263
        $username  = $request->getParsedBody()['username'];
264
        $real_name = $request->getParsedBody()['real_name'];
265
        $email     = $request->getParsedBody()['email'];
266
        $password  = $request->getParsedBody()['password'];
267
268
        $errors = false;
269
        if ($this->user_service->findByUserName($username)) {
270
            FlashMessages::addMessage(I18N::translate('Duplicate username. A user with that username already exists. Please choose another username.'));
271
            $errors = true;
272
        }
273
274
        if ($this->user_service->findByEmail($email)) {
275
            FlashMessages::addMessage(I18N::translate('Duplicate email address. A user with that email already exists.'));
276
            $errors = true;
277
        }
278
279
        if ($errors) {
280
            $url = route('admin-users-create', [
281
                'email'     => $email,
282
                'real_name' => $real_name,
283
                'username'  => $username,
284
            ]);
285
286
            return redirect($url);
287
        }
288
289
        $new_user = $this->user_service->create($username, $real_name, $email, $password)
290
            ->setPreference('verified', '1')
291
            ->setPreference('language', $locale->languageTag())
292
            ->setPreference('timezone', Site::getPreference('TIMEZONE'))
293
            ->setPreference('reg_timestamp', date('U'))
294
            ->setPreference('sessiontime', '0');
295
296
        Log::addAuthenticationLog('User ->' . $username . '<- created');
297
298
        $url = route('admin-users-edit', [
299
            'user_id' => $new_user->id(),
300
        ]);
301
302
        return redirect($url);
303
    }
304
305
    /**
306
     * @param ServerRequestInterface $request
307
     *
308
     * @return ResponseInterface
309
     */
310
    public function update(ServerRequestInterface $request): ResponseInterface
311
    {
312
        $user = $request->getAttribute('user');
313
314
        $user_id        = (int) $request->getParsedBody()['user_id'];
315
        $username       = $request->getParsedBody()['username'];
316
        $real_name      = $request->getParsedBody()['real_name'];
317
        $email          = $request->getParsedBody()['email'];
318
        $password       = $request->getParsedBody()['password'];
319
        $theme          = $request->getParsedBody()['theme'];
320
        $language       = $request->getParsedBody()['language'];
321
        $timezone       = $request->getParsedBody()['timezone'];
322
        $contact_method = $request->getParsedBody()['contact_method'];
323
        $comment        = $request->getParsedBody()['comment'];
324
        $auto_accept    = (bool) ($request->getParsedBody()['auto_accept'] ?? false);
325
        $canadmin       = (bool) ($request->getParsedBody()['canadmin'] ?? false);
326
        $visible_online = (bool) ($request->getParsedBody()['visible_online'] ?? false);
327
        $verified       = (bool) ($request->getParsedBody()['verified'] ?? false);
328
        $approved       = (bool) ($request->getParsedBody()['approved'] ?? false);
329
330
        $edit_user = $this->user_service->find($user_id);
331
332
        if ($edit_user === null) {
333
            throw new NotFoundHttpException(I18N::translate('%1$s does not exist', 'user_id:' . $user_id));
334
        }
335
336
        // We have just approved a user.  Tell them
337
        if ($approved && $edit_user->getPreference('verified_by_admin') !== '1') {
338
            I18N::init($edit_user->getPreference('language'));
339
340
            $base_url = $request->getAttribute('base_url');
341
342
            $this->email_service->send(
343
                new SiteUser(),
344
                $edit_user,
345
                Auth::user(),
346
                /* I18N: %s is a server name/URL */
347
                I18N::translate('New user at %s', $base_url),
348
                view('emails/approve-user-text', ['user' => $edit_user, 'base_url' => $base_url]),
349
                view('emails/approve-user-html', ['user' => $edit_user, 'base_url' => $base_url])
350
            );
351
        }
352
353
        $edit_user
354
            ->setRealName($real_name)
355
            ->setPreference('theme', $theme)
356
            ->setPreference('language', $language)
357
            ->setPreference('TIMEZONE', $timezone)
358
            ->setPreference('contactmethod', $contact_method)
359
            ->setPreference('comment', $comment)
360
            ->setPreference('auto_accept', (string) $auto_accept)
361
            ->setPreference('visibleonline', (string) $visible_online)
362
            ->setPreference('verified', (string) $verified)
363
            ->setPreference('verified_by_admin', (string) $approved);
364
365
        if ($password !== '') {
366
            $edit_user->setPassword($password);
367
        }
368
369
        // We cannot change our own admin status. Another admin will need to do it.
370
        if ($edit_user->id() !== $user->id()) {
371
            $edit_user->setPreference('canadmin', $canadmin ? '1' : '0');
372
        }
373
374
        foreach ($this->tree_service->all() as $tree) {
375
            $path_length = (int) $request->getParsedBody()['RELATIONSHIP_PATH_LENGTH' . $tree->id()];
376
            $gedcom_id   = $request->getParsedBody()['gedcomid' . $tree->id()] ?? '';
377
            $can_edit    = $request->getParsedBody()['canedit' . $tree->id()] ?? '';
378
379
            // Do not allow a path length to be set if the individual ID is not
380
            if ($gedcom_id === '') {
381
                $path_length = 0;
382
            }
383
384
            $tree->setUserPreference($edit_user, 'gedcomid', $gedcom_id);
385
            $tree->setUserPreference($edit_user, 'canedit', $can_edit);
386
            $tree->setUserPreference($edit_user, 'RELATIONSHIP_PATH_LENGTH', (string) $path_length);
387
        }
388
389
        if ($edit_user->email() !== $email && $this->user_service->findByEmail($email) instanceof User) {
390
            FlashMessages::addMessage(I18N::translate('Duplicate email address. A user with that email already exists.') . $email, 'danger');
391
392
            return redirect(route('admin-users-edit', ['user_id' => $edit_user->id()]));
393
        }
394
395
        if ($edit_user->userName() !== $username && $this->user_service->findByUserName($username) instanceof User) {
396
            FlashMessages::addMessage(I18N::translate('Duplicate username. A user with that username already exists. Please choose another username.'), 'danger');
397
398
            return redirect(route('admin-users-edit', ['user_id' => $edit_user->id()]));
399
        }
400
401
        $edit_user
402
            ->setEmail($email)
403
            ->setUserName($username);
404
405
        return redirect(route('admin-users'));
406
    }
407
408
    /**
409
     * @return string[]
410
     */
411
    private function roles(): array
412
    {
413
        return [
414
            /* I18N: Listbox entry; name of a role */
415
            'none'   => I18N::translate('Visitor'),
416
            /* I18N: Listbox entry; name of a role */
417
            'access' => I18N::translate('Member'),
418
            /* I18N: Listbox entry; name of a role */
419
            'edit'   => I18N::translate('Editor'),
420
            /* I18N: Listbox entry; name of a role */
421
            'accept' => I18N::translate('Moderator'),
422
            /* I18N: Listbox entry; name of a role */
423
            'admin'  => I18N::translate('Manager'),
424
        ];
425
    }
426
427
    /**
428
     * @return Collection
429
     */
430
    private function themeOptions(): Collection
431
    {
432
        return $this->module_service
433
            ->findByInterface(ModuleThemeInterface::class)
434
            ->map($this->module_service->titleMapper())
435
            ->prepend(I18N::translate('<default theme>'), '');
436
    }
437
}
438