Passed
Push — master ( dec352...b4a2f8 )
by Greg
05:56
created

Tree::rowMapper()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 4
rs 10
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;
21
22
use Closure;
23
use Fisharebest\Flysystem\Adapter\ChrootAdapter;
24
use Fisharebest\Webtrees\Contracts\UserInterface;
25
use Fisharebest\Webtrees\Services\GedcomExportService;
26
use Fisharebest\Webtrees\Services\PendingChangesService;
27
use Illuminate\Database\Capsule\Manager as DB;
28
use InvalidArgumentException;
29
use League\Flysystem\Filesystem;
30
use League\Flysystem\FilesystemInterface;
31
use Psr\Http\Message\StreamInterface;
32
use stdClass;
33
34
use function app;
35
use function array_key_exists;
36
use function date;
1 ignored issue
show
Bug introduced by
This use statement conflicts with another class in this namespace, Fisharebest\Webtrees\date. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
37
use function str_starts_with;
38
use function strlen;
39
use function strtoupper;
40
use function substr;
41
use function substr_replace;
42
43
/**
44
 * Provide an interface to the wt_gedcom table.
45
 */
46
class Tree
47
{
48
    private const RESN_PRIVACY = [
49
        'none'         => Auth::PRIV_PRIVATE,
50
        'privacy'      => Auth::PRIV_USER,
51
        'confidential' => Auth::PRIV_NONE,
52
        'hidden'       => Auth::PRIV_HIDE,
53
    ];
54
55
    /** @var int The tree's ID number */
56
    private $id;
57
58
    /** @var string The tree's name */
59
    private $name;
60
61
    /** @var string The tree's title */
62
    private $title;
63
64
    /** @var int[] Default access rules for facts in this tree */
65
    private $fact_privacy;
66
67
    /** @var int[] Default access rules for individuals in this tree */
68
    private $individual_privacy;
69
70
    /** @var integer[][] Default access rules for individual facts in this tree */
71
    private $individual_fact_privacy;
72
73
    /** @var string[] Cached copy of the wt_gedcom_setting table. */
74
    private $preferences = [];
75
76
    /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
77
    private $user_preferences = [];
78
79
    /**
80
     * Create a tree object.
81
     *
82
     * @param int    $id
83
     * @param string $name
84
     * @param string $title
85
     */
86
    public function __construct(int $id, string $name, string $title)
87
    {
88
        $this->id                      = $id;
89
        $this->name                    = $name;
90
        $this->title                   = $title;
91
        $this->fact_privacy            = [];
92
        $this->individual_privacy      = [];
93
        $this->individual_fact_privacy = [];
94
95
        // Load the privacy settings for this tree
96
        $rows = DB::table('default_resn')
97
            ->where('gedcom_id', '=', $this->id)
98
            ->get();
99
100
        foreach ($rows as $row) {
101
            // Convert GEDCOM privacy restriction to a webtrees access level.
102
            $row->resn = self::RESN_PRIVACY[$row->resn];
103
104
            if ($row->xref !== null) {
105
                if ($row->tag_type !== null) {
106
                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn;
107
                } else {
108
                    $this->individual_privacy[$row->xref] = $row->resn;
109
                }
110
            } else {
111
                $this->fact_privacy[$row->tag_type] = $row->resn;
112
            }
113
        }
114
    }
115
116
    /**
117
     * A closure which will create a record from a database row.
118
     *
119
     * @return Closure
120
     */
121
    public static function rowMapper(): Closure
122
    {
123
        return static function (stdClass $row): Tree {
124
            return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title);
125
        };
126
    }
127
128
    /**
129
     * Set the tree’s configuration settings.
130
     *
131
     * @param string $setting_name
132
     * @param string $setting_value
133
     *
134
     * @return $this
135
     */
136
    public function setPreference(string $setting_name, string $setting_value): Tree
137
    {
138
        if ($setting_value !== $this->getPreference($setting_name)) {
139
            DB::table('gedcom_setting')->updateOrInsert([
140
                'gedcom_id'    => $this->id,
141
                'setting_name' => $setting_name,
142
            ], [
143
                'setting_value' => $setting_value,
144
            ]);
145
146
            $this->preferences[$setting_name] = $setting_value;
147
148
            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
149
        }
150
151
        return $this;
152
    }
