Passed
Push — master ( d93bbd...13aa75 )
by Greg
07:46
created

MediaFileService::allFilesInDatabase()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 11
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 17
rs 9.9
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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Services;
21
22
use Fisharebest\Webtrees\FlashMessages;
23
use Fisharebest\Webtrees\GedcomTag;
24
use Fisharebest\Webtrees\I18N;
25
use Fisharebest\Webtrees\Tree;
26
use Illuminate\Database\Capsule\Manager as DB;
27
use Illuminate\Database\Query\Expression;
28
use Illuminate\Support\Collection;
29
use InvalidArgumentException;
30
use League\Flysystem\FilesystemInterface;
31
use Psr\Http\Message\ServerRequestInterface;
32
use Psr\Http\Message\UploadedFileInterface;
33
use RuntimeException;
34
use Symfony\Component\HttpFoundation\File\UploadedFile;
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 intdiv;
43
use function pathinfo;
44
use function preg_match;
45
use function sha1;
46
use function sort;
47
use function str_replace;
48
use function strpos;
49
use function strtolower;
50
use function trim;
51
52
use const PATHINFO_EXTENSION;
53
use const UPLOAD_ERR_OK;
54
55
/**
56
 * Managing media files.
57
 */
58
class MediaFileService
59
{
60
    public const EDIT_RESTRICTIONS = [
61
        'locked',
62
    ];
63
64
    public const PRIVACY_RESTRICTIONS = [
65
        'none',
66
        'privacy',
67
        'confidential',
68
    ];
69
70
    /**
71
     * What is the largest file a user may upload?
72
     */
73
    public function maxUploadFilesize(): string
74
    {
75
        $bytes = UploadedFile::getMaxFilesize();
76
        $kb    = intdiv($bytes + 1023, 1024);
77
78
        return I18N::translate('%s KB', I18N::number($kb));
79
    }
80
81
    /**
82
     * A list of key/value options for media types.
83
     *
84
     * @param string $current
85
     *
86
     * @return array
87
     */
88
    public function mediaTypes($current = ''): array
89
    {
90
        $media_types = GedcomTag::getFileFormTypes();
91
92
        $media_types = ['' => ''] + [$current => $current] + $media_types;
93
94
        return $media_types;
95
    }
96
97
    /**
98
     * A list of media files not already linked to a media object.
99
     *
100
     * @param Tree                $tree
101
     * @param FilesystemInterface $data_filesystem
102
     *
103
     * @return array
104
     */
105
    public function unusedFiles(Tree $tree, FilesystemInterface $data_filesystem): array
106
    {
107
        $used_files = DB::table('media_file')
108
            ->where('m_file', '=', $tree->id())
109
            ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
110
            ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
111
            ->pluck('multimedia_file_refn')
112
            ->all();
113
114
        $disk_files = $tree->mediaFilesystem($data_filesystem)->listContents('', true);
115
116
        $disk_files = array_filter($disk_files, static function (array $item) {
117
            // Older versions of webtrees used a couple of special folders.
118
            return
119
                $item['type'] === 'file' &&
120
                strpos($item['path'], '/thumbs/') === false &&
121
                strpos($item['path'], '/watermarks/') === false;
122
        });
123
124
        $disk_files = array_map(static function (array $item): string {
125
            return $item['path'];
126
        }, $disk_files);
127
128
        $unused_files = array_diff($disk_files, $used_files);
129
130
        sort($unused_files);
131
132
        return array_combine($unused_files, $unused_files);
133
    }
134
135
    /**
136
     * Store an uploaded file (or URL), either to be added to a media object
137
     * or to create a media object.
138
     *
139
     * @param ServerRequestInterface $request
140
     *
141
     * @return string The value to be stored in the 'FILE' field of the media object.
142
     */
143
    public function uploadFile(ServerRequestInterface $request): string
144
    {
145
        $tree = $request->getAttribute('tree');
146
        assert($tree instanceof Tree);
147
148
        $data_filesystem = $request->getAttribute('filesystem.data');
149
        assert($data_filesystem instanceof FilesystemInterface);
150
151
        $params        = $request->getParsedBody();
152
        $file_location = $params['file_location'];
153
154
        switch ($file_location) {
155
            case 'url':
156
                $remote = $params['remote'];
157
158
                if (strpos($remote, '://') !== false) {
159
                    return $remote;
160
                }
161
162
                return '';
163
164
            case 'unused':
165
                $unused = $params['unused'];
166
167
                if ($tree->mediaFilesystem($data_filesystem)->has($unused)) {
168
                    return $unused;
169
                }
170
171
                return '';
172
173
            case 'upload':
174
            default:
175
                $folder   = $params['folder'];
176
                $auto     = $params['auto'];
177
                $new_file = $params['new_file'];
178
179
                /** @var UploadedFileInterface|null $uploaded_file */
180
                $uploaded_file = $request->getUploadedFiles()['file'];
181
                if ($uploaded_file === null || $uploaded_file->getError() !== UPLOAD_ERR_OK) {
182
                    return '';
183
                }
184
185
                // The filename
186
                $new_file = str_replace('\\', '/', $new_file);
187
                if ($new_file !== '' && strpos($new_file, '/') === false) {
188
                    $file = $new_file;
189
                } else {
190
                    $file = $uploaded_file->getClientFilename();
191
                }
192
193
                // The folder
194
                $folder = str_replace('\\', '/', $folder);
195
                $folder = trim($folder, '/');
196
                if ($folder !== '') {
197
                    $folder .= '/';
198
                }
199
200
                // Generate a unique name for the file?
201
                if ($auto === '1' || $tree->mediaFilesystem($data_filesystem)->has($folder . $file)) {
202
                    $folder    = '';
203
                    $extension = pathinfo($uploaded_file->getClientFilename(), PATHINFO_EXTENSION);
204
                    $file      = sha1((string) $uploaded_file->getStream()) . '.' . $extension;
205
                }
206
207
                try {
208
                    $tree->mediaFilesystem($data_filesystem)->writeStream($folder . $file, $uploaded_file->getStream()->detach());
209
210
                    return $folder . $file;
211
                } catch (RuntimeException | InvalidArgumentException $ex) {
212
                    FlashMessages::addMessage(I18N::translate('There was an error uploading your file.'));
213
214
                    return '';
215
                }
216
        }
217
    }
218
219
    /**
220
     * Convert the media file attributes into GEDCOM format.
221
     *
222
     * @param string $file
223
     * @param string $type
224
     * @param string $title
225
     *
226
     * @return string
227
     */
228
    public function createMediaFileGedcom(string $file, string $type, string $title): string
229
    {
230
        if (preg_match('/\.([a-z0-9]+)/i', $file, $match)) {
231
            $extension = strtolower($match[1]);
232
            $extension = str_replace('jpg', 'jpeg', $extension);
233
            $extension = ' ' . $extension;
234
        } else {
235
            $extension = '';
236
        }
237
238
        $gedcom = '1 FILE ' . $file;
239
        if ($type !== '') {
240
            $gedcom .= "\n2 FORM" . $extension . "\n3 TYPE " . $type;
241
        }
242
        if ($title !== '') {
243
            $gedcom .= "\n2 TITL " . $title;
244
        }
245
246
        return $gedcom;
247
    }
248
249
    /**
250
     * Fetch a list of all files on disk (in folders used by any tree).
251
     *
252
     * @param FilesystemInterface $data_filesystem Fileystem to search
253
     * @param string              $media_folder    Root folder
254
     * @param bool                $subfolders      Include subfolders
255
     *
256
     * @return Collection
257
     */
258
    public function allFilesOnDisk(FilesystemInterface $data_filesystem, string $media_folder, bool $subfolders): Collection
259
    {
260
        $array = $data_filesystem->listContents($media_folder, $subfolders);
261
262
        return Collection::make($array)
263
            ->filter(static function (array $metadata): bool {
264
                return
265
                    $metadata['type'] === 'file' &&
266
                    strpos($metadata['path'], '/thumbs/') === false &&
267
                    strpos($metadata['path'], '/watermark/') === false;
268
            })
269
            ->map(static function (array $metadata): string {
270
                return $metadata['path'];
271
            });
272
    }
273
274
    /**
275
     * Fetch a list of all files on in the database.
276
     *
277
     * @param string $media_folder Root folder
278
     * @param bool   $subfolders   Include subfolders
279
     *
280
     * @return Collection
281
     */
282
    public function allFilesInDatabase(string $media_folder, bool $subfolders): Collection
283
    {
284
        $query = DB::table('media_file')
285
            ->join('gedcom_setting', 'gedcom_id', '=', 'm_file')
286
            ->where('setting_name', '=', 'MEDIA_DIRECTORY')
287
            //->where('multimedia_file_refn', 'LIKE', '%/%')
288
            ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
289
            ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
290
            ->where(new Expression('setting_value || multimedia_file_refn'), 'LIKE', $media_folder . '%')
291
            ->select(new Expression('setting_value || multimedia_file_refn AS path'))
292
            ->orderBy(new Expression('setting_value || multimedia_file_refn'));
293
294
        if (!$subfolders) {
295
            $query->where(new Expression('setting_value || multimedia_file_refn'), 'NOT LIKE', $media_folder . '%/%');
296
        }
297
298
        return $query->pluck('path');
299
    }
300
301
    /**
302
     * Generate a list of all folders in either the database or the filesystem.
303
     *
304
     * @param FilesystemInterface $data_filesystem
305
     *
306
     * @return Collection
307
     */
308
    public function allMediaFolders(FilesystemInterface $data_filesystem): Collection
309
    {
310
        $db_folders = DB::table('media_file')
311
            ->join('gedcom_setting', 'gedcom_id', '=', 'm_file')
312
            ->where('setting_name', '=', 'MEDIA_DIRECTORY')
313
            ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
314
            ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
315
            ->select(new Expression('setting_value || multimedia_file_refn AS path'))
316
            ->pluck('path')
317
            ->map(static function (string $path): string {
318
                return dirname($path) . '/';
319
            });
320
321
        $media_roots = DB::table('gedcom_setting')
322
            ->where('setting_name', '=', 'MEDIA_DIRECTORY')
323
            ->pluck('setting_value')
324
            ->unique();
325
326
        $disk_folders = new Collection($media_roots);
327
328
        foreach ($media_roots as $media_folder) {
329
            $tmp = Collection::make($data_filesystem->listContents($media_folder, true))
330
                ->filter(static function (array $metadata) {
331
                    return $metadata['type'] === 'dir';
332
                })
333
                ->map(static function (array $metadata): string {
334
                    return $metadata['path'] . '/';
335
                })
336
                ->filter(static function (string $dir): bool {
337
                    return strpos($dir, '/thumbs/') === false && strpos($dir, 'watermarks') === false;
338
                });
339
340
            $disk_folders = $disk_folders->concat($tmp);
341
        }
342
343
        return $disk_folders->concat($db_folders)
344
            ->unique()
345
            ->mapWithKeys(static function (string $folder): array {
346
                return [$folder => $folder];
347
            });
348
    }
349
}
350