Passed
Pull Request — main (#5282)
by
unknown
08:15
created

ManageMediaData::handle()   D

Complexity

Conditions 15
Paths 5

Size

Total Lines 179
Code Lines 119

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 119
nc 5
nop 1
dl 0
loc 179
rs 4.7333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\RequestHandlers;
21
22
use Fisharebest\Webtrees\DB;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\DB was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException;
24
use Fisharebest\Webtrees\I18N;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\I18N was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
use Fisharebest\Webtrees\Media;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Media was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use Fisharebest\Webtrees\Mime;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Mime was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
use Fisharebest\Webtrees\Registry;
28
use Fisharebest\Webtrees\Services\DatatablesService;
29
use Fisharebest\Webtrees\Services\LinkedRecordService;
30
use Fisharebest\Webtrees\Services\MediaFileService;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Services\MediaFileService was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
31
use Fisharebest\Webtrees\Services\TreeService;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Services\TreeService was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
32
use Fisharebest\Webtrees\Validator;
33
use Illuminate\Database\Query\Builder;
34
use Illuminate\Database\Query\Expression;
35
use Illuminate\Database\Query\JoinClause;
36
use League\Flysystem\FilesystemException;
37
use League\Flysystem\FilesystemOperator;
38
use League\Flysystem\UnableToCheckFileExistence;
39
use League\Flysystem\UnableToReadFile;
40
use League\Flysystem\UnableToRetrieveMetadata;
41
use Psr\Http\Message\ResponseInterface;
42
use Psr\Http\Message\ServerRequestInterface;
43
use Psr\Http\Server\RequestHandlerInterface;
44
use Throwable;
45
46
use function assert;
47
use function e;
48
use function getimagesizefromstring;
49
use function intdiv;
50
use function route;
51
use function str_starts_with;
52
use function strlen;
53
use function substr;
54
use function view;
55
56
final class ManageMediaData implements RequestHandlerInterface
57
{
58
    private DatatablesService $datatables_service;
59
60
    private LinkedRecordService $linked_record_service;
61
62
    private MediaFileService $media_file_service;
63
64
    private TreeService $tree_service;
65
66
    /**
67
     * @param DatatablesService   $datatables_service
68
     * @param LinkedRecordService $linked_record_service
69
     * @param MediaFileService    $media_file_service
70
     * @param TreeService         $tree_service
71
     */
72
    public function __construct(
73
        DatatablesService $datatables_service,
74
        LinkedRecordService $linked_record_service,
75
        MediaFileService $media_file_service,
76
        TreeService $tree_service
77
    ) {
78
        $this->datatables_service    = $datatables_service;
79
        $this->linked_record_service = $linked_record_service;
80
        $this->media_file_service    = $media_file_service;
81
        $this->tree_service          = $tree_service;
82
    }
83
84
    public function handle(ServerRequestInterface $request): ResponseInterface
85
    {
86
        $this->fixTreePreferences();
87
        $data_filesystem = Registry::filesystem()->data();
88
89
        $files = Validator::queryParams($request)->isInArray(['local', 'external', 'unused'])->string('files');
90
91
        // Files within this folder
92
        $media_folders = $this->media_file_service->allMediaFolders($data_filesystem)->all();
93
        $media_folder  = Validator::queryParams($request)->isInArray($media_folders)->string('media_folder');
94
95
        // Show sub-folders within $media_folder
96
        $subfolders = Validator::queryParams($request)->isInArray(['include', 'exclude'])->string('subfolders');
97
98
        $search_columns = ['multimedia_file_refn', 'descriptive_title'];
99
100
        $sort_columns = [
101
            0 => 'multimedia_file_refn',
102
            2 => new Expression(DB::concat(['descriptive_title', 'multimedia_file_refn'])),
103
        ];
104
105
        // Convert a row from the database into a row for datatables
106
        $callback = function (object $row): array {
107
            $tree  = $this->tree_service->find((int) $row->m_file);
108
            $media = Registry::mediaFactory()->make($row->m_id, $tree, $row->m_gedcom);
109
            assert($media instanceof Media);
110
111
            $is_http  = str_starts_with($row->multimedia_file_refn, 'http://');
112
            $is_https = str_starts_with($row->multimedia_file_refn, 'https://');
113
114
            if ($is_http || $is_https) {
115
                return [
116
                    '<a href="' . e($row->multimedia_file_refn) . '">' . e($row->multimedia_file_refn) . '</a>',
117
                    view('icons/mime', ['type' => Mime::DEFAULT_TYPE]),
118
                    $this->mediaObjectInfo($media),
119
                ];
120
            }
121
122
            try {
123
                $path = $row->media_folder . $row->multimedia_file_refn;
124
125
                try {
126
                    $mime_type = Registry::filesystem()->data()->mimeType($path);
127
                } catch (UnableToRetrieveMetadata) {
128
                    $mime_type = Mime::DEFAULT_TYPE;
129
                }
130
131
                if (str_starts_with($mime_type, 'image/')) {
132
                    $url = route(AdminMediaFileThumbnail::class, ['path' => $path]);
133
                    $img = '<img src="' . e($url) . '">';
134
                } else {
135
                    $img = view('icons/mime', ['type' => $mime_type]);
136
                }
137
138
                $url = route(AdminMediaFileDownload::class, ['path' => $path]);
139
                $img = '<a href="' . e($url) . '" type="' . $mime_type . '" class="gallery">' . $img . '</a>';
140
            } catch (UnableToReadFile) {
141
                $url = route(AdminMediaFileThumbnail::class, ['path' => $path]);
142
                $img = '<img src="' . e($url) . '">';
143
            }
144
145
            return [
146
                e($row->multimedia_file_refn),
147
                $img,
148
                $this->mediaObjectInfo($media),
149
            ];
150
        };
151
152
        switch ($files) {
153
            case 'local':
154
                $query = DB::table('media_file')
155
                    ->join('media', static function (JoinClause $join): void {
156
                        $join
157
                            ->on('media.m_file', '=', 'media_file.m_file')
158
                            ->on('media.m_id', '=', 'media_file.m_id');
159
                    })
160
                    ->leftJoin('gedcom_setting', static function (JoinClause $join): void {
161
                        $join
162
                            ->on('gedcom_setting.gedcom_id', '=', 'media.m_file')
163
                            ->where('setting_name', '=', 'MEDIA_DIRECTORY');
164
                    })
165
                    ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
166
                    ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
167
                    ->select([
168
                        'media.*',
169
                        'multimedia_file_refn',
170
                        'descriptive_title',
171
                        'setting_value AS media_folder',
172
                    ]);
173
174
                $query->where(new Expression(DB::concat(['setting_value', 'multimedia_file_refn'])), 'LIKE', $media_folder . '%');
175
176
                if ($subfolders === 'exclude') {
177
                    $query->where(new Expression(DB::concat(['setting_value', 'multimedia_file_refn'])), 'NOT LIKE', $media_folder . '%/%');
178
                }
179
180
                return $this->datatables_service->handleQuery($request, $query, $search_columns, $sort_columns, $callback);
181
182
            case 'external':
183
                $query = DB::table('media_file')
184
                    ->join('media', static function (JoinClause $join): void {
185
                        $join
186
                            ->on('media.m_file', '=', 'media_file.m_file')
187
                            ->on('media.m_id', '=', 'media_file.m_id');
188
                    })
189
                    ->where(static function (Builder $query): void {
190
                        $query
191
                            ->where('multimedia_file_refn', 'LIKE', 'http://%')
192
                            ->orWhere('multimedia_file_refn', 'LIKE', 'https://%');
193
                    })
194
                    ->select([
195
                        'media.*',
196
                        'multimedia_file_refn',
197
                        'descriptive_title',
198
                        new Expression("'' AS media_folder"),
0 ignored issues
show
Bug introduced by
''' AS media_folder' of type string is incompatible with the type Illuminate\Database\Query\TValue expected by parameter $value of Illuminate\Database\Quer...pression::__construct(). ( Ignorable by Annotation )

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

198
                        new Expression(/** @scrutinizer ignore-type */ "'' AS media_folder"),
Loading history...
199
                    ]);
200
201
                return $this->datatables_service->handleQuery($request, $query, $search_columns, $sort_columns, $callback);
202
203
            case 'unused':
204
                // Which trees use which media folder?
205
                $media_trees = DB::table('gedcom')
206
                    ->join('gedcom_setting', 'gedcom_setting.gedcom_id', '=', 'gedcom.gedcom_id')
207
                    ->where('setting_name', '=', 'MEDIA_DIRECTORY')
208
                    ->where('gedcom.gedcom_id', '>', 0)
209
                    ->pluck('setting_value', 'gedcom_name');
210
211
                $disk_files = $this->media_file_service->allFilesOnDisk($data_filesystem, $media_folder, $subfolders === 'include');
212
                $db_files   = $this->media_file_service->allFilesInDatabase($media_folder, $subfolders === 'include');
213
214
                // All unused files
215
                $unused_files = $disk_files->diff($db_files)
216
                    ->map(static fn (string $file): array => (array) $file);
217
218
                $search_columns = [0];
219
                $sort_columns   = [0 => 0];
220
221
                $callback = function (array $row) use ($data_filesystem, $media_trees): array {
222
                    try {
223
                        $mime_type = $data_filesystem->mimeType($row[0]) ?: Mime::DEFAULT_TYPE;
224
                    } catch (FilesystemException | UnableToRetrieveMetadata) {
225
                        $mime_type = Mime::DEFAULT_TYPE;
226
                    }
227
228
                    if (str_starts_with($mime_type, 'image/')) {
229
                        $url = route(AdminMediaFileThumbnail::class, ['path' => $row[0]]);
230
                        $img = '<img src="' . e($url) . '">';
231
                    } else {
232
                        $img = view('icons/mime', ['type' => $mime_type]);
233
                    }
234
235
                    $url = route(AdminMediaFileDownload::class, ['path' => $row[0]]);
236
                    $img = '<a href="' . e($url) . '">' . $img . '</a>';
237
238
                    // Form to create new media object in each tree
239
                    $create_form = '';
240
                    foreach ($media_trees as $media_tree => $media_directory) {
241
                        if (str_starts_with($row[0], $media_directory)) {
242
                            $tmp = substr($row[0], strlen($media_directory));
243
                            $create_form .=
244
                                '<p><a href="#" data-bs-toggle="modal" data-bs-target="#modal-create-media-from-file" data-file="' . e($tmp) . '" data-url="' . e(route(CreateMediaObjectFromFile::class, ['tree' => $media_tree])) . '" onclick="document.getElementById(\'modal-create-media-from-file-form\').action=this.dataset.url; document.getElementById(\'file\').value=this.dataset.file;">' . I18N::translate('Create') . '</a> — ' . e($media_tree) . '<p>';
245
                        }
246
                    }
247
248
                    $delete_link = '<p><a data-wt-confirm="' . I18N::translate('Are you sure you want to delete “%s”?', e($row[0])) . '" data-wt-post-url="' . e(route(DeletePath::class, [
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Htt...uestHandlers\DeletePath was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
249
                            'path' => $row[0],
250
                        ])) . '" href="#">' . I18N::translate('Delete') . '</a></p>';
251
252
                    return [
253
                        $this->mediaFileInfo($data_filesystem, $row[0]) . $delete_link,
254
                        $img,
255
                        $create_form,
256
                    ];
257
                };
258
259
                return $this->datatables_service->handleCollection($request, $unused_files, $search_columns, $sort_columns, $callback);
260
261
            default:
262
                throw new HttpNotFoundException();
263
        }
264
    }
265
266
    /**
267
     * Generate some useful information and links about a media object.
268
     *
269
     * @param Media $media
270
     *
271
     * @return string HTML
272
     */
273
    private function mediaObjectInfo(Media $media): string
274
    {
275
        $element = Registry::elementFactory()->make('NOTE:CONC');
276
        $html    = '<a href="' . e($media->url()) . '" title="' . e($media->tree()->title()) . '">' . $media->fullName() . '</a>';
277
278
        if ($this->tree_service->all()->count() > 1) {
279
            $html .= ' — ' . e($media->tree()->title());
280
        }
281
282
        $html .= $element->value($media->getNote(), $media->tree());
283
284
        $linked = [];
285
286
        foreach ($this->linked_record_service->linkedIndividuals($media) as $link) {
287
            $linked[] = view('icons/individual') . '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
288
        }
289
290
        foreach ($this->linked_record_service->linkedFamilies($media) as $link) {
291
            $linked[] = view('icons/family') . '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
292
        }
293
294
        foreach ($this->linked_record_service->linkedSources($media) as $link) {
295
            $linked[] = view('icons/source') . '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
296
        }
297
298
        foreach ($this->linked_record_service->linkedNotes($media) as $link) {
299
            $linked[] = view('icons/note') . '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
300
        }
301
302
        foreach ($this->linked_record_service->linkedRepositories($media) as $link) {
303
            $linked[] = view('icons/media') . '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
304
        }
305
306
        foreach ($this->linked_record_service->linkedMedia($media) as $link) {
307
            $linked[] = view('icons/location') . '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
308
        }
309
310
        if ($linked !== []) {
311
            $html .= '<ul class="list-unstyled">';
312
            foreach ($linked as $link) {
313
                $html .= '<li>' . $link . '</li>';
314
            }
315
            $html .= '</ul>';
316
        } else {
317
            $html .= '<div class="alert alert-danger">' . I18N::translate('There are no links to this media object.') . '</div>';
318
        }
319
320
        return $html;
321
    }
322
323
    /**
324
     * Generate some useful information and links about a media file.
325
     *
326
     * @param FilesystemOperator $data_filesystem
327
     * @param string             $file
328
     *
329
     * @return string
330
     */
331
    private function mediaFileInfo(FilesystemOperator $data_filesystem, string $file): string
332
    {
333
        $html = '<dl>';
334
        $html .= '<dt>' . I18N::translate('Filename') . '</dt>';
335
        $html .= '<dd>' . e($file) . '</dd>';
336
337
        try {
338
            $file_exists = $data_filesystem->fileExists($file);
339
        } catch (FilesystemException | UnableToCheckFileExistence) {
340
            $file_exists = false;
341
        }
342
343
        if ($file_exists) {
344
            try {
345
                $size = $data_filesystem->fileSize($file);
346
            } catch (FilesystemException | UnableToRetrieveMetadata) {
347
                $size = 0;
348
            }
349
            $size = intdiv($size + 1023, 1024); // Round up to next KB
350
            /* I18N: size of file in KB */
351
            $size = I18N::translate('%s KB', I18N::number($size));
352
            $html .= '<dt>' . I18N::translate('File size') . '</dt>';
353
            $html .= '<dd>' . $size . '</dd>';
354
355
            try {
356
                // This will work for local filesystems.  For remote filesystems, we will
357
                // need to copy the file locally to work out the image size.
358
                $imgsize = getimagesizefromstring($data_filesystem->read($file));
359
                $html .= '<dt>' . I18N::translate('Image dimensions') . '</dt>';
360
                /* I18N: image dimensions, width × height */
361
                $html .= '<dd>' . I18N::translate('%1$s × %2$s pixels', I18N::number($imgsize[0]), I18N::number($imgsize[1])) . '</dd>';
362
            } catch (FilesystemException | UnableToReadFile | Throwable) {
363
                // Not an image, or not a valid image?
364
            }
365
        }
366
367
        $html .= '</dl>';
368
369
        return $html;
370
    }
371
372
    private function fixTreePreferences() : void
373
    {
374
        foreach ($this->tree_service->all() as $tree) {
375
            if ($tree->getPreference('MEDIA_DIRECTORY', ':none:') === ':none:') {
376
                $defaultValue = $tree->getPreference('MEDIA_DIRECTORY');
377
                $tree->setPreference('MEDIA_DIRECTORY', $defaultValue, true);
378
            };
379
        }
380
    }
381
}
382