153
154
    /**
155
     * Get the tree’s configuration settings.
156
     *
157
     * @param string $setting_name
158
     * @param string $default
159
     *
160
     * @return string
161
     */
162
    public function getPreference(string $setting_name, string $default = ''): string
163
    {
164
        if ($this->preferences === []) {
165
            $this->preferences = DB::table('gedcom_setting')
166
                ->where('gedcom_id', '=', $this->id)
167
                ->pluck('setting_value', 'setting_name')
168
                ->all();
169
        }
170
171
        return $this->preferences[$setting_name] ?? $default;
172
    }
173
174
    /**
175
     * The name of this tree
176
     *
177
     * @return string
178
     */
179
    public function name(): string
180
    {
181
        return $this->name;
182
    }
183
184
    /**
185
     * The title of this tree
186
     *
187
     * @return string
188
     */
189
    public function title(): string
190
    {
191
        return $this->title;
192
    }
193
194
    /**
195
     * The fact-level privacy for this tree.
196
     *
197
     * @return int[]
198
     */
199
    public function getFactPrivacy(): array
200
    {
201
        return $this->fact_privacy;
202
    }
203
204
    /**
205
     * The individual-level privacy for this tree.
206
     *
207
     * @return int[]
208
     */
209
    public function getIndividualPrivacy(): array
210
    {
211
        return $this->individual_privacy;
212
    }
213
214
    /**
215
     * The individual-fact-level privacy for this tree.
216
     *
217
     * @return int[][]
218
     */
219
    public function getIndividualFactPrivacy(): array
220
    {
221
        return $this->individual_fact_privacy;
222
    }
223
224
    /**
225
     * Set the tree’s user-configuration settings.
226
     *
227
     * @param UserInterface $user
228
     * @param string        $setting_name
229
     * @param string        $setting_value
230
     *
231
     * @return $this
232
     */
233
    public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree
234
    {
235
        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
236
            // Update the database
237
            DB::table('user_gedcom_setting')->updateOrInsert([
238
                'gedcom_id'    => $this->id(),
239
                'user_id'      => $user->id(),
240
                'setting_name' => $setting_name,
241
            ], [
242
                'setting_value' => $setting_value,
243
            ]);
244
245
            // Update the cache
246
            $this->user_preferences[$user->id()][$setting_name] = $setting_value;
247
            // Audit log of changes
248
            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this);
249
        }
250
251
        return $this;
252
    }
253
254
    /**
255
     * Get the tree’s user-configuration settings.
256
     *
257
     * @param UserInterface $user
258
     * @param string        $setting_name
259
     * @param string        $default
260
     *
261
     * @return string
262
     */
263
    public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string
264
    {
265
        // There are lots of settings, and we need to fetch lots of them on every page
266
        // so it is quicker to fetch them all in one go.
267
        if (!array_key_exists($user->id(), $this->user_preferences)) {
268
            $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting')
269
                ->where('user_id', '=', $user->id())
270
                ->where('gedcom_id', '=', $this->id)
271
                ->pluck('setting_value', 'setting_name')
272
                ->all();
273
        }
274
275
        return $this->user_preferences[$user->id()][$setting_name] ?? $default;
276
    }
277
278
    /**
279
     * The ID of this tree
280
     *
281
     * @return int
282
     */
283
    public function id(): int
284
    {
285
        return $this->id;
286
    }
287
288
    /**
289
     * Can a user accept changes for this tree?
290
     *
291
     * @param UserInterface $user
292
     *
293
     * @return bool
294
     */
295
    public function canAcceptChanges(UserInterface $user): bool
296
    {
297
        return Auth::isModerator($this, $user);
298
    }
299
300
    /**
301
     * Are there any pending edits for this tree, than need reviewing by a moderator.
302
     *
303
     * @return bool
304
     */
305
    public function hasPendingEdit(): bool
