Completed
Push — test ( fcdd70...8957cd )
by Greg
24:03 queued 04:42
created

MediaFileService::allMediaFolders()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 40
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 28
nc 2
nop 1
dl 0
loc 40
rs 9.472
c 0
b 0
f 0
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\Services;
21
22
use Fisharebest\Webtrees\Registry;
23
use Fisharebest\Webtrees\FlashMessages;
24
use Fisharebest\Webtrees\GedcomTag;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Tree;
27
use Illuminate\Database\Capsule\Manager as DB;
28
use Illuminate\Database\Query\Expression;
29
use Illuminate\Support\Collection;
30
use InvalidArgumentException;
31
use League\Flysystem\FilesystemInterface;
32
use Psr\Http\Message\ServerRequestInterface;
33
use Psr\Http\Message\UploadedFileInterface;
34
use RuntimeException;
35
36
use function array_combine;
37
use function array_diff;
38
use function array_filter;
39
use function array_map;
40
use function assert;
41
use function dirname;
42
use function ini_get;
43
use function intdiv;
44
use function min;
45
use function pathinfo;
46
use function preg_replace;
47
use function sha1;
48
use function sort;
49
use function str_contains;
50
use function strtolower;
51
use function strtr;
52
use function substr;
53
use function trim;
54
55
use const PATHINFO_EXTENSION;
56
use const UPLOAD_ERR_OK;
57
58
/**
59
 * Managing media files.
60
 */
