Completed
Push — develop ( c3a705...4b9394 )
by Greg
11:16
created

ManageMediaData::mediaFileInfo()   A

Complexity

Conditions 5
Paths 18

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 23
c 0
b 0
f 0
nc 18
nop 2
dl 0
loc 39
rs 9.2408
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2021 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\Exceptions\HttpNotFoundException;
23
use Fisharebest\Webtrees\I18N;
24
use Fisharebest\Webtrees\Media;
25
use Fisharebest\Webtrees\Mime;
26
use Fisharebest\Webtrees\Registry;
27
use Fisharebest\Webtrees\Services\DatatablesService;
28
use Fisharebest\Webtrees\Services\MediaFileService;
29
use Fisharebest\Webtrees\Services\TreeService;
30
use Illuminate\Database\Capsule\Manager as DB;
31
use Illuminate\Database\Query\Builder;
32
use Illuminate\Database\Query\Expression;
33
use Illuminate\Database\Query\JoinClause;
34
use League\Flysystem\FilesystemException;
35
use League\Flysystem\FilesystemOperator;
36
use League\Flysystem\UnableToCheckFileExistence;
37
use League\Flysystem\UnableToReadFile;
38
use League\Flysystem\UnableToRetrieveMetadata;
39
use Psr\Http\Message\ResponseInterface;
40
use Psr\Http\Message\ServerRequestInterface;
41
use Psr\Http\Server\RequestHandlerInterface;
42
use stdClass;
43
use Throwable;
44
45
use function assert;
46
use function e;
47
use function getimagesizefromstring;
48
use function intdiv;
49
use function route;
50
use function str_starts_with;
51
use function strlen;
52
use function substr;
53
use function view;
54
55
/**
56
 * Manage media from the control panel.
57
 */
