Passed
Push — master ( 602c43...6577bf )
by Greg
06:13
created

MediaFileService::createMediaFileGedcom()   A

Complexity

Conditions 6
Paths 24

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 16
nc 24
nop 4
dl 0
loc 31
rs 9.1111
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2020 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\Services;
21
22
use Fisharebest\Webtrees\Factory;
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<string,string>
124
     */
125
    public function mediaTypes($current = ''): array
126
    {
127
        $media_types = GedcomTag::getFileFormTypes();
128
129
        $media_types = ['' => ''] + [$current => $current] + $media_types;
130
131
        return $media_types;
132
    }
133
134
    /**
135
     * A list of media files not already linked to a media object.
136
     *
137
     * @param Tree                $tree
138
     * @param FilesystemInterface $data_filesystem
139
     *
140
     * @return array<string>
141
     */
142
    public function unusedFiles(Tree $tree, FilesystemInterface $data_filesystem): array
143
    {
144
        $used_files = DB::table('media_file')
145
            ->where('m_file', '=', $tree->id())
146
            ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
147
            ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
148
            ->pluck('multimedia_file_refn')
149
            ->all();
150
151
        $disk_files = $tree->mediaFilesystem($data_filesystem)->listContents('', true);
152
153
        $disk_files = array_filter($disk_files, static function (array $item) {
154
            // Older versions of webtrees used a couple of special folders.
155
            return
156
                $item['type'] === 'file' &&
157
                !str_contains($item['path'], '/thumbs/') &&
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

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

157
                !/** @scrutinizer ignore-deprecated */ str_contains($item['path'], '/thumbs/') &&

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
158
                !str_contains($item['path'], '/watermarks/');
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

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

158
                !/** @scrutinizer ignore-deprecated */ str_contains($item['path'], '/watermarks/');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
159
        });
160
161
        $disk_files = array_map(static function (array $item): string {
162
            return $item['path'];
163
        }, $disk_files);
164
165
        $unused_files = array_diff($disk_files, $used_files);
166
167
        sort($unused_files);
168
169
        return array_combine($unused_files, $unused_files);
170
    }
171
172
    /**
173
     * Store an uploaded file (or URL), either to be added to a media object
174
     * or to create a media object.
175
     *
176
     * @param ServerRequestInterface $request
177
     *
178
     * @return string The value to be stored in the 'FILE' field of the media object.
179
     */
180
    public function uploadFile(ServerRequestInterface $request): string
181
    {
182
        $tree = $request->getAttribute('tree');
183
        assert($tree instanceof Tree);
184
185
        $data_filesystem = Factory::filesystem()->data();
186
187
        $params        = (array) $request->getParsedBody();
188
        $file_location = $params['file_location'];
189
190
        switch ($file_location) {
191
            case 'url':
192
                $remote = $params['remote'];
193
194
                if (str_contains($remote, '://')) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

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

194
                if (/** @scrutinizer ignore-deprecated */ str_contains($remote, '://')) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
195
                    return $remote;
196
                }
197
198
                return '';
199
200
            case 'unused':
201
                $unused = $params['unused'];
202
203
                if ($tree->mediaFilesystem($data_filesystem)->has($unused)) {
204
                    return $unused;
205
                }
206
207
                return '';
208
209
            case 'upload':
210
            default:
211
                $folder   = $params['folder'];
212
                $auto     = $params['auto'];
213
                $new_file = $params['new_file'];
214
215
                /** @var UploadedFileInterface|null $uploaded_file */
216
                $uploaded_file = $request->getUploadedFiles()['file'];
217
                if ($uploaded_file === null || $uploaded_file->getError() !== UPLOAD_ERR_OK) {
218
                    return '';
219
                }
220
221
                // The filename
222
                $new_file = strtr($new_file, ['\\' => '/']);
223
                if ($new_file !== '' && !str_contains($new_file, '/')) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

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

223
                if ($new_file !== '' && !/** @scrutinizer ignore-deprecated */ str_contains($new_file, '/')) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
224
                    $file = $new_file;
225
                } else {
226
                    $file = $uploaded_file->getClientFilename();
227
                }
228
229
                // The folder
230
                $folder = strtr($folder, ['\\' => '/']);
231
                $folder = trim($folder, '/');
232
                if ($folder !== '') {
233
                    $folder .= '/';
234
                }
235
236
                // Generate a unique name for the file?
237
                if ($auto === '1' || $tree->mediaFilesystem($data_filesystem)->has($folder . $file)) {
238
                    $folder    = '';
239
                    $extension = pathinfo($uploaded_file->getClientFilename(), PATHINFO_EXTENSION);
240
                    $file      = sha1((string) $uploaded_file->getStream()) . '.' . $extension;
241
                }
242
243
                try {
244
                    $tree->mediaFilesystem($data_filesystem)->putStream($folder . $file, $uploaded_file->getStream()->detach());
245
246
                    return $folder . $file;
247
                } catch (RuntimeException | InvalidArgumentException $ex) {
248
                    FlashMessages::addMessage(I18N::translate('There was an error uploading your file.'));
249
250
                    return '';
251
                }
252
        }
253
    }
254
255
    /**
256
     * Convert the media file attributes into GEDCOM format.
257
     *
258
     * @param string $file
259
     * @param string $type
260
     * @param string $title
261
     * @param string $note
262
     *
263
     * @return string
264
     */
265
    public function createMediaFileGedcom(string $file, string $type, string $title, string $note): string
