Passed
Push — master ( b1a542...9022ab )
by Greg
07:10
created

AdminSiteController::serverInformation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
nc 1
nop 1
dl 0
loc 11
rs 10
c 1
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
declare(strict_types=1);
18
19
namespace Fisharebest\Webtrees\Http\Controllers;
20
21
use Exception;
22
use Fig\Http\Message\StatusCodeInterface;
23
use Fisharebest\Webtrees\Carbon;
24
use Fisharebest\Webtrees\FlashMessages;
25
use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
26
use Fisharebest\Webtrees\I18N;
27
use Fisharebest\Webtrees\Module\ModuleThemeInterface;
28
use Fisharebest\Webtrees\Services\DatatablesService;
29
use Fisharebest\Webtrees\Services\MailService;
30
use Fisharebest\Webtrees\Services\ModuleService;
31
use Fisharebest\Webtrees\Services\TreeService;
32
use Fisharebest\Webtrees\Services\UserService;
33
use Fisharebest\Webtrees\Site;
34
use Fisharebest\Webtrees\Tree;
35
use Fisharebest\Webtrees\User;
36
use Illuminate\Database\Capsule\Manager as DB;
37
use Illuminate\Database\Query\Builder;
38
use Illuminate\Database\Query\Expression;
39
use Illuminate\Support\Collection;
40
use League\Flysystem\FilesystemInterface;
41
use Psr\Http\Message\ResponseInterface;
42
use Psr\Http\Message\ServerRequestInterface;
43
use stdClass;
44
45
use function filter_var;
46
47
use const FILTER_VALIDATE_DOMAIN;
48
49
/**
50
 * Controller for site administration.
51
 */
