xref: /webtrees/app/MediaFile.php (revision e5dda0ae8fbfa38b6a29ec01421abd803cf1ba09)
18f5f5da8SGreg Roach<?php
23976b470SGreg Roach
38f5f5da8SGreg Roach/**
48f5f5da8SGreg Roach * webtrees: online genealogy
58fcd0d32SGreg Roach * Copyright (C) 2019 webtrees development team
68f5f5da8SGreg Roach * This program is free software: you can redistribute it and/or modify
78f5f5da8SGreg Roach * it under the terms of the GNU General Public License as published by
88f5f5da8SGreg Roach * the Free Software Foundation, either version 3 of the License, or
98f5f5da8SGreg Roach * (at your option) any later version.
108f5f5da8SGreg Roach * This program is distributed in the hope that it will be useful,
118f5f5da8SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
128f5f5da8SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
138f5f5da8SGreg Roach * GNU General Public License for more details.
148f5f5da8SGreg Roach * You should have received a copy of the GNU General Public License
158f5f5da8SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
168f5f5da8SGreg Roach */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
208f5f5da8SGreg Roachnamespace Fisharebest\Webtrees;
218f5f5da8SGreg Roach
225c98992aSGreg Roachuse League\Flysystem\Adapter\Local;
2385a166d8SGreg Roachuse League\Flysystem\FileNotFoundException;
245c98992aSGreg Roachuse League\Flysystem\Filesystem;
25a04bb9a2SGreg Roachuse League\Flysystem\FilesystemInterface;
26ee4364daSGreg Roachuse League\Glide\Signatures\SignatureFactory;
273976b470SGreg Roach
2885a166d8SGreg Roachuse function getimagesize;
2985a166d8SGreg Roachuse function intdiv;
3085a166d8SGreg Roachuse function pathinfo;
3185a166d8SGreg Roachuse function strtolower;
323976b470SGreg Roach
3385a166d8SGreg Roachuse const PATHINFO_EXTENSION;
348f5f5da8SGreg Roach
358f5f5da8SGreg Roach/**
368f5f5da8SGreg Roach * A GEDCOM media file.  A media object can contain many media files,
378f5f5da8SGreg Roach * such as scans of both sides of a document, the transcript of an audio
388f5f5da8SGreg Roach * recording, etc.
398f5f5da8SGreg Roach */
40c1010edaSGreg Roachclass MediaFile
41c1010edaSGreg Roach{
4216d6367aSGreg Roach    private const MIME_TYPES = [
435225fdfcSGreg Roach        'bmp'  => 'image/bmp',
445225fdfcSGreg Roach        'doc'  => 'application/msword',
455225fdfcSGreg Roach        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
465225fdfcSGreg Roach        'ged'  => 'text/x-gedcom',
475225fdfcSGreg Roach        'gif'  => 'image/gif',
485225fdfcSGreg Roach        'html' => 'text/html',
495225fdfcSGreg Roach        'htm'  => 'text/html',
50*e5dda0aeSGreg Roach        'jpe'  => 'image/jpeg',
515225fdfcSGreg Roach        'jpeg' => 'image/jpeg',
525225fdfcSGreg Roach        'jpg'  => 'image/jpeg',
535225fdfcSGreg Roach        'mov'  => 'video/quicktime',
545225fdfcSGreg Roach        'mp3'  => 'audio/mpeg',
555225fdfcSGreg Roach        'mp4'  => 'video/mp4',
565225fdfcSGreg Roach        'ogv'  => 'video/ogg',
575225fdfcSGreg Roach        'pdf'  => 'application/pdf',
585225fdfcSGreg Roach        'png'  => 'image/png',
595225fdfcSGreg Roach        'rar'  => 'application/x-rar-compressed',
605225fdfcSGreg Roach        'swf'  => 'application/x-shockwave-flash',
615225fdfcSGreg Roach        'svg'  => 'image/svg',
625225fdfcSGreg Roach        'tiff' => 'image/tiff',
635225fdfcSGreg Roach        'tif'  => 'image/tiff',
645225fdfcSGreg Roach        'xls'  => 'application/vnd-ms-excel',
655225fdfcSGreg Roach        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
665225fdfcSGreg Roach        'wmv'  => 'video/x-ms-wmv',
675225fdfcSGreg Roach        'zip'  => 'application/zip',
685225fdfcSGreg Roach    ];
695225fdfcSGreg Roach
7085a166d8SGreg Roach    private const SUPPORTED_IMAGE_MIME_TYPES = [
7185a166d8SGreg Roach        'image/gif',
7285a166d8SGreg Roach        'image/jpeg',
7385a166d8SGreg Roach        'image/png',
7485a166d8SGreg Roach    ];
7585a166d8SGreg Roach
768f5f5da8SGreg Roach    /** @var string The filename */
778f5f5da8SGreg Roach    private $multimedia_file_refn = '';
788f5f5da8SGreg Roach
798f5f5da8SGreg Roach    /** @var string The file extension; jpeg, txt, mp4, etc. */
808f5f5da8SGreg Roach    private $multimedia_format = '';
818f5f5da8SGreg Roach
828f5f5da8SGreg Roach    /** @var string The type of document; newspaper, microfiche, etc. */
838f5f5da8SGreg Roach    private $source_media_type = '';
848f5f5da8SGreg Roach    /** @var string The filename */
858f5f5da8SGreg Roach
868f5f5da8SGreg Roach    /** @var string The name of the document */
878f5f5da8SGreg Roach    private $descriptive_title = '';
888f5f5da8SGreg Roach
898f5f5da8SGreg Roach    /** @var Media $media The media object to which this file belongs */
908f5f5da8SGreg Roach    private $media;
918f5f5da8SGreg Roach
9264b90bf1SGreg Roach    /** @var string */
9364b90bf1SGreg Roach    private $fact_id;
9464b90bf1SGreg Roach
958f5f5da8SGreg Roach    /**
968f5f5da8SGreg Roach     * Create a MediaFile from raw GEDCOM data.
978f5f5da8SGreg Roach     *
988f5f5da8SGreg Roach     * @param string $gedcom
998f5f5da8SGreg Roach     * @param Media  $media
1008f5f5da8SGreg Roach     */
101c1010edaSGreg Roach    public function __construct($gedcom, Media $media)
102c1010edaSGreg Roach    {
1038f5f5da8SGreg Roach        $this->media   = $media;
10464b90bf1SGreg Roach        $this->fact_id = md5($gedcom);
1058f5f5da8SGreg Roach
1068f5f5da8SGreg Roach        if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) {
1078f5f5da8SGreg Roach            $this->multimedia_file_refn = $match[1];
108423c6ccdSGreg Roach            $this->multimedia_format    = pathinfo($match[1], PATHINFO_EXTENSION);
1098f5f5da8SGreg Roach        }
1108f5f5da8SGreg Roach
1118f5f5da8SGreg Roach        if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) {
1128f5f5da8SGreg Roach            $this->multimedia_format = $match[1];
1138f5f5da8SGreg Roach        }
1148f5f5da8SGreg Roach
1158f5f5da8SGreg Roach        if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) {
1168f5f5da8SGreg Roach            $this->source_media_type = $match[1];
1178f5f5da8SGreg Roach        }
1188f5f5da8SGreg Roach
1198f5f5da8SGreg Roach        if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) {
1208f5f5da8SGreg Roach            $this->descriptive_title = $match[1];
1218f5f5da8SGreg Roach        }
1228f5f5da8SGreg Roach    }
1238f5f5da8SGreg Roach
1248f5f5da8SGreg Roach    /**
12564b90bf1SGreg Roach     * Get the format.
1268f5f5da8SGreg Roach     *
1278f5f5da8SGreg Roach     * @return string
1288f5f5da8SGreg Roach     */
129c1010edaSGreg Roach    public function format(): string
130c1010edaSGreg Roach    {
1318f5f5da8SGreg Roach        return $this->multimedia_format;
1328f5f5da8SGreg Roach    }
1338f5f5da8SGreg Roach
1348f5f5da8SGreg Roach    /**
13564b90bf1SGreg Roach     * Get the type.
1368f5f5da8SGreg Roach     *
1378f5f5da8SGreg Roach     * @return string
1388f5f5da8SGreg Roach     */
139c1010edaSGreg Roach    public function type(): string
140c1010edaSGreg Roach    {
1418f5f5da8SGreg Roach        return $this->source_media_type;
1428f5f5da8SGreg Roach    }
1438f5f5da8SGreg Roach
1448f5f5da8SGreg Roach    /**
14564b90bf1SGreg Roach     * Get the title.
1468f5f5da8SGreg Roach     *
1478f5f5da8SGreg Roach     * @return string
1488f5f5da8SGreg Roach     */
149c1010edaSGreg Roach    public function title(): string
150c1010edaSGreg Roach    {
1518f5f5da8SGreg Roach        return $this->descriptive_title;
1528f5f5da8SGreg Roach    }
1538f5f5da8SGreg Roach
1548f5f5da8SGreg Roach    /**
15564b90bf1SGreg Roach     * Get the fact ID.
15664b90bf1SGreg Roach     *
15764b90bf1SGreg Roach     * @return string
15864b90bf1SGreg Roach     */
159c1010edaSGreg Roach    public function factId(): string
160c1010edaSGreg Roach    {
16164b90bf1SGreg Roach        return $this->fact_id;
16264b90bf1SGreg Roach    }
16364b90bf1SGreg Roach
16464b90bf1SGreg Roach    /**
165d6641c58SGreg Roach     * @return bool
166d6641c58SGreg Roach     */
1678f53f488SRico Sonntag    public function isPendingAddition(): bool
168c1010edaSGreg Roach    {
16930158ae7SGreg Roach        foreach ($this->media->facts() as $fact) {
170905ab80aSGreg Roach            if ($fact->id() === $this->fact_id) {
171d6641c58SGreg Roach                return $fact->isPendingAddition();
172d6641c58SGreg Roach            }
173d6641c58SGreg Roach        }
174d6641c58SGreg Roach
175d6641c58SGreg Roach        return false;
176d6641c58SGreg Roach    }
177d6641c58SGreg Roach
178d6641c58SGreg Roach    /**
179d6641c58SGreg Roach     * @return bool
180d6641c58SGreg Roach     */
1818f53f488SRico Sonntag    public function isPendingDeletion(): bool
182c1010edaSGreg Roach    {
18330158ae7SGreg Roach        foreach ($this->media->facts() as $fact) {
184905ab80aSGreg Roach            if ($fact->id() === $this->fact_id) {
185d6641c58SGreg Roach                return $fact->isPendingDeletion();
186d6641c58SGreg Roach            }
187d6641c58SGreg Roach        }
188d6641c58SGreg Roach
189d6641c58SGreg Roach        return false;
190d6641c58SGreg Roach    }
191d6641c58SGreg Roach
192d6641c58SGreg Roach    /**
19364b90bf1SGreg Roach     * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox.
19464b90bf1SGreg Roach     *
19564b90bf1SGreg Roach     * @param int      $width            Pixels
19664b90bf1SGreg Roach     * @param int      $height           Pixels
19764b90bf1SGreg Roach     * @param string   $fit              "crop" or "contain"
198e364afe4SGreg Roach     * @param string[] $image_attributes Additional HTML attributes
19964b90bf1SGreg Roach     *
20064b90bf1SGreg Roach     * @return string
20164b90bf1SGreg Roach     */
202e364afe4SGreg Roach    public function displayImage($width, $height, $fit, $image_attributes = []): string
203c1010edaSGreg Roach    {
20464b90bf1SGreg Roach        if ($this->isExternal()) {
20564b90bf1SGreg Roach            $src    = $this->multimedia_file_refn;
20664b90bf1SGreg Roach            $srcset = [];
20764b90bf1SGreg Roach        } else {
20864b90bf1SGreg Roach            // Generate multiple images for displays with higher pixel densities.
20964b90bf1SGreg Roach            $src    = $this->imageUrl($width, $height, $fit);
21064b90bf1SGreg Roach            $srcset = [];
211bb308685SGreg Roach            foreach ([2, 3, 4] as $x) {
21264b90bf1SGreg Roach                $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x';
21364b90bf1SGreg Roach            }
21464b90bf1SGreg Roach        }
21564b90bf1SGreg Roach
21648b53306SGreg Roach        if ($this->isImage()) {
217e364afe4SGreg Roach            $image = '<img ' . Html::attributes($image_attributes + [
21864b90bf1SGreg Roach                        'dir'    => 'auto',
21964b90bf1SGreg Roach                        'src'    => $src,
22064b90bf1SGreg Roach                        'srcset' => implode(',', $srcset),
221d33f6d5eSGreg Roach                        'alt'    => strip_tags($this->media->fullName()),
22264b90bf1SGreg Roach                    ]) . '>';
22364b90bf1SGreg Roach
224e364afe4SGreg Roach            $link_attributes = Html::attributes([
22564b90bf1SGreg Roach                'class'      => 'gallery',
22664b90bf1SGreg Roach                'type'       => $this->mimeType(),
22760e3c46aSGreg Roach                'href'       => $this->imageUrl(0, 0, 'contain'),
228d33f6d5eSGreg Roach                'data-title' => strip_tags($this->media->fullName()),
22964b90bf1SGreg Roach            ]);
230e1d1700bSGreg Roach        } else {
23148b53306SGreg Roach            $image = view('icons/mime', ['type' => $this->mimeType()]);
232e364afe4SGreg Roach
233e364afe4SGreg Roach            $link_attributes = Html::attributes([
234e1d1700bSGreg Roach                'type' => $this->mimeType(),
23571625badSGreg Roach                'href' => $this->downloadUrl('inline'),
236e1d1700bSGreg Roach            ]);
237e1d1700bSGreg Roach        }
23864b90bf1SGreg Roach
239e364afe4SGreg Roach        return '<a ' . $link_attributes . '>' . $image . '</a>';
24064b90bf1SGreg Roach    }
24164b90bf1SGreg Roach
2424a9f750fSGreg Roach    /**
2434a9f750fSGreg Roach     * Is the media file actually a URL?
2444a9f750fSGreg Roach     */
245c1010edaSGreg Roach    public function isExternal(): bool
246c1010edaSGreg Roach    {
2474a9f750fSGreg Roach        return strpos($this->multimedia_file_refn, '://') !== false;
2484a9f750fSGreg Roach    }
2494a9f750fSGreg Roach
2504a9f750fSGreg Roach    /**
2518f5f5da8SGreg Roach     * Generate a URL for an image.
2528f5f5da8SGreg Roach     *
2538f5f5da8SGreg Roach     * @param int    $width  Maximum width in pixels
2548f5f5da8SGreg Roach     * @param int    $height Maximum height in pixels
2558f5f5da8SGreg Roach     * @param string $fit    "crop" or "contain"
2568f5f5da8SGreg Roach     *
2578f5f5da8SGreg Roach     * @return string
2588f5f5da8SGreg Roach     */
2598f53f488SRico Sonntag    public function imageUrl($width, $height, $fit): string
260c1010edaSGreg Roach    {
2618f5f5da8SGreg Roach        // Sign the URL, to protect against mass-resize attacks.
2628f5f5da8SGreg Roach        $glide_key = Site::getPreference('glide-key');
26354c1ab5eSGreg Roach        if ($glide_key === '') {
2648f5f5da8SGreg Roach            $glide_key = bin2hex(random_bytes(128));
2658f5f5da8SGreg Roach            Site::setPreference('glide-key', $glide_key);
2668f5f5da8SGreg Roach        }
2678f5f5da8SGreg Roach
268f4afa648SGreg Roach        if (Auth::accessLevel($this->media->tree()) > $this->media->tree()->getPreference('SHOW_NO_WATERMARK')) {
2698f5f5da8SGreg Roach            $mark = 'watermark.png';
2708f5f5da8SGreg Roach        } else {
2718f5f5da8SGreg Roach            $mark = '';
2728f5f5da8SGreg Roach        }
2738f5f5da8SGreg Roach
274ee4364daSGreg Roach        $params = [
275c0935879SGreg Roach            'xref'      => $this->media->xref(),
276d72b284aSGreg Roach            'tree'      => $this->media->tree()->name(),
2774a9f750fSGreg Roach            'fact_id'   => $this->fact_id,
2788f5f5da8SGreg Roach            'w'         => $width,
2798f5f5da8SGreg Roach            'h'         => $height,
2808f5f5da8SGreg Roach            'fit'       => $fit,
2818f5f5da8SGreg Roach            'mark'      => $mark,
2828f5f5da8SGreg Roach            'markh'     => '100h',
2838f5f5da8SGreg Roach            'markw'     => '100w',
2848f5f5da8SGreg Roach            'markalpha' => 25,
285c1010edaSGreg Roach            'or'        => 0,
286ee4364daSGreg Roach        ];
2878f5f5da8SGreg Roach
288ee4364daSGreg Roach        $signature = SignatureFactory::create($glide_key)->generateSignature('', $params);
289ee4364daSGreg Roach
290ee4364daSGreg Roach        $params = ['route' => '/media-thumbnail', 's' => $signature] + $params;
291ee4364daSGreg Roach
292ee4364daSGreg Roach        return route('media-thumbnail', $params);
2938f5f5da8SGreg Roach    }
2948f5f5da8SGreg Roach
2958f5f5da8SGreg Roach    /**
29685a166d8SGreg Roach     * Is the media file an image?
2978f5f5da8SGreg Roach     */
29885a166d8SGreg Roach    public function isImage(): bool
299c1010edaSGreg Roach    {
30085a166d8SGreg Roach        return in_array($this->mimeType(), self::SUPPORTED_IMAGE_MIME_TYPES, true);
3018f5f5da8SGreg Roach    }
3028f5f5da8SGreg Roach
3038f5f5da8SGreg Roach    /**
3048f5f5da8SGreg Roach     * What is the mime-type of this object?
3058f5f5da8SGreg Roach     * For simplicity and efficiency, use the extension, rather than the contents.
3068f5f5da8SGreg Roach     *
3078f5f5da8SGreg Roach     * @return string
3088f5f5da8SGreg Roach     */
3098f53f488SRico Sonntag    public function mimeType(): string
310c1010edaSGreg Roach    {
31185a166d8SGreg Roach        $extension = strtolower(pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION));
31285a166d8SGreg Roach
31385a166d8SGreg Roach        return self::MIME_TYPES[$extension] ?? 'application/octet-stream';
31485a166d8SGreg Roach    }
31585a166d8SGreg Roach
31685a166d8SGreg Roach    /**
31785a166d8SGreg Roach     * Generate a URL to download a non-image media file.
31885a166d8SGreg Roach     *
31971625badSGreg Roach     * @param string $disposition How should the image be returned - "attachment" or "inline"
32071625badSGreg Roach     *
32185a166d8SGreg Roach     * @return string
32285a166d8SGreg Roach     */
32371625badSGreg Roach    public function downloadUrl(string $disposition): string
32485a166d8SGreg Roach    {
32585a166d8SGreg Roach        return route('media-download', [
32685a166d8SGreg Roach            'xref'        => $this->media->xref(),
327d72b284aSGreg Roach            'tree'        => $this->media->tree()->name(),
32885a166d8SGreg Roach            'fact_id'     => $this->fact_id,
32971625badSGreg Roach            'disposition' => $disposition,
33085a166d8SGreg Roach        ]);
33185a166d8SGreg Roach    }
33285a166d8SGreg Roach
33385a166d8SGreg Roach    /**
33485a166d8SGreg Roach     * A list of image attributes
33585a166d8SGreg Roach     *
33685a166d8SGreg Roach     * @return string[]
33785a166d8SGreg Roach     */
338a04bb9a2SGreg Roach    public function attributes(FilesystemInterface $data_filesystem): array
33985a166d8SGreg Roach    {
34085a166d8SGreg Roach        $attributes = [];
34185a166d8SGreg Roach
342a04bb9a2SGreg Roach        if (!$this->isExternal() || $this->fileExists($data_filesystem)) {
34385a166d8SGreg Roach            try {
344a04bb9a2SGreg Roach                $bytes                       = $this->media()->tree()->mediaFilesystem($data_filesystem)->getSize($this->filename());
34585a166d8SGreg Roach                $kb                          = intdiv($bytes + 1023, 1024);
34685a166d8SGreg Roach                $attributes['__FILE_SIZE__'] = I18N::translate('%s KB', I18N::number($kb));
34785a166d8SGreg Roach            } catch (FileNotFoundException $ex) {
34885a166d8SGreg Roach                // External/missing files have no size.
34985a166d8SGreg Roach            }
35085a166d8SGreg Roach
3515c98992aSGreg Roach            // Note: getAdapter() is defined on Filesystem, but not on FilesystemInterface.
352a04bb9a2SGreg Roach            $filesystem = $this->media()->tree()->mediaFilesystem($data_filesystem);
3535c98992aSGreg Roach            if ($filesystem instanceof Filesystem) {
3545c98992aSGreg Roach                $adapter = $filesystem->getAdapter();
3555c98992aSGreg Roach                // Only works for local filesystems.
3565c98992aSGreg Roach                if ($adapter instanceof Local) {
3575c98992aSGreg Roach                    $file = $adapter->applyPathPrefix($this->filename());
35885a166d8SGreg Roach                    [$width, $height] = getimagesize($file);
35985a166d8SGreg Roach                    $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($width), I18N::number($height));
3605c98992aSGreg Roach                }
36185a166d8SGreg Roach            }
36285a166d8SGreg Roach        }
36385a166d8SGreg Roach
36485a166d8SGreg Roach        return $attributes;
36585a166d8SGreg Roach    }
36685a166d8SGreg Roach
36785a166d8SGreg Roach    /**
36829518ad2SGreg Roach     * Read the contents of a media file.
36929518ad2SGreg Roach     *
370a04bb9a2SGreg Roach     * @param FilesystemInterface $data_filesystem
371a04bb9a2SGreg Roach     *
37229518ad2SGreg Roach     * @return string
37329518ad2SGreg Roach     */
374a04bb9a2SGreg Roach    public function fileContents(FilesystemInterface $data_filesystem): string
37529518ad2SGreg Roach    {
376a04bb9a2SGreg Roach        return $this->media->tree()->mediaFilesystem($data_filesystem)->read($this->multimedia_file_refn);
37729518ad2SGreg Roach    }
37829518ad2SGreg Roach
37929518ad2SGreg Roach    /**
38029518ad2SGreg Roach     * Check if the file exists on this server
38185a166d8SGreg Roach     *
382a04bb9a2SGreg Roach     * @param FilesystemInterface $data_filesystem
383a04bb9a2SGreg Roach     *
38485a166d8SGreg Roach     * @return bool
38585a166d8SGreg Roach     */
386a04bb9a2SGreg Roach    public function fileExists(FilesystemInterface $data_filesystem): bool
38785a166d8SGreg Roach    {
388a04bb9a2SGreg Roach        return $this->media->tree()->mediaFilesystem($data_filesystem)->has($this->multimedia_file_refn);
38985a166d8SGreg Roach    }
39085a166d8SGreg Roach
39185a166d8SGreg Roach    /**
39285a166d8SGreg Roach     * @return Media
39385a166d8SGreg Roach     */
39485a166d8SGreg Roach    public function media(): Media
39585a166d8SGreg Roach    {
39685a166d8SGreg Roach        return $this->media;
39785a166d8SGreg Roach    }
39885a166d8SGreg Roach
39985a166d8SGreg Roach    /**
40085a166d8SGreg Roach     * Get the filename.
40185a166d8SGreg Roach     *
40285a166d8SGreg Roach     * @return string
40385a166d8SGreg Roach     */
40485a166d8SGreg Roach    public function filename(): string
40585a166d8SGreg Roach    {
40685a166d8SGreg Roach        return $this->multimedia_file_refn;
40785a166d8SGreg Roach    }
40885a166d8SGreg Roach
40985a166d8SGreg Roach    /**
41085a166d8SGreg Roach     * What file extension is used by this file?
41185a166d8SGreg Roach     *
41285a166d8SGreg Roach     * @return string
41385a166d8SGreg Roach     */
41485a166d8SGreg Roach    public function extension(): string
41585a166d8SGreg Roach    {
41685a166d8SGreg Roach        return pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION);
4178f5f5da8SGreg Roach    }
4188f5f5da8SGreg Roach}
419