58
class ManageMediaData implements RequestHandlerInterface
59
{
60
    /** @var DatatablesService */
61
    private $datatables_service;
62
63
    /** @var MediaFileService */
64
    private $media_file_service;
65
66
    /** @var TreeService */
67
    private $tree_service;
68
69
    /**
70
     * MediaController constructor.
71
     *
72
     * @param DatatablesService $datatables_service
73
     * @param MediaFileService  $media_file_service
74
     * @param TreeService       $tree_service
75
     */
76
    public function __construct(
77
        DatatablesService $datatables_service,
78
        MediaFileService $media_file_service,
79
        TreeService $tree_service
80
    ) {
81
        $this->datatables_service = $datatables_service;
82
        $this->media_file_service = $media_file_service;
83
        $this->tree_service       = $tree_service;
84
    }
85
86
    /**
87
     * @param ServerRequestInterface $request
88
     *
89
     * @return ResponseInterface
90
     */
91
    public function handle(ServerRequestInterface $request): ResponseInterface
92
    {
93
        $data_filesystem = Registry::filesystem()->data();
94
95
        $files = $request->getQueryParams()['files']; // local|external|unused
96
97
        // Files within this folder
98
        $media_folder = $request->getQueryParams()['media_folder'];
99
100
        // Show sub-folders within $media_folder
101
        $subfolders = $request->getQueryParams()['subfolders']; // include|exclude
102
103
        $search_columns = ['multimedia_file_refn', 'descriptive_title'];
104
105
        $sort_columns = [
106
            0 => 'multimedia_file_refn',
107
            2 => new Expression('descriptive_title || multimedia_file_refn'),
108
        ];
109
110
        // Convert a row from the database into a row for datatables
111
        $callback = function (stdClass $row): array {
112
            $tree  = $this->tree_service->find((int) $row->m_file);
113
            $media = Registry::mediaFactory()->make($row->m_id, $tree, $row->m_gedcom);
114
            assert($media instanceof Media);
115
116
            $is_http  = str_starts_with($row->multimedia_file_refn, 'http://');
117
            $is_https = str_starts_with($row->multimedia_file_refn, 'https://');
118
119
            if ($is_http || $is_https) {
120
                return [
121
                    '<a href="' . e($row->multimedia_file_refn) . '">' . e($row->multimedia_file_refn) . '</a>',
122
                    view('icons/mime', ['type' => Mime::DEFAULT_TYPE]),
123
                    $this->mediaObjectInfo($media),
124
                ];
125
            }
126
127
            try {
128
                $path = $row->media_folder . $row->multimedia_file_refn;
129
130
                try {
131
                    $mime_type = Registry::filesystem()->data()->mimeType($path);
132
                } catch (UnableToRetrieveMetadata $ex) {
133
                    $mime_type = Mime::DEFAULT_TYPE;
134
                }
135
136
                if (str_starts_with($mime_type, 'image/')) {
137
                    $url = route(AdminMediaFileThumbnail::class, ['path' => $path]);
138
                    $img = '<img src="' . e($url) . '">';
139
                } else {
140
                    $img = view('icons/mime', ['type' => $mime_type]);
141
                }
142
143
                $url = route(AdminMediaFileDownload::class, ['path' => $path]);
144
                $img = '<a href="' . e($url) . '" type="' . $mime_type . '" class="gallery">' . $img . '</a>';
145
            } catch (UnableToReadFile $ex) {
146
                $url = route(AdminMediaFileThumbnail::class, ['path' => $path]);
147
                $img = '<img src="' . e($url) . '">';
148
            }
149
150
            return [
151
                e($row->multimedia_file_refn),
152
                $img,
153
                $this->mediaObjectInfo($media),
154
            ];
155
        };
156
157
        switch ($files) {
158
            case 'local':
159
                $query = DB::table('media_file')
160
                    ->join('media', static function (JoinClause $join): void {
161
                        $join
162
                            ->on('media.m_file', '=', 'media_file.m_file')
163
                            ->on('media.m_id', '=', 'media_file.m_id');
164
                    })
165
                    ->join('gedcom_setting', 'gedcom_id', '=', 'media.m_file')
166
                    ->where('setting_name', '=', 'MEDIA_DIRECTORY')
167
                    ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
168
                    ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
169
                    ->select([
170
                        'media.*',
171
                        'multimedia_file_refn',
172
                        'descriptive_title',
173
                        'setting_value AS media_folder',
174
                    ]);
175
176
                $query->where(new Expression('setting_value || multimedia_file_refn'), 'LIKE', $media_folder . '%');
177
178
                if ($subfolders === 'exclude') {
179
                    $query->where(new Expression('setting_value || multimedia_file_refn'), 'NOT LIKE', $media_folder . '%/%');
180
                }
181
182
                return $this->datatables_service->handleQuery($request, $query, $search_columns, $sort_columns, $callback);
183
184
            case 'external':
185
                $query = DB::table('media_file')
186
                    ->join('media', static function (JoinClause $join): void {
187
                        $join
188
                            ->on('media.m_file', '=', 'media_file.m_file')
189
                            ->on('media.m_id', '=', 'media_file.m_id');
190
                    })
191
                    ->where(static function (Builder $query): void {
192
                        $query
193
                            ->where('multimedia_file_refn', 'LIKE', 'http://%')
194
                            ->orWhere('multimedia_file_refn', 'LIKE', 'https://%');
195
                    })
196
                    ->select([
197
                        'media.*',
198
                        'multimedia_file_refn',
199
                        'descriptive_title',
200
                        new Expression("'' AS media_folder"),
201
                    ]);
202
203
                return $this->datatables_service->handleQuery($request, $query, $search_columns, $sort_columns, $callback);
204
205
            case 'unused':
206
                // Which trees use which media folder?
207
                $media_trees = DB::table('gedcom')
208
                    ->join('gedcom_setting', 'gedcom_setting.gedcom_id', '=', 'gedcom.gedcom_id')
209
                    ->where('setting_name', '=', 'MEDIA_DIRECTORY')
210
                    ->where('gedcom.gedcom_id', '>', 0)
211
                    ->pluck('setting_value', 'gedcom_name');
212
213
                $disk_files = $this->media_file_service->allFilesOnDisk($data_filesystem, $media_folder, $subfolders === 'include');
214
                $db_files   = $this->media_file_service->allFilesInDatabase($media_folder, $subfolders === 'include');
215
216
                // All unused files
217
                $unused_files = $disk_files->diff($db_files)
218
                    ->map(static function (string $file): array {
219
                        return (array) $file;
220
                    });
221
222
                $search_columns = [0];
223
                $sort_columns   = [0 => 0];
224
225
                $callback = function (array $row) use ($data_filesystem, $media_trees): array {
226
                    try {
227
                        $mime_type = $data_filesystem->mimeType($row[0]) ?: Mime::DEFAULT_TYPE;
228
                    } catch (FileSystemException | UnableToRetrieveMetadata $ex) {
229
                        $mime_type = Mime::DEFAULT_TYPE;
230
                    }
231
232
233
                    if (str_starts_with($mime_type, 'image/')) {
234
                        $url = route(AdminMediaFileThumbnail::class, ['path' => $row[0]]);
235
                        $img = '<img src="' . e($url) . '">';
236
                    } else {
237
                        $img = view('icons/mime', ['type' => $mime_type]);
238
                    }
239
240
                    $url = route(AdminMediaFileDownload::class, ['path' => $row[0]]);
241
                    $img = '<a href="' . e($url) . '">' . $img . '</a>';
242
243
                    // Form to create new media object in each tree
244
                    $create_form = '';
245
                    foreach ($media_trees as $media_tree => $media_directory) {
246
                        if (str_starts_with($row[0], $media_directory)) {
247
                            $tmp         = substr($row[0], strlen($media_directory));
248
                            $create_form .=
249
                                '<p><a href="#" data-toggle="modal" data-backdrop="static" data-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>';
250
                        }
251
                    }
252
253
                    $delete_link = '<p><a data-confirm="' . I18N::translate('Are you sure you want to delete “%s”?', e($row[0])) . '" data-post-url="' . e(route(DeletePath::class, [
254
                            'path' => $row[0],
255
                        ])) . '" href="#">' . I18N::translate('Delete') . '</a></p>';
256
257
                    return [
258
                        $this->mediaFileInfo($data_filesystem, $row[0]) . $delete_link,
259
                        $img,
260
                        $create_form,
261
                    ];
262
                };
263
264
                return $this->datatables_service->handleCollection($request, $unused_files, $search_columns, $sort_columns, $callback);
265
266
            default:
267
                throw new HttpNotFoundException();
268
        }
269
    }
270
271
    /**
272
     * Generate some useful information and links about a media object.
273
     *
274
     * @param Media $media
275
     *
276
     * @return string HTML
277
     */
278
    private function mediaObjectInfo(Media $media): string
279
    {
280
        $html = '<b><a href="' . e($media->url()) . '">' . $media->fullName() . '</a></b>' . '<br><i>' . e($media->getNote()) . '</i></br><br>';
281
282
        $linked = [];
283
        foreach ($media->linkedIndividuals('OBJE') as $link) {
284
            $linked[] = '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
285
        }
286
        foreach ($media->linkedFamilies('OBJE') as $link) {
287
            $linked[] = '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
288
        }
289
        foreach ($media->linkedSources('OBJE') as $link) {
290
            $linked[] = '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
291
        }
292
        foreach ($media->linkedNotes('OBJE') as $link) {
293
            // Invalid GEDCOM - you cannot link a NOTE to an OBJE
294
            $linked[] = '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
295
        }
296
        foreach ($media->linkedRepositories('OBJE') as $link) {
297
            // Invalid GEDCOM - you cannot link a REPO to an OBJE
298
            $linked[] = '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
299
        }
300
        foreach ($media->linkedLocations('OBJE') as $link) {
301
            $linked[] = '<a href="' . e($link->url()) . '">' . $link->fullName() . '</a>';
302
        }
303
        if ($linked !== []) {
304
            $html .= '<ul>';
305
            foreach ($linked as $link) {
306
                $html .= '<li>' . $link . '</li>';
307
            }
308
            $html .= '</ul>';
309
        } else {
310
            $html .= '<div class="alert alert-danger">' . I18N::translate('There are no links to this media object.') . '</div>';
311
        }
312
313
        return $html;
314
    }
315
316
    /**
317
     * Generate some useful information and links about a media file.
318
     *
319
     * @param FilesystemOperator $data_filesystem
320
     * @param string             $file
321
     *
322
     * @return string
323
     */
324
    private function mediaFileInfo(FilesystemOperator $data_filesystem, string $file): string
325
    {
326
        $html = '<dl>';
327
        $html .= '<dt>' . I18N::translate('Filename') . '</dt>';
328
        $html .= '<dd>' . e($file) . '</dd>';
329
330
        try {
331
            $file_exists = $data_filesystem->fileExists($file);
332
        } catch (FilesystemException | UnableToCheckFileExistence $ex) {
333
            $file_exists = false;
334
        }
335
336
        if ($file_exists) {
337
            try {
338
                $size = $data_filesystem->fileSize($file);
339
            } catch (FilesystemException | UnableToRetrieveMetadata $ex) {
340
                $size = 0;
341
            }
342
            $size = intdiv($size + 1023, 1024); // Round up to next KB
343
            /* I18N: size of file in KB */
344
            $size = I18N::translate('%s KB', I18N::number($size));
345
            $html .= '<dt>' . I18N::translate('File size') . '</dt>';
346
            $html .= '<dd>' . $size . '</dd>';
347
348
            try {
349
                // This will work for local filesystems.  For remote filesystems, we will
350
                // need to copy the file locally to work out the image size.
351
                $imgsize = getimagesizefromstring($data_filesystem->read($file));
352
                $html    .= '<dt>' . I18N::translate('Image dimensions') . '</dt>';
353
                /* I18N: image dimensions, width × height */
354
                $html .= '<dd>' . I18N::translate('%1$s × %2$s pixels', I18N::number($imgsize['0']), I18N::number($imgsize['1'])) . '</dd>';
355
            } catch (FilesystemException | UnableToReadFile | Throwable $ex) {
356
                // Not an image, or not a valid image?
357
            }
358
        }
359
360
        $html .= '</dl>';
361
362
        return $html;
363
    }
364
}
365