52
class AdminSiteController extends AbstractBaseController
53
{
54
    /** @var string */
55
    protected $layout = 'layouts/administration';
56
57
    /** @var DatatablesService */
58
    private $datatables_service;
59
60
    /** @var FilesystemInterface */
61
    private $filesystem;
62
63
    /** @var MailService */
64
    private $mail_service;
65
66
    /** @var ModuleService */
67
    private $module_service;
68
69
    /** @var TreeService */
70
    private $tree_service;
71
72
    /** @var UserService */
73
    private $user_service;
74
75
    /**
76
     * AdminSiteController constructor.
77
     *
78
     * @param DatatablesService   $datatables_service
79
     * @param FilesystemInterface $filesystem
80
     * @param MailService         $mail_service
81
     * @param ModuleService       $module_service
82
     * @param UserService         $user_service
83
     */
84
    public function __construct(DatatablesService $datatables_service, FilesystemInterface $filesystem, MailService $mail_service, ModuleService $module_service, TreeService $tree_service, UserService $user_service)
85
    {
86
        $this->mail_service       = $mail_service;
87
        $this->datatables_service = $datatables_service;
88
        $this->filesystem         = $filesystem;
89
        $this->module_service     = $module_service;
90
        $this->tree_service       = $tree_service;
91
        $this->user_service       = $user_service;
92
    }
93
94
    /**
95
     * Show old user files in the data folder.
96
     *
97
     * @param ServerRequestInterface $request
98
     *
99
     * @return ResponseInterface
100
     */
101
    public function cleanData(ServerRequestInterface $request): ResponseInterface
102
    {
103
        $protected = [
104
            '.htaccess',
105
            '.gitignore',
106
            'index.php',
107
            'config.ini.php',
108
        ];
109
110
        if ($request->getAttribute('dbtype') === 'sqlite') {
111
            $protected[] = $request->getAttribute('dbname') . '.sqlite';
112
        }
113
114
        // Protect the media folders
115
        foreach ($this->tree_service->all() as $tree) {
116
            $media_directory = $tree->getPreference('MEDIA_DIRECTORY');
117
            [$folder] = explode('/', $media_directory);
118
119
            $protected[] = $folder;
120
        }
121
122
        // List the top-level contents of the data folder
123
        $entries = array_map(static function (array $content) {
124
            return $content['path'];
125
        }, $this->filesystem->listContents());
126
127
        return $this->viewResponse('admin/clean-data', [
128
            'title'     => I18N::translate('Clean up data folder'),
129
            'entries'   => $entries,
130
            'protected' => $protected,
131
        ]);
132
    }
133
134
    /**
135
     * Delete old user files in the data folder.
136
     *
137
     * @param ServerRequestInterface $request
138
     *
139
     * @return ResponseInterface
140
     */
141
    public function cleanDataAction(ServerRequestInterface $request): ResponseInterface
142
    {
143
        $to_delete = $request->getParsedBody()['to_delete'] ?? [];
144
        $to_delete = array_filter($to_delete);
145
146
        foreach ($to_delete as $path) {
147
            $metadata = $this->filesystem->getMetadata($path);
148
149
            if ($metadata === false) {
150
                // Already deleted?
151
                continue;
152
            }
153
154
            if ($metadata['type'] === 'dir') {
155
                try {
156
                    $this->filesystem->deleteDir($path);
157
158
                    FlashMessages::addMessage(I18N::translate('The folder %s has been deleted.', e($path)), 'success');
159
                } catch (Exception $ex) {
160
                    FlashMessages::addMessage(I18N::translate('The folder %s could not be deleted.', e($path)), 'danger');
161
                }
162
            }
163
164
            if ($metadata['type'] === 'file') {
165
                try {
166
                    $this->filesystem->delete($path);
167
168
                    FlashMessages::addMessage(I18N::translate('The file %s has been deleted.', e($path)), 'success');
169
                } catch (Exception $ex) {
170
                    FlashMessages::addMessage(I18N::translate('The file %s could not be deleted.', e($path)), 'danger');
171
                }
172
            }
173
        }
174
175
        return redirect(route('admin-clean-data'));
176
    }
177
178
    /**
179
     * @param ServerRequestInterface $request
180
     *
181
     * @return ResponseInterface
182
     */
183
    public function logs(ServerRequestInterface $request): ResponseInterface
184
    {
185
        $earliest = DB::table('log')->min('log_time');
186
        $latest   = DB::table('log')->max('log_time');
187
188
        $earliest = $earliest ? Carbon::make($earliest) : Carbon::now();
189
        $latest   = $latest ? Carbon::make($latest) : Carbon::now();
190
191
        $earliest = $earliest->toDateString();
0 ignored issues
show
Bug introduced by
The method toDateString() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

191
        /** @scrutinizer ignore-call */ 
192
        $earliest = $earliest->toDateString();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
192
        $latest   = $latest->toDateString();
0 ignored issues
show
Bug introduced by
The method toDateString() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

192
        /** @scrutinizer ignore-call */ 
193
        $latest   = $latest->toDateString();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
193
194
        $params   = $request->getQueryParams();
195
        $action   = $params['action'] ?? '';
196
        $from     = $params['from'] ?? $earliest;
197
        $to       = $params['to'] ?? $latest;
198
        $type     = $params['type'] ?? '';
199
        $text     = $params['text'] ?? '';
200
        $ip       = $params['ip'] ?? '';
201
        $username = $params['username'] ?? '';
202
        $gedc     = $params['gedc'] ?? '';
203
204
        $from = max($from, $earliest);
205
        $to   = min(max($from, $to), $latest);
206
207
        $user_options = $this->user_service->all()->mapWithKeys(static function (User $user): array {
208
            return [$user->userName() => $user->userName()];
209
        });
210
        $user_options = (new Collection(['' => '']))->merge($user_options);
211
212
        $tree_options = $this->tree_service->all()->mapWithKeys(static function (Tree $tree): array {
213
            return [$tree->name() => $tree->title()];
214
        });
215
        $tree_options = (new Collection(['' => '']))->merge($tree_options);
216
217
        $title = I18N::translate('Website logs');
218
219
        return $this->viewResponse('admin/site-logs', [
220
            'action'       => $action,
221
            'earliest'     => $earliest,
222
            'from'         => $from,
223
            'gedc'         => $gedc,
224
            'ip'           => $ip,
225
            'latest'       => $latest,
226
            'tree_options' => $tree_options,
227
            'title'        => $title,
228
            'to'           => $to,
229
            'text'         => $text,
230
            'type'         => $type,
231
            'username'     => $username,
232
            'user_options' => $user_options,
233
        ]);
234
    }
235
236
    /**
237
     * @param ServerRequestInterface $request
238
     *
239
     * @return ResponseInterface
240
     */
241
    public function logsData(ServerRequestInterface $request): ResponseInterface
242
    {
243
        $query = $this->logsQuery($request->getQueryParams());
244
245
        return $this->datatables_service->handle($request, $query, [], [], static function (stdClass $row): array {
246
            return [
247
                $row->log_id,
248
                Carbon::make($row->log_time)->local()->format('Y-m-d H:i:s'),
249
                $row->log_type,
250
                '<span dir="auto">' . e($row->log_message) . '</span>',
251
                '<span dir="auto">' . e($row->ip_address) . '</span>',
252
                '<span dir="auto">' . e($row->user_name) . '</span>',
253
                '<span dir="auto">' . e($row->gedcom_name) . '</span>',
254
            ];
255
        });
256
    }
257
258
    /**
259
     * Generate a query for filtering the site log.
260
     *
261
     * @param string[] $params
262
     *
263
     * @return Builder
264
     */
265
    private function logsQuery(array $params): Builder
266
    {
267
        $from     = $params['from'];
268
        $to       = $params['to'];
269
        $type     = $params['type'];
270
        $text     = $params['text'];
271
        $ip       = $params['ip'];
272
        $username = $params['username'];
273
        $gedc     = $params['gedc'];
274
275
        $query = DB::table('log')
276
            ->leftJoin('user', 'user.user_id', '=', 'log.user_id')
277
            ->leftJoin('gedcom', 'gedcom.gedcom_id', '=', 'log.gedcom_id')
278
            ->select(['log.*', new Expression("COALESCE(user_name, '<none>') AS user_name"), new Expression("COALESCE(gedcom_name, '<none>') AS gedcom_name")]);
279
280
        if ($from !== '') {
281
            $query->where('log_time', '>=', $from);
282
        }
283
284
        if ($to !== '') {
285
            // before end of the day
286
            $query->where('log_time', '<', Carbon::make($to)->addDay());
287
        }
288
289
        if ($type !== '') {
290
            $query->where('log_type', '=', $type);
291
        }
292
293
        if ($text) {
294
            $query->whereContains('log_message', $text);
295
        }
296
297
        if ($ip) {
298
            $query->whereContains('ip_address', $ip);
299
        }
300
301
        if ($username) {
302
            $query->whereContains('user_name', $ip);
303
        }
304
305
        if ($gedc) {
306
            $query->where('gedcom_name', '=', $gedc);
307
        }
308
309
        return $query;
310
    }
311
312
    /**
313
     * @param ServerRequestInterface $request
314
     *
315
     * @return ResponseInterface
316
     */
317
    public function logsDelete(ServerRequestInterface $request): ResponseInterface
318
    {
319
        $this->logsQuery($request->getParsedBody())->delete();
0 ignored issues
show
Bug introduced by
It seems like $request->getParsedBody() can also be of type null and object; however, parameter $params of Fisharebest\Webtrees\Htt...Controller::logsQuery() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

319
        $this->logsQuery(/** @scrutinizer ignore-type */ $request->getParsedBody())->delete();
Loading history...
320
321
        return response();
322
    }
323
324
    /**
325
     * @param ServerRequestInterface $request
326
     *
327
     * @return ResponseInterface
328
     */
329
    public function logsExport(ServerRequestInterface $request): ResponseInterface
330
    {
331
        $content = $this->logsQuery($request->getQueryParams())
332
            ->orderBy('log_id')
333
            ->get()
334
            ->map(static function (stdClass $row): string {
335
                return
336
                    '"' . $row->log_time . '",' .
337
                    '"' . $row->log_type . '",' .
338
                    '"' . str_replace('"', '""', $row->log_message) . '",' .
339
                    '"' . $row->ip_address . '",' .
340
                    '"' . str_replace('"', '""', $row->user_name) . '",' .
341
                    '"' . str_replace('"', '""', $row->gedcom_name) . '"' .
342
                    "\n";
343
            })
344
            ->implode('');
345
346
        return response($content, StatusCodeInterface::STATUS_OK, [
347
            'Content-Type'        => 'text/csv; charset=utf-8',
348
            'Content-Disposition' => 'attachment; filename="webtrees-logs.csv"',
349
        ]);
350
    }
351
352
    /**
353
     * @param ServerRequestInterface $request
354
     *
355
     * @return ResponseInterface
356
     */
357
    public function mailForm(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

357
    public function mailForm(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
358
    {
359
        $mail_ssl_options       = $this->mail_service->mailSslOptions();
360
        $mail_transport_options = $this->mail_service->mailTransportOptions();
361
362
        $title = I18N::translate('Sending email');
363
364
        $SMTP_ACTIVE    = Site::getPreference('SMTP_ACTIVE');
365
        $SMTP_AUTH      = Site::getPreference('SMTP_AUTH');
366
        $SMTP_AUTH_USER = Site::getPreference('SMTP_AUTH_USER');
367
        $SMTP_FROM_NAME = $this->mail_service->senderEmail();
368
        $SMTP_HELO      = $this->mail_service->localDomain();
369
        $SMTP_HOST      = Site::getPreference('SMTP_HOST');
370
        $SMTP_PORT      = Site::getPreference('SMTP_PORT');
371
        $SMTP_SSL       = Site::getPreference('SMTP_SSL');
372
        $DKIM_DOMAIN    = Site::getPreference('DKIM_DOMAIN');
373
        $DKIM_SELECTOR  = Site::getPreference('DKIM_SELECTOR');
374
        $DKIM_KEY       = Site::getPreference('DKIM_KEY');
375
376
        $smtp_from_name_valid = $this->mail_service->isValidEmail($SMTP_FROM_NAME);
377
        $smtp_helo_valid      = filter_var($SMTP_HELO, FILTER_VALIDATE_DOMAIN);
378
379
        return $this->viewResponse('admin/site-mail', [
380
            'mail_ssl_options'       => $mail_ssl_options,
381
            'mail_transport_options' => $mail_transport_options,
382
            'title'                  => $title,
383
            'smtp_helo_valid'        => $smtp_helo_valid,
384
            'smtp_from_name_valid'   => $smtp_from_name_valid,
385
            'SMTP_ACTIVE'            => $SMTP_ACTIVE,
386
            'SMTP_AUTH'              => $SMTP_AUTH,
387
            'SMTP_AUTH_USER'         => $SMTP_AUTH_USER,
388
            'SMTP_FROM_NAME'         => $SMTP_FROM_NAME,
389
            'SMTP_HELO'              => $SMTP_HELO,
390
            'SMTP_HOST'              => $SMTP_HOST,
391
            'SMTP_PORT'              => $SMTP_PORT,
392
            'SMTP_SSL'               => $SMTP_SSL,
393
            'DKIM_DOMAIN'            => $DKIM_DOMAIN,
394
            'DKIM_SELECTOR'          => $DKIM_SELECTOR,
395
            'DKIM_KEY'               => $DKIM_KEY,
396
        ]);
397
    }
398
399
    /**
400
     * @param ServerRequestInterface $request
401
     *
402
     * @return ResponseInterface
403
     */
404
    public function mailSave(ServerRequestInterface $request): ResponseInterface
405
    {
406
        $params = $request->getParsedBody();
407
408
        Site::setPreference('SMTP_ACTIVE', $params['SMTP_ACTIVE']);
409
        Site::setPreference('SMTP_FROM_NAME', $params['SMTP_FROM_NAME']);
410
        Site::setPreference('SMTP_HOST', $params['SMTP_HOST']);
411
        Site::setPreference('SMTP_PORT', $params['SMTP_PORT']);
412
        Site::setPreference('SMTP_AUTH', $params['SMTP_AUTH']);
413
        Site::setPreference('SMTP_AUTH_USER', $params['SMTP_AUTH_USER']);
414
        Site::setPreference('SMTP_SSL', $params['SMTP_SSL']);
415
        Site::setPreference('SMTP_HELO', $params['SMTP_HELO']);
416
        Site::setPreference('DKIM_DOMAIN', $params['DKIM_DOMAIN']);
417
        Site::setPreference('DKIM_SELECTOR', $params['DKIM_SELECTOR']);
418
        Site::setPreference('DKIM_KEY', $params['DKIM_KEY']);
419
420
        if ($params['SMTP_AUTH_PASS'] !== '') {
421
            Site::setPreference('SMTP_AUTH_PASS', $params['SMTP_AUTH_PASS']);
422
        }
423
424
        FlashMessages::addMessage(I18N::translate('The website preferences have been updated.'), 'success');
425
        $url = route(ControlPanel::class);
426
427
        return redirect($url);
428
    }
429
430
    /**
431
     * @param ServerRequestInterface $request
432
     *
433
     * @return ResponseInterface
434
     */
435
    public function preferencesForm(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

435
    public function preferencesForm(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
436
    {
437
        $all_themes = $this->themeOptions();
438
439
        $title = I18N::translate('Website preferences');
440
441
        return $this->viewResponse('admin/site-preferences', [
442
            'all_themes'         => $all_themes,
443
            'max_execution_time' => (int) get_cfg_var('max_execution_time'),
444
            'title'              => $title,
445
        ]);
446
    }
447
448
    /**
449
     * @return Collection
450
     */
451
    private function themeOptions(): Collection
452
    {
453
        return $this->module_service
454
            ->findByInterface(ModuleThemeInterface::class)
455
            ->map($this->module_service->titleMapper());
456
    }
457
458
    /**
459
     * @param ServerRequestInterface $request
460
     *
461
     * @return ResponseInterface
462
     */
463
    public function preferencesSave(ServerRequestInterface $request): ResponseInterface
464
    {
465
        $params = $request->getParsedBody();
466
467
        $INDEX_DIRECTORY = $params['INDEX_DIRECTORY'];
468
        if (substr($INDEX_DIRECTORY, -1) !== '/') {
469
            $INDEX_DIRECTORY .= '/';
470
        }
471
        if (is_dir($INDEX_DIRECTORY)) {
472
            Site::setPreference('INDEX_DIRECTORY', $INDEX_DIRECTORY);
473
        } else {
474
            FlashMessages::addMessage(I18N::translate('The folder “%s” does not exist.', e($INDEX_DIRECTORY)), 'danger');
475
        }
476
477
        Site::setPreference('THEME_DIR', $params['THEME_DIR']);
478
        Site::setPreference('ALLOW_CHANGE_GEDCOM', $params['ALLOW_CHANGE_GEDCOM']);
479
        Site::setPreference('TIMEZONE', $params['TIMEZONE']);
480
481
        FlashMessages::addMessage(I18N::translate('The website preferences have been updated.'), 'success');
482
        $url = route(ControlPanel::class);
483
484
        return redirect($url);
485
    }
486
487
    /**
488
     * @param ServerRequestInterface $request
489
     *
490
     * @return ResponseInterface
491
     */
492
    public function registrationForm(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

492
    public function registrationForm(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
493
    {
494
        $title = I18N::translate('Sign-in and registration');
495
496
        $registration_text_options = $this->registrationTextOptions();
497
498
        return $this->viewResponse('admin/site-registration', [
499
            'registration_text_options' => $registration_text_options,
500
            'title'                     => $title,
501
        ]);
502
    }
503
504
    /**
505
     * A list of registration rules (e.g. for an edit control).
506
     *
507
     * @return string[]
508
     */
509
    private function registrationTextOptions(): array
510
    {
511
        return [
512
            0 => I18N::translate('No predefined text'),
513
            1 => I18N::translate('Predefined text that states all users can request a user account'),
514
            2 => I18N::translate('Predefined text that states admin will decide on each request for a user account'),
515
            3 => I18N::translate('Predefined text that states only family members can request a user account'),
516
            4 => I18N::translate('Choose user defined welcome text typed below'),
517
        ];
518
    }
519
520
    /**
521
     * @param ServerRequestInterface $request
522
     *
523
     * @return ResponseInterface
524
     */
525
    public function registrationSave(ServerRequestInterface $request): ResponseInterface
526
    {
527
        $params = $request->getParsedBody();
528
529
        Site::setPreference('WELCOME_TEXT_AUTH_MODE', $params['WELCOME_TEXT_AUTH_MODE']);
530
        Site::setPreference('WELCOME_TEXT_AUTH_MODE_' . WT_LOCALE, $params['WELCOME_TEXT_AUTH_MODE_4']);
531
        Site::setPreference('USE_REGISTRATION_MODULE', $params['USE_REGISTRATION_MODULE']);
532
        Site::setPreference('SHOW_REGISTER_CAUTION', $params['SHOW_REGISTER_CAUTION']);
533
534
        FlashMessages::addMessage(I18N::translate('The website preferences have been updated.'), 'success');
535
        $url = route(ControlPanel::class);
536
537
        return redirect($url);
538
    }
539
}
540