306
    {
307
        return DB::table('change')
308
            ->where('gedcom_id', '=', $this->id)
309
            ->where('status', '=', 'pending')
310
            ->exists();
311
    }
312
313
    /**
314
     * Delete everything relating to a tree
315
     *
316
     * @return void
317
     */
318
    public function delete(): void
319
    {
320
        // If this is the default tree, then unset it
321
        if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) {
322
            Site::setPreference('DEFAULT_GEDCOM', '');
323
        }
324
325
        $this->deleteGenealogyData(false);
326
327
        DB::table('block_setting')
328
            ->join('block', 'block.block_id', '=', 'block_setting.block_id')
329
            ->where('gedcom_id', '=', $this->id)
330
            ->delete();
331
        DB::table('block')->where('gedcom_id', '=', $this->id)->delete();
332
        DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
333
        DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
334
        DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete();
335
        DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete();
336
        DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete();
337
        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
338
        DB::table('log')->where('gedcom_id', '=', $this->id)->delete();
339
        DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete();
340
    }
341
342
    /**
343
     * Delete all the genealogy data from a tree - in preparation for importing
344
     * new data. Optionally retain the media data, for when the user has been
345
     * editing their data offline using an application which deletes (or does not
346
     * support) media data.
347
     *
348
     * @param bool $keep_media
349
     *
350
     * @return void
351
     */
352
    public function deleteGenealogyData(bool $keep_media): void
353
    {
354
        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
355
        DB::table('individuals')->where('i_file', '=', $this->id)->delete();
356
        DB::table('families')->where('f_file', '=', $this->id)->delete();
357
        DB::table('sources')->where('s_file', '=', $this->id)->delete();
358
        DB::table('other')->where('o_file', '=', $this->id)->delete();
359
        DB::table('places')->where('p_file', '=', $this->id)->delete();
360
        DB::table('placelinks')->where('pl_file', '=', $this->id)->delete();
361
        DB::table('name')->where('n_file', '=', $this->id)->delete();
362
        DB::table('dates')->where('d_file', '=', $this->id)->delete();
363
        DB::table('change')->where('gedcom_id', '=', $this->id)->delete();
364
365
        if ($keep_media) {
366
            DB::table('link')->where('l_file', '=', $this->id)
367
                ->where('l_type', '<>', 'OBJE')
368
                ->delete();
369
        } else {
370
            DB::table('link')->where('l_file', '=', $this->id)->delete();
371
            DB::table('media_file')->where('m_file', '=', $this->id)->delete();
372
            DB::table('media')->where('m_file', '=', $this->id)->delete();
373
        }
374
    }
375
376
    /**
377
     * Export the tree to a GEDCOM file
378
     *
379
     * @param resource $stream
380
     *
381
     * @return void
382
     *
383
     * @deprecated since 2.0.5.  Will be removed in 2.1.0
384
     */
385
    public function exportGedcom($stream): void
386
    {
387
        $gedcom_export_service = new GedcomExportService();
388
389
        $gedcom_export_service->export($this, $stream);
390
    }
391
392
    /**
393
     * Import data from a gedcom file into this tree.
394
     *
395
     * @param StreamInterface $stream   The GEDCOM file.
396
     * @param string          $filename The preferred filename, for export/download.
397
     *
398
     * @return void
399
     */
400
    public function importGedcomFile(StreamInterface $stream, string $filename): void
401
    {
402
        // Read the file in blocks of roughly 64K. Ensure that each block
403
        // contains complete gedcom records. This will ensure we don’t split
404
        // multi-byte characters, as well as simplifying the code to import
405
        // each block.
406
407
        $file_data = '';
408
409
        $this->deleteGenealogyData((bool) $this->getPreference('keep_media'));
410
        $this->setPreference('gedcom_filename', $filename);
411
        $this->setPreference('imported', '0');
412
413
        while (!$stream->eof()) {
414
            $file_data .= $stream->read(65536);
415
            // There is no strrpos() function that searches for substrings :-(
416
            for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) {
417
                if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) {
418
                    // We’ve found the last record boundary in this chunk of data
419
                    break;
420
                }
421
            }