266
    {
267
        // Tidy non-printing characters
268
        $type  = trim(preg_replace('/\s+/', ' ', $type));
269
        $title = trim(preg_replace('/\s+/', ' ', $title));
270
271
        $gedcom = '1 FILE ' . $file;
272
273
        $format = strtolower(pathinfo($file, PATHINFO_EXTENSION));
274
        $format = self::EXTENSION_TO_FORM[$format] ?? $format;
275
276
        if ($format !== '') {
277
            $gedcom .= "\n2 FORM " . $format;
278
        } elseif ($type !== '') {
279
            $gedcom .= "\n2 FORM";
280
        }
281
282
        if ($type !== '') {
283
            $gedcom .= "\n3 TYPE " . $type;
284
        }
285
286
        if ($title !== '') {
287
            $gedcom .= "\n2 TITL " . $title;
288
        }
289
290
        if ($note !== '') {
291
            // Convert HTML line endings to GEDCOM continuations
292
            $gedcom .= "\n1 NOTE " . strtr($note, ["\r\n" => "\n2 CONT "]);
293
        }
294
295
        return $gedcom;
296
    }
297
298
    /**
299
     * Fetch a list of all files on disk (in folders used by any tree).
300
     *
301
     * @param FilesystemInterface $data_filesystem Fileystem to search
302
     * @param string              $media_folder    Root folder
303
     * @param bool                $subfolders      Include subfolders
304
     *
305
     * @return Collection<string>
306
     */
307
    public function allFilesOnDisk(FilesystemInterface $data_filesystem, string $media_folder, bool $subfolders): Collection
308
    {
309
        $array = $data_filesystem->listContents($media_folder, $subfolders);
310
311
        return Collection::make($array)
312
            ->filter(static function (array $metadata): bool {
313
                return
314
                    $metadata['type'] === 'file' &&
315
                    !str_contains($metadata['path'], '/thumbs/') &&
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

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

315
                    !/** @scrutinizer ignore-deprecated */ str_contains($metadata['path'], '/thumbs/') &&

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
316
                    !str_contains($metadata['path'], '/watermark/');
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

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

316
                    !/** @scrutinizer ignore-deprecated */ str_contains($metadata['path'], '/watermark/');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
317
            })
318
            ->map(static function (array $metadata): string {
319
                return $metadata['path'];
320
            });
321
    }
322
323
    /**
324
     * Fetch a list of all files on in the database.
325
     *
326
     * @param string $media_folder Root folder
327
     * @param bool   $subfolders   Include subfolders
328
     *
329
     * @return Collection<string>
330
     */
331
    public function allFilesInDatabase(string $media_folder, bool $subfolders): Collection
332
    {
333
        $query = DB::table('media_file')
334
            ->join('gedcom_setting', 'gedcom_id', '=', 'm_file')
335
            ->where('setting_name', '=', 'MEDIA_DIRECTORY')
336
            //->where('multimedia_file_refn', 'LIKE', '%/%')
337
            ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
338
            ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
339
            ->where(new Expression('setting_value || multimedia_file_refn'), 'LIKE', $media_folder . '%')
340
            ->select(new Expression('setting_value || multimedia_file_refn AS path'))
341
            ->orderBy(new Expression('setting_value || multimedia_file_refn'));
342
343
        if (!$subfolders) {
344
            $query->where(new Expression('setting_value || multimedia_file_refn'), 'NOT LIKE', $media_folder . '%/%');
345
        }
346
347
        return $query->pluck('path');
348
    }
349
350
    /**
351
     * Generate a list of all folders in either the database or the filesystem.
352
     *
353
     * @param FilesystemInterface $data_filesystem
354
     *
355
     * @return Collection<string,string>
356
     */
357
    public function allMediaFolders(FilesystemInterface $data_filesystem): Collection
358
    {
359
        $db_folders = DB::table('media_file')
360
            ->join('gedcom_setting', 'gedcom_id', '=', 'm_file')
361
            ->where('setting_name', '=', 'MEDIA_DIRECTORY')
362
            ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
363
            ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
364
            ->select(new Expression('setting_value || multimedia_file_refn AS path'))
365
            ->pluck('path')
366
            ->map(static function (string $path): string {
367
                return dirname($path) . '/';
368
            });
369
370
        $media_roots = DB::table('gedcom_setting')
371
            ->where('setting_name', '=', 'MEDIA_DIRECTORY')
372
            ->where('gedcom_id', '>', '0')
373
            ->pluck('setting_value')
374
            ->uniqueStrict();
375
376
        $disk_folders = new Collection($media_roots);
377
378
        foreach ($media_roots as $media_folder) {
379
            $tmp = Collection::make($data_filesystem->listContents($media_folder, true))
380
                ->filter(static function (array $metadata) {
381
                    return $metadata['type'] === 'dir';
382
                })
383
                ->map(static function (array $metadata): string {
384
                    return $metadata['path'] . '/';
385
                })
386
                ->filter(static function (string $dir): bool {
387
                    return !str_contains($dir, '/thumbs/') && !str_contains($dir, 'watermarks');
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

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

387
                    return !/** @scrutinizer ignore-deprecated */ str_contains($dir, '/thumbs/') && !str_contains($dir, 'watermarks');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
388
                });
389
390
            $disk_folders = $disk_folders->concat($tmp);
391
        }
392
393
        return $disk_folders->concat($db_folders)
394
            ->uniqueStrict()
395
            ->mapWithKeys(static function (string $folder): array {
396
                return [$folder => $folder];
397
            });
398
    }
399
}
400