61
class MediaFileService
62
{
63
    public const EDIT_RESTRICTIONS = [
64
        'locked',
65
    ];
66
67
    public const PRIVACY_RESTRICTIONS = [
68
        'none',
69
        'privacy',
70
        'confidential',
71
    ];
72
73
    public const EXTENSION_TO_FORM = [
74
        'jpg' => 'jpeg',
75
        'tif' => 'tiff',
76
    ];
77
78
    /**
79
     * What is the largest file a user may upload?
80
     */
81
    public function maxUploadFilesize(): string
82
    {
83
        $sizePostMax = $this->parseIniFileSize(ini_get('post_max_size'));
84
        $sizeUploadMax = $this->parseIniFileSize(ini_get('upload_max_filesize'));
85
86
        $bytes =  min($sizePostMax, $sizeUploadMax);
87
        $kb    = intdiv($bytes + 1023, 1024);
88
89
        return I18N::translate('%s KB', I18N::number($kb));
90
    }
91
92
    /**
93
     * Returns the given size from an ini value in bytes.
94
     *
95
     * @param string $size
96
     *
97
     * @return int
98
     */
99
    private function parseIniFileSize(string $size): int
100
    {
101
        $number = (int) $size;
102
103
        switch (substr($size, -1)) {
104
            case 'g':
105
            case 'G':
106
                return $number * 1073741824;
107
            case 'm':
108
            case 'M':
109
                return $number * 1048576;
110
            case 'k':
111
            case 'K':
112
                return $number * 1024;
113
            default:
114
                return $number;
115
        }
116
    }
117
118
    /**
119
     * A list of key/value options for media types.
120
     *
121
     * @param string $current
122
     *
123
     * @return array<int|string,string>
124
     *
125
     * @deprecated - Will be removed in 2.1.0 - use Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE')->values()
126
     */
127
    public function mediaTypes($current = ''): array
0 ignored issues
show
Unused Code introduced by
The parameter $current 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

127
    public function mediaTypes(/** @scrutinizer ignore-unused */ $current = ''): array

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...
128
    {
129
        return Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE')->values();
130
    }
131
132
    /**
133
     * A list of media files not already linked to a media object.
134
     *
135
     * @param Tree                $tree
136
     * @param FilesystemInterface $data_filesystem
137
     *
138
     * @return array<string>
139
     */
140
    public function unusedFiles(Tree $tree, FilesystemInterface $data_filesystem): array
141
    {
142
        $used_files = DB::table('media_file')
143
            ->where('m_file', '=', $tree->id())
144
            ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
145
            ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
146
            ->pluck('multimedia_file_refn')
147
            ->all();
148
149
        $disk_files = $tree->mediaFilesystem($data_filesystem)->listContents('', true);
150
151
        $disk_files = array_filter($disk_files, static function (array $item) {
152
            // Older versions of webtrees used a couple of special folders.
153
            return
154
                $item['type'] === 'file' &&
155
                !str_contains($item['path'], '/thumbs/') &&
156
                !str_contains($item['path'], '/watermarks/');
157
        });
158
159
        $disk_files = array_map(static function (array $item): string {
160
            return $item['path'];
161
        }, $disk_files);
162
163
        $unused_files = array_diff($disk_files, $used_files);
164
165
        sort($unused_files);
166
167
        return array_combine($unused_files, $unused_files);
168
    }
169
170
    /**
171
     * Store an uploaded file (or URL), either to be added to a media object
172
     * or to create a media object.
173
     *
174
     * @param ServerRequestInterface $request
175
     *
176
     * @return string The value to be stored in the 'FILE' field of the media object.
177
     */
178
    public function uploadFile(ServerRequestInterface $request): string
179
    {
180
        $tree = $request->getAttribute('tree');
181
        assert($tree instanceof Tree);
182
183
        $data_filesystem = Registry::filesystem()->data();
184
185
        $params        = (array) $request->getParsedBody();
186
        $file_location = $params['file_location'];
187
188
        switch ($file_location) {
189
            case 'url':
190
                $remote = $params['remote'];
191
192
                if (str_contains($remote, '://')) {
193
                    return $remote;
194
                }
195
196
                return '';
197
198
            case 'unused':
199
                $unused = $params['unused'];
200
201
                if ($tree->mediaFilesystem($data_filesystem)->has($unused)) {
202
                    return $unused;
203
                }
204
205
                return '';
206
207
            case 'upload':
208
            default:
209
                $folder   = $params['folder'];
210
                $auto     = $params['auto'];
211
                $new_file = $params['new_file'];
212
213
                /** @var UploadedFileInterface|null $uploaded_file */
214
                $uploaded_file = $request->getUploadedFiles()['file'];
215
                if ($uploaded_file === null || $uploaded_file->getError() !== UPLOAD_ERR_OK) {
216
                    return '';
217
                }
218
219
                // The filename
220
                $new_file = strtr($new_file, ['\\' => '/']);
221
                if ($new_file !== '' && !str_contains($new_file, '/')) {
222
                    $file = $new_file;
223
                } else {
224
                    $file = $uploaded_file->getClientFilename();
225
                }
226
227
                // The folder
228
                $folder = strtr($folder, ['\\' => '/']);
229
                $folder = trim($folder, '/');
230
                if ($folder !== '') {
231
                    $folder .= '/';
232
                }
233
234
                // Generate a unique name for the file?
235
                if ($auto === '1' || $tree->mediaFilesystem($data_filesystem)->has($folder . $file)) {
236
                    $folder    = '';
237
                    $extension = pathinfo($uploaded_file->getClientFilename(), PATHINFO_EXTENSION);
0 ignored issues
show
Bug introduced by
It seems like $uploaded_file->getClientFilename() can also be of type null; however, parameter $path of pathinfo() does only seem to accept string, 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

237
                    $extension = pathinfo(/** @scrutinizer ignore-type */ $uploaded_file->getClientFilename(), PATHINFO_EXTENSION);
Loading history...
238
                    $file      = sha1((string) $uploaded_file->getStream()) . '.' . $extension;
1 ignored issue
show
Bug introduced by
Are you sure $extension of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

238
                    $file      = sha1((string) $uploaded_file->getStream()) . '.' . /** @scrutinizer ignore-type */ $extension;
Loading history...
239
                }
240
241
                try {
242
                    $tree->mediaFilesystem($data_filesystem)->putStream($folder . $file, $uploaded_file->getStream()->detach());
243
244
                    return $folder . $file;
245
                } catch (RuntimeException | InvalidArgumentException $ex) {
246
                    FlashMessages::addMessage(I18N::translate('There was an error uploading your file.'));
247
248
                    return '';
249
                }
250
        }
251
    }
252
253
    /**
254
     * Convert the media file attributes into GEDCOM format.
255
     *
256
     * @param string $file
257
     * @param string $type
258
     * @param string $title
259
     * @param string $note
260
     *
261
     * @return string
262
     */
263
    public function createMediaFileGedcom(string $file, string $type, string $title, string $note): string
264
    {
265
        // Tidy non-printing characters
266
        $type  = trim(preg_replace('/\s+/', ' ', $type));
267
        $title = trim(preg_replace('/\s+/', ' ', $title));
268
269
        $gedcom = '1 FILE ' . $file;
270
271
        $format = strtolower(pathinfo($file, PATHINFO_EXTENSION));
1 ignored issue
show
Bug introduced by
It seems like pathinfo($file, PATHINFO_EXTENSION) can also be of type array; however, parameter $string of strtolower() does only seem to accept string, 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

271
        $format = strtolower(/** @scrutinizer ignore-type */ pathinfo($file, PATHINFO_EXTENSION));
Loading history...
272
        $format = self::EXTENSION_TO_FORM[$format] ?? $format;
273
274
        if ($format !== '') {
275
            $gedcom .= "\n2 FORM " . $format;
276
        } elseif ($type !== '') {
277
            $gedcom .= "\n2 FORM";
278
        }
279
280
        if ($type !== '') {
281
            $gedcom .= "\n3 TYPE " . $type;
282
        }
283
284
        if ($title !== '') {
285
            $gedcom .= "\n2 TITL " . $title;
286
        }
287
288
        if ($note !== '') {
289
            // Convert HTML line endings to GEDCOM continuations
290
            $gedcom .= "\n1 NOTE " . strtr($note, ["\r\n" => "\n2 CONT "]);
291
        }
292
293
        return $gedcom;
294
    }
295
296
    /**
297
     * Fetch a list of all files on disk (in folders used by any tree).
298
     *
299
     * @param FilesystemInterface $data_filesystem Fileystem to search
300
     * @param string              $media_folder    Root folder
301
     * @param bool                $subfolders      Include subfolders
302
     *
303
     * @return Collection<string>
304
     */
305
    public function allFilesOnDisk(FilesystemInterface $data_filesystem, string $media_folder, bool $subfolders): Collection
306
    {
307
        $array = $data_filesystem->listContents($media_folder, $subfolders);
308
309
        return Collection::make($array)
310
            ->filter(static function (array $metadata): bool {
311
                return
312
                    $metadata['type'] === 'file' &&
313
                    !str_contains($metadata['path'], '/thumbs/') &&
314
                    !str_contains($metadata['path'], '/watermark/');
315
            })
316
            ->map(static function (array $metadata): string {
317
                return $metadata['path'];
318
            });
319
    }
320
321
    /**
322
     * Fetch a list of all files on in the database.
323
     *
324
     * @param string $media_folder Root folder
325
     * @param bool   $subfolders   Include subfolders
326
     *
327
     * @return Collection<string>
328
     */
329
    public function allFilesInDatabase(string $media_folder, bool $subfolders): Collection
330
    {
331
        $query = DB::table('media_file')
332
            ->join('gedcom_setting', 'gedcom_id', '=', 'm_file')
333
            ->where('setting_name', '=', 'MEDIA_DIRECTORY')
334
            //->where('multimedia_file_refn', 'LIKE', '%/%')
335
            ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
336
            ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
337
            ->where(new Expression('setting_value || multimedia_file_refn'), 'LIKE', $media_folder . '%')
338
            ->select(new Expression('setting_value || multimedia_file_refn AS path'))
339
            ->orderBy(new Expression('setting_value || multimedia_file_refn'));
340
341
        if (!$subfolders) {
342
            $query->where(new Expression('setting_value || multimedia_file_refn'), 'NOT LIKE', $media_folder . '%/%');
343
        }
344
345
        return $query->pluck('path');
346
    }
347
348
    /**
349
     * Generate a list of all folders in either the database or the filesystem.
350
     *
351
     * @param FilesystemInterface $data_filesystem
352
     *
353
     * @return Collection<string,string>
354
     */
355
    public function allMediaFolders(FilesystemInterface $data_filesystem): Collection
356
    {
357
        $db_folders = DB::table('media_file')
358
            ->join('gedcom_setting', 'gedcom_id', '=', 'm_file')
359
            ->where('setting_name', '=', 'MEDIA_DIRECTORY')
360
            ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
361
            ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
362
            ->select(new Expression('setting_value || multimedia_file_refn AS path'))
363
            ->pluck('path')
364
            ->map(static function (string $path): string {
365
                return dirname($path) . '/';
366
            });
367
368
        $media_roots = DB::table('gedcom_setting')
369
            ->where('setting_name', '=', 'MEDIA_DIRECTORY')
370
            ->where('gedcom_id', '>', '0')
371
            ->pluck('setting_value')
372
            ->uniqueStrict();
373
374
        $disk_folders = new Collection($media_roots);
375
376
        foreach ($media_roots as $media_folder) {
377
            $tmp = Collection::make($data_filesystem->listContents($media_folder, true))
378
                ->filter(static function (array $metadata) {
379
                    return $metadata['type'] === 'dir';
380
                })
381
                ->map(static function (array $metadata): string {
382
                    return $metadata['path'] . '/';
383
                })
384
                ->filter(static function (string $dir): bool {
385
                    return !str_contains($dir, '/thumbs/') && !str_contains($dir, 'watermarks');
386
                });
387
388
            $disk_folders = $disk_folders->concat($tmp);
389
        }
390
391
        return $disk_folders->concat($db_folders)
392
            ->uniqueStrict()
393
            ->mapWithKeys(static function (string $folder): array {
394
                return [$folder => $folder];
395
            });
396
    }
397
}
398