422
            if ($pos) {
423
                DB::table('gedcom_chunk')->insert([
424
                    'gedcom_id'  => $this->id,
425
                    'chunk_data' => substr($file_data, 0, $pos),
426
                ]);
427
428
                $file_data = substr($file_data, $pos);
429
            }
430
        }
431
        DB::table('gedcom_chunk')->insert([
432
            'gedcom_id'  => $this->id,
433
            'chunk_data' => $file_data,
434
        ]);
435
436
        $stream->close();
437
    }
438
439
    /**
440
     * Create a new record from GEDCOM data.
441
     *
442
     * @param string $gedcom
443
     *
444
     * @return GedcomRecord|Individual|Family|Location|Note|Source|Repository|Media|Submitter|Submission
445
     * @throws InvalidArgumentException
446
     */
447
    public function createRecord(string $gedcom): GedcomRecord
448
    {
449
        if (!preg_match('/^0 @@ ([_A-Z]+)/', $gedcom, $match)) {
450
            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
451
        }
452
453
        $xref   = Factory::xref()->make($match[1]);
454
        $gedcom = substr_replace($gedcom, $xref, 3, 0);
455
456
        // Create a change record
457
        $today = strtoupper(date('d M Y'));
458
        $now   = date('H:i:s');
459
        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
460
461
        // Create a pending change
462
        DB::table('change')->insert([
463
            'gedcom_id'  => $this->id,
464
            'xref'       => $xref,
465
            'old_gedcom' => '',
466
            'new_gedcom' => $gedcom,
467
            'user_id'    => Auth::id(),
468
        ]);
469
470
        // Accept this pending change
471
        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS)) {
472
            $record = Factory::gedcomRecord()->new($xref, $gedcom, null, $this);
473
474
            app(PendingChangesService::class)->acceptRecord($record);
475
476
            return $record;
477
        }
478
479
        return Factory::gedcomRecord()->new($xref, '', $gedcom, $this);
480
    }
481
482
    /**
483
     * Generate a new XREF, unique across all family trees
484
     *
485
     * @return string
486
     * @deprecated - use the factory directly.
487
     */
488
    public function getNewXref(): string
489
    {
490
        return Factory::xref()->make(GedcomRecord::RECORD_TYPE);
491
    }
492
493
    /**
494
     * Create a new family from GEDCOM data.
495
     *
496
     * @param string $gedcom
497
     *
498
     * @return Family
499
     * @throws InvalidArgumentException
500
     */
501
    public function createFamily(string $gedcom): GedcomRecord
502
    {
503
        if (!str_starts_with($gedcom, '0 @@ FAM')) {
504
            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
505
        }
506
507
        $xref   = Factory::xref()->make(Family::RECORD_TYPE);
508
        $gedcom = substr_replace($gedcom, $xref, 3, 0);
509
510
        // Create a change record
511
        $today = strtoupper(date('d M Y'));
512
        $now   = date('H:i:s');
513
        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
514
515
        // Create a pending change
516
        DB::table('change')->insert([
517
            'gedcom_id'  => $this->id,
518
            'xref'       => $xref,
519
            'old_gedcom' => '',
520
            'new_gedcom' => $gedcom,
521
            'user_id'    => Auth::id(),
522
        ]);
523
524
        // Accept this pending change
525
        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') {
526
            $record = Factory::family()->new($xref, $gedcom, null, $this);
527
528
            app(PendingChangesService::class)->acceptRecord($record);
529
530
            return $record;
531
        }
532
533
        return Factory::family()->new($xref, '', $gedcom, $this);
534
    }
535
536
    /**
537
     * Create a new individual from GEDCOM data.
538
     *
539
     * @param string $gedcom
540
     *
541
     * @return Individual
542
     * @throws InvalidArgumentException
543
     */
544
    public function createIndividual(string $gedcom): GedcomRecord
545
    {
546
        if (!str_starts_with($gedcom, '0 @@ INDI')) {
547
            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
548
        }
549
550
        $xref   = Factory::xref()->make(Individual::RECORD_TYPE);
551
        $gedcom = substr_replace($gedcom, $xref, 3, 0);
552
553
        // Create a change record
554
        $today = strtoupper(date('d M Y'));
555
        $now   = date('H:i:s');
556
        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
557
558
        // Create a pending change
559
        DB::table('change')->insert([
560
            'gedcom_id'  => $this->id,
561
            'xref'       => $xref,
562
            'old_gedcom' => '',
563
            'new_gedcom' => $gedcom,
564
            'user_id'    => Auth::id(),
565
        ]);
566
567
        // Accept this pending change
568
        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') {
569
            $record = Factory::individual()->new($xref, $gedcom, null, $this);
570
571
            app(PendingChangesService::class)->acceptRecord($record);
572
573
            return $record;
574
        }
575
576
        return Factory::individual()->new($xref, '', $gedcom, $this);
577
    }
578
579
    /**
580
     * Create a new media object from GEDCOM data.
581
     *
582
     * @param string $gedcom
583
     *
584
     * @return Media
585
     * @throws InvalidArgumentException
586
     */
587
    public function createMediaObject(string $gedcom): Media
588
    {
589
        if (!str_starts_with($gedcom, '0 @@ OBJE')) {
590
            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
591
        }
592
593
        $xref   = Factory::xref()->make(Media::RECORD_TYPE);
594
        $gedcom = substr_replace($gedcom, $xref, 3, 0);
595
596
        // Create a change record
597
        $today = strtoupper(date('d M Y'));
598
        $now   = date('H:i:s');
599
        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
600
601
        // Create a pending change
602
        DB::table('change')->insert([
603
            'gedcom_id'  => $this->id,
604
            'xref'       => $xref,
605
            'old_gedcom' => '',
606
            'new_gedcom' => $gedcom,
607
            'user_id'    => Auth::id(),
608
        ]);
609
610
        // Accept this pending change
611
        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') {
612
            $record = Factory::media()->new($xref, $gedcom, null, $this);
613
614
            app(PendingChangesService::class)->acceptRecord($record);
615
616
            return $record;
617
        }
618
619
        return Factory::media()->new($xref, '', $gedcom, $this);
620
    }
621
622
    /**
623
     * What is the most significant individual in this tree.
624
     *
625
     * @param UserInterface $user
626
     * @param string        $xref
627
     *
628
     * @return Individual
629
     */
630
    public function significantIndividual(UserInterface $user, $xref = ''): Individual
631
    {
632
        if ($xref === '') {
633
            $individual = null;
634
        } else {
635
            $individual = Factory::individual()->make($xref, $this);
636
637
            if ($individual === null) {
638
                $family = Factory::family()->make($xref, $this);
639
640
                if ($family instanceof Family) {
641
                    $individual = $family->spouses()->first() ?? $family->children()->first();
642
                }
643
            }
644
        }
645
646
        if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF) !== '') {
647
            $individual = Factory::individual()->make($this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF), $this);
648
        }
649
650
        if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF) !== '') {
651
            $individual = Factory::individual()->make($this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF), $this);
652
        }
653
654
        if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') {
655
            $individual = Factory::individual()->make($this->getPreference('PEDIGREE_ROOT_ID'), $this);
656
        }
657
        if ($individual === null) {
658
            $xref = (string) DB::table('individuals')
659
                ->where('i_file', '=', $this->id())
660
                ->min('i_id');
661
662
            $individual = Factory::individual()->make($xref, $this);
663
        }
664
        if ($individual === null) {
665
            // always return a record
666
            $individual = Factory::individual()->new('I', '0 @I@ INDI', null, $this);
667
        }
668
669
        return $individual;
670
    }
671
672
    /**
673
     * Where do we store our media files.
674
     *
675
     * @param FilesystemInterface $data_filesystem
676
     *
677
     * @return FilesystemInterface
678
     */
679
    public function mediaFilesystem(FilesystemInterface $data_filesystem): FilesystemInterface
680
    {
681
        $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/');
682
        $adapter   = new ChrootAdapter($data_filesystem, $media_dir);
683
684
        return new Filesystem($adapter);
685
    }
686
}
687