xref: /webtrees/app/MediaFile.php (revision 24f2a3af38709f9bf0a739b30264240d20ba34e8)
18f5f5da8SGreg Roach<?php
23976b470SGreg Roach
38f5f5da8SGreg Roach/**
48f5f5da8SGreg Roach * webtrees: online genealogy
589f7189bSGreg Roach * Copyright (C) 2021 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
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
168f5f5da8SGreg Roach */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
208f5f5da8SGreg Roachnamespace Fisharebest\Webtrees;
218f5f5da8SGreg Roach
2246b03695SGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\MediaFileDownload;
2346b03695SGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\MediaFileThumbnail;
245c98992aSGreg Roachuse League\Flysystem\Adapter\Local;
2585a166d8SGreg Roachuse League\Flysystem\FileNotFoundException;
265c98992aSGreg Roachuse League\Flysystem\Filesystem;
27a04bb9a2SGreg Roachuse League\Flysystem\FilesystemInterface;
283976b470SGreg Roach
296577bfc3SGreg Roachuse function bin2hex;
3085a166d8SGreg Roachuse function getimagesize;
316577bfc3SGreg Roachuse function http_build_query;
3285a166d8SGreg Roachuse function intdiv;
336577bfc3SGreg Roachuse function ksort;
346577bfc3SGreg Roachuse function md5;
3585a166d8SGreg Roachuse function pathinfo;
366577bfc3SGreg Roachuse function random_bytes;
37dec352c1SGreg Roachuse function str_contains;
3885a166d8SGreg Roachuse function strtolower;
393976b470SGreg Roach
4085a166d8SGreg Roachuse const PATHINFO_EXTENSION;
418f5f5da8SGreg Roach
428f5f5da8SGreg Roach/**
438f5f5da8SGreg Roach * A GEDCOM media file.  A media object can contain many media files,
448f5f5da8SGreg Roach * such as scans of both sides of a document, the transcript of an audio
458f5f5da8SGreg Roach * recording, etc.
468f5f5da8SGreg Roach */
47c1010edaSGreg Roachclass MediaFile
48c1010edaSGreg Roach{
4985a166d8SGreg Roach    private const SUPPORTED_IMAGE_MIME_TYPES = [
5085a166d8SGreg Roach        'image/gif',
5185a166d8SGreg Roach        'image/jpeg',
5285a166d8SGreg Roach        'image/png',
53c68bc8e2SGreg Roach        'image/webp',
5485a166d8SGreg Roach    ];
5585a166d8SGreg Roach
568f5f5da8SGreg Roach    /** @var string The filename */
578f5f5da8SGreg Roach    private $multimedia_file_refn = '';
588f5f5da8SGreg Roach
598f5f5da8SGreg Roach    /** @var string The file extension; jpeg, txt, mp4, etc. */
608f5f5da8SGreg Roach    private $multimedia_format = '';
618f5f5da8SGreg Roach
628f5f5da8SGreg Roach    /** @var string The type of document; newspaper, microfiche, etc. */
638f5f5da8SGreg Roach    private $source_media_type = '';
648f5f5da8SGreg Roach    /** @var string The filename */
658f5f5da8SGreg Roach
668f5f5da8SGreg Roach    /** @var string The name of the document */
678f5f5da8SGreg Roach    private $descriptive_title = '';
688f5f5da8SGreg Roach
698f5f5da8SGreg Roach    /** @var Media $media The media object to which this file belongs */
708f5f5da8SGreg Roach    private $media;
718f5f5da8SGreg Roach
7264b90bf1SGreg Roach    /** @var string */
7364b90bf1SGreg Roach    private $fact_id;
7464b90bf1SGreg Roach
758f5f5da8SGreg Roach    /**
768f5f5da8SGreg Roach     * Create a MediaFile from raw GEDCOM data.
778f5f5da8SGreg Roach     *
788f5f5da8SGreg Roach     * @param string $gedcom
798f5f5da8SGreg Roach     * @param Media  $media
808f5f5da8SGreg Roach     */
81*24f2a3afSGreg Roach    public function __construct(string $gedcom, Media $media)
82c1010edaSGreg Roach    {
838f5f5da8SGreg Roach        $this->media   = $media;
8464b90bf1SGreg Roach        $this->fact_id = md5($gedcom);
858f5f5da8SGreg Roach
868f5f5da8SGreg Roach        if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) {
878f5f5da8SGreg Roach            $this->multimedia_file_refn = $match[1];
88423c6ccdSGreg Roach            $this->multimedia_format    = pathinfo($match[1], PATHINFO_EXTENSION);
898f5f5da8SGreg Roach        }
908f5f5da8SGreg Roach
918f5f5da8SGreg Roach        if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) {
928f5f5da8SGreg Roach            $this->multimedia_format = $match[1];
938f5f5da8SGreg Roach        }
948f5f5da8SGreg Roach
958f5f5da8SGreg Roach        if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) {
968f5f5da8SGreg Roach            $this->source_media_type = $match[1];
978f5f5da8SGreg Roach        }
988f5f5da8SGreg Roach
998f5f5da8SGreg Roach        if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) {
1008f5f5da8SGreg Roach            $this->descriptive_title = $match[1];
1018f5f5da8SGreg Roach        }
1028f5f5da8SGreg Roach    }
1038f5f5da8SGreg Roach
1048f5f5da8SGreg Roach    /**
10564b90bf1SGreg Roach     * Get the format.
1068f5f5da8SGreg Roach     *
1078f5f5da8SGreg Roach     * @return string
1088f5f5da8SGreg Roach     */
109c1010edaSGreg Roach    public function format(): string
110c1010edaSGreg Roach    {
1118f5f5da8SGreg Roach        return $this->multimedia_format;
1128f5f5da8SGreg Roach    }
1138f5f5da8SGreg Roach
1148f5f5da8SGreg Roach    /**
11564b90bf1SGreg Roach     * Get the type.
1168f5f5da8SGreg Roach     *
1178f5f5da8SGreg Roach     * @return string
1188f5f5da8SGreg Roach     */
119c1010edaSGreg Roach    public function type(): string
120c1010edaSGreg Roach    {
1218f5f5da8SGreg Roach        return $this->source_media_type;
1228f5f5da8SGreg Roach    }
1238f5f5da8SGreg Roach
1248f5f5da8SGreg Roach    /**
12564b90bf1SGreg Roach     * Get the title.
1268f5f5da8SGreg Roach     *
1278f5f5da8SGreg Roach     * @return string
1288f5f5da8SGreg Roach     */
129c1010edaSGreg Roach    public function title(): string
130c1010edaSGreg Roach    {
1318f5f5da8SGreg Roach        return $this->descriptive_title;
1328f5f5da8SGreg Roach    }
1338f5f5da8SGreg Roach
1348f5f5da8SGreg Roach    /**
13564b90bf1SGreg Roach     * Get the fact ID.
13664b90bf1SGreg Roach     *
13764b90bf1SGreg Roach     * @return string
13864b90bf1SGreg Roach     */
139c1010edaSGreg Roach    public function factId(): string
140c1010edaSGreg Roach    {
14164b90bf1SGreg Roach        return $this->fact_id;
14264b90bf1SGreg Roach    }
14364b90bf1SGreg Roach
14464b90bf1SGreg Roach    /**
145d6641c58SGreg Roach     * @return bool
146d6641c58SGreg Roach     */
1478f53f488SRico Sonntag    public function isPendingAddition(): bool
148c1010edaSGreg Roach    {
14930158ae7SGreg Roach        foreach ($this->media->facts() as $fact) {
150905ab80aSGreg Roach            if ($fact->id() === $this->fact_id) {
151d6641c58SGreg Roach                return $fact->isPendingAddition();
152d6641c58SGreg Roach            }
153d6641c58SGreg Roach        }
154d6641c58SGreg Roach
155d6641c58SGreg Roach        return false;
156d6641c58SGreg Roach    }
157d6641c58SGreg Roach
158d6641c58SGreg Roach    /**
159d6641c58SGreg Roach     * @return bool
160d6641c58SGreg Roach     */
1618f53f488SRico Sonntag    public function isPendingDeletion(): bool
162c1010edaSGreg Roach    {
16330158ae7SGreg Roach        foreach ($this->media->facts() as $fact) {
164905ab80aSGreg Roach            if ($fact->id() === $this->fact_id) {
165d6641c58SGreg Roach                return $fact->isPendingDeletion();
166d6641c58SGreg Roach            }
167d6641c58SGreg Roach        }
168d6641c58SGreg Roach
169d6641c58SGreg Roach        return false;
170d6641c58SGreg Roach    }
171d6641c58SGreg Roach
172d6641c58SGreg Roach    /**
17364b90bf1SGreg Roach     * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox.
17464b90bf1SGreg Roach     *
17564b90bf1SGreg Roach     * @param int                  $width            Pixels
17664b90bf1SGreg Roach     * @param int                  $height           Pixels
17764b90bf1SGreg Roach     * @param string               $fit              "crop" or "contain"
178*24f2a3afSGreg Roach     * @param array<string,string> $image_attributes Additional HTML attributes
17964b90bf1SGreg Roach     *
18064b90bf1SGreg Roach     * @return string
18164b90bf1SGreg Roach     */
182*24f2a3afSGreg Roach    public function displayImage(int $width, int $height, string $fit, array $image_attributes = []): string
183c1010edaSGreg Roach    {
18464b90bf1SGreg Roach        if ($this->isExternal()) {
18564b90bf1SGreg Roach            $src    = $this->multimedia_file_refn;
18664b90bf1SGreg Roach            $srcset = [];
18764b90bf1SGreg Roach        } else {
18864b90bf1SGreg Roach            // Generate multiple images for displays with higher pixel densities.
18964b90bf1SGreg Roach            $src    = $this->imageUrl($width, $height, $fit);
19064b90bf1SGreg Roach            $srcset = [];
191bb308685SGreg Roach            foreach ([2, 3, 4] as $x) {
19264b90bf1SGreg Roach                $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x';
19364b90bf1SGreg Roach            }
19464b90bf1SGreg Roach        }
19564b90bf1SGreg Roach
19648b53306SGreg Roach        if ($this->isImage()) {
197e364afe4SGreg Roach            $image = '<img ' . Html::attributes($image_attributes + [
19864b90bf1SGreg Roach                        'dir'    => 'auto',
19964b90bf1SGreg Roach                        'src'    => $src,
20064b90bf1SGreg Roach                        'srcset' => implode(',', $srcset),
201d33f6d5eSGreg Roach                        'alt'    => strip_tags($this->media->fullName()),
20264b90bf1SGreg Roach                    ]) . '>';
20364b90bf1SGreg Roach
204e364afe4SGreg Roach            $link_attributes = Html::attributes([
20564b90bf1SGreg Roach                'class'      => 'gallery',
20664b90bf1SGreg Roach                'type'       => $this->mimeType(),
2076577bfc3SGreg Roach                'href'       => $this->downloadUrl('inline'),
208d33f6d5eSGreg Roach                'data-title' => strip_tags($this->media->fullName()),
20964b90bf1SGreg Roach            ]);
210e1d1700bSGreg Roach        } else {
21148b53306SGreg Roach            $image = view('icons/mime', ['type' => $this->mimeType()]);
212e364afe4SGreg Roach
213e364afe4SGreg Roach            $link_attributes = Html::attributes([
214e1d1700bSGreg Roach                'type' => $this->mimeType(),
21571625badSGreg Roach                'href' => $this->downloadUrl('inline'),
216e1d1700bSGreg Roach            ]);
217e1d1700bSGreg Roach        }
21864b90bf1SGreg Roach
219e364afe4SGreg Roach        return '<a ' . $link_attributes . '>' . $image . '</a>';
22064b90bf1SGreg Roach    }
22164b90bf1SGreg Roach
2224a9f750fSGreg Roach    /**
2234a9f750fSGreg Roach     * Is the media file actually a URL?
2244a9f750fSGreg Roach     */
225c1010edaSGreg Roach    public function isExternal(): bool
226c1010edaSGreg Roach    {
227dec352c1SGreg Roach        return str_contains($this->multimedia_file_refn, '://');
2284a9f750fSGreg Roach    }
2294a9f750fSGreg Roach
2304a9f750fSGreg Roach    /**
2318f5f5da8SGreg Roach     * Generate a URL for an image.
2328f5f5da8SGreg Roach     *
2338f5f5da8SGreg Roach     * @param int    $width  Maximum width in pixels
2348f5f5da8SGreg Roach     * @param int    $height Maximum height in pixels
2358f5f5da8SGreg Roach     * @param string $fit    "crop" or "contain"
2368f5f5da8SGreg Roach     *
2378f5f5da8SGreg Roach     * @return string
2388f5f5da8SGreg Roach     */
239*24f2a3afSGreg Roach    public function imageUrl(int $width, int $height, string $fit): string
240c1010edaSGreg Roach    {
2418f5f5da8SGreg Roach        // Sign the URL, to protect against mass-resize attacks.
2428f5f5da8SGreg Roach        $glide_key = Site::getPreference('glide-key');
24346b03695SGreg Roach
24454c1ab5eSGreg Roach        if ($glide_key === '') {
2458f5f5da8SGreg Roach            $glide_key = bin2hex(random_bytes(128));
2468f5f5da8SGreg Roach            Site::setPreference('glide-key', $glide_key);
2478f5f5da8SGreg Roach        }
2488f5f5da8SGreg Roach
2496577bfc3SGreg Roach        // The "mark" parameter is ignored, but needed for cache-busting.
250ee4364daSGreg Roach        $params = [
251c0935879SGreg Roach            'xref'      => $this->media->xref(),
252d72b284aSGreg Roach            'tree'      => $this->media->tree()->name(),
2534a9f750fSGreg Roach            'fact_id'   => $this->fact_id,
2548f5f5da8SGreg Roach            'w'         => $width,
2558f5f5da8SGreg Roach            'h'         => $height,
2568f5f5da8SGreg Roach            'fit'       => $fit,
2576b9cb339SGreg Roach            'mark'      => Registry::imageFactory()->thumbnailNeedsWatermark($this, Auth::user())
25846b03695SGreg Roach        ];
25946b03695SGreg Roach
2606577bfc3SGreg Roach        $params['s'] = $this->signature($params);
261ee4364daSGreg Roach
26246b03695SGreg Roach        return route(MediaFileThumbnail::class, $params);
2638f5f5da8SGreg Roach    }
2648f5f5da8SGreg Roach
2658f5f5da8SGreg Roach    /**
26685a166d8SGreg Roach     * Is the media file an image?
2678f5f5da8SGreg Roach     */
26885a166d8SGreg Roach    public function isImage(): bool
269c1010edaSGreg Roach    {
27085a166d8SGreg Roach        return in_array($this->mimeType(), self::SUPPORTED_IMAGE_MIME_TYPES, true);
2718f5f5da8SGreg Roach    }
2728f5f5da8SGreg Roach
2738f5f5da8SGreg Roach    /**
2748f5f5da8SGreg Roach     * What is the mime-type of this object?
2758f5f5da8SGreg Roach     * For simplicity and efficiency, use the extension, rather than the contents.
2768f5f5da8SGreg Roach     *
2778f5f5da8SGreg Roach     * @return string
2788f5f5da8SGreg Roach     */
2798f53f488SRico Sonntag    public function mimeType(): string
280c1010edaSGreg Roach    {
28185a166d8SGreg Roach        $extension = strtolower(pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION));
28285a166d8SGreg Roach
283e7f16b43SGreg Roach        return Mime::TYPES[$extension] ?? Mime::DEFAULT_TYPE;
28485a166d8SGreg Roach    }
28585a166d8SGreg Roach
28685a166d8SGreg Roach    /**
2876577bfc3SGreg Roach     * Generate a URL to download a media file.
28885a166d8SGreg Roach     *
28971625badSGreg Roach     * @param string $disposition How should the image be returned - "attachment" or "inline"
29071625badSGreg Roach     *
29185a166d8SGreg Roach     * @return string
29285a166d8SGreg Roach     */
29371625badSGreg Roach    public function downloadUrl(string $disposition): string
29485a166d8SGreg Roach    {
2956577bfc3SGreg Roach        // The "mark" parameter is ignored, but needed for cache-busting.
29646b03695SGreg Roach        return route(MediaFileDownload::class, [
29785a166d8SGreg Roach            'xref'        => $this->media->xref(),
298d72b284aSGreg Roach            'tree'        => $this->media->tree()->name(),
29985a166d8SGreg Roach            'fact_id'     => $this->fact_id,
30071625badSGreg Roach            'disposition' => $disposition,
3016b9cb339SGreg Roach            'mark'        => Registry::imageFactory()->fileNeedsWatermark($this, Auth::user())
30285a166d8SGreg Roach        ]);
30385a166d8SGreg Roach    }
30485a166d8SGreg Roach
30585a166d8SGreg Roach    /**
30685a166d8SGreg Roach     * A list of image attributes
30785a166d8SGreg Roach     *
3088a3784e1SGreg Roach     * @param FilesystemInterface $data_filesystem
3098a3784e1SGreg Roach     *
310*24f2a3afSGreg Roach     * @return array<string>
31185a166d8SGreg Roach     */
312a04bb9a2SGreg Roach    public function attributes(FilesystemInterface $data_filesystem): array
31385a166d8SGreg Roach    {
31485a166d8SGreg Roach        $attributes = [];
31585a166d8SGreg Roach
316a04bb9a2SGreg Roach        if (!$this->isExternal() || $this->fileExists($data_filesystem)) {
31785a166d8SGreg Roach            try {
318a04bb9a2SGreg Roach                $bytes                       = $this->media()->tree()->mediaFilesystem($data_filesystem)->getSize($this->filename());
31985a166d8SGreg Roach                $kb                          = intdiv($bytes + 1023, 1024);
32085a166d8SGreg Roach                $attributes['__FILE_SIZE__'] = I18N::translate('%s KB', I18N::number($kb));
32185a166d8SGreg Roach            } catch (FileNotFoundException $ex) {
32285a166d8SGreg Roach                // External/missing files have no size.
32385a166d8SGreg Roach            }
32485a166d8SGreg Roach
3255c98992aSGreg Roach            // Note: getAdapter() is defined on Filesystem, but not on FilesystemInterface.
326a04bb9a2SGreg Roach            $filesystem = $this->media()->tree()->mediaFilesystem($data_filesystem);
3275c98992aSGreg Roach            if ($filesystem instanceof Filesystem) {
3285c98992aSGreg Roach                $adapter = $filesystem->getAdapter();
3295c98992aSGreg Roach                // Only works for local filesystems.
3305c98992aSGreg Roach                if ($adapter instanceof Local) {
3315c98992aSGreg Roach                    $file = $adapter->applyPathPrefix($this->filename());
33285a166d8SGreg Roach                    [$width, $height] = getimagesize($file);
33385a166d8SGreg Roach                    $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($width), I18N::number($height));
3345c98992aSGreg Roach                }
33585a166d8SGreg Roach            }
33685a166d8SGreg Roach        }
33785a166d8SGreg Roach
33885a166d8SGreg Roach        return $attributes;
33985a166d8SGreg Roach    }
34085a166d8SGreg Roach
34185a166d8SGreg Roach    /**
34229518ad2SGreg Roach     * Read the contents of a media file.
34329518ad2SGreg Roach     *
344a04bb9a2SGreg Roach     * @param FilesystemInterface $data_filesystem
345a04bb9a2SGreg Roach     *
34629518ad2SGreg Roach     * @return string
34729518ad2SGreg Roach     */
348a04bb9a2SGreg Roach    public function fileContents(FilesystemInterface $data_filesystem): string
34929518ad2SGreg Roach    {
350a04bb9a2SGreg Roach        return $this->media->tree()->mediaFilesystem($data_filesystem)->read($this->multimedia_file_refn);
35129518ad2SGreg Roach    }
35229518ad2SGreg Roach
35329518ad2SGreg Roach    /**
35429518ad2SGreg Roach     * Check if the file exists on this server
35585a166d8SGreg Roach     *
356a04bb9a2SGreg Roach     * @param FilesystemInterface $data_filesystem
357a04bb9a2SGreg Roach     *
35885a166d8SGreg Roach     * @return bool
35985a166d8SGreg Roach     */
360a04bb9a2SGreg Roach    public function fileExists(FilesystemInterface $data_filesystem): bool
36185a166d8SGreg Roach    {
362a04bb9a2SGreg Roach        return $this->media->tree()->mediaFilesystem($data_filesystem)->has($this->multimedia_file_refn);
36385a166d8SGreg Roach    }
36485a166d8SGreg Roach
36585a166d8SGreg Roach    /**
36685a166d8SGreg Roach     * @return Media
36785a166d8SGreg Roach     */
36885a166d8SGreg Roach    public function media(): Media
36985a166d8SGreg Roach    {
37085a166d8SGreg Roach        return $this->media;
37185a166d8SGreg Roach    }
37285a166d8SGreg Roach
37385a166d8SGreg Roach    /**
37485a166d8SGreg Roach     * Get the filename.
37585a166d8SGreg Roach     *
37685a166d8SGreg Roach     * @return string
37785a166d8SGreg Roach     */
37885a166d8SGreg Roach    public function filename(): string
37985a166d8SGreg Roach    {
38085a166d8SGreg Roach        return $this->multimedia_file_refn;
38185a166d8SGreg Roach    }
38285a166d8SGreg Roach
38385a166d8SGreg Roach    /**
38485a166d8SGreg Roach     * What file extension is used by this file?
38585a166d8SGreg Roach     *
38685a166d8SGreg Roach     * @return string
387e7f16b43SGreg Roach     *
388e7f16b43SGreg Roach     * @deprecated since 2.0.4.  Will be removed in 2.1.0
38985a166d8SGreg Roach     */
39085a166d8SGreg Roach    public function extension(): string
39185a166d8SGreg Roach    {
39285a166d8SGreg Roach        return pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION);
3938f5f5da8SGreg Roach    }
3946577bfc3SGreg Roach
3956577bfc3SGreg Roach    /**
396fceda430SGreg Roach     * Create a URL signature parameter, using the same algorithm as league/glide,
3976577bfc3SGreg Roach     * for compatibility with URLs generated by older versions of webtrees.
3986577bfc3SGreg Roach     *
3996577bfc3SGreg Roach     * @param array<mixed> $params
4006577bfc3SGreg Roach     *
4016577bfc3SGreg Roach     * @return string
4026577bfc3SGreg Roach     */
4036577bfc3SGreg Roach    public function signature(array $params): string
4046577bfc3SGreg Roach    {
4056577bfc3SGreg Roach        unset($params['s']);
4066577bfc3SGreg Roach
4076577bfc3SGreg Roach        ksort($params);
4086577bfc3SGreg Roach
4096577bfc3SGreg Roach        // Sign the URL, to protect against mass-resize attacks.
4106577bfc3SGreg Roach        $glide_key = Site::getPreference('glide-key');
4116577bfc3SGreg Roach
4126577bfc3SGreg Roach        if ($glide_key === '') {
4136577bfc3SGreg Roach            $glide_key = bin2hex(random_bytes(128));
4146577bfc3SGreg Roach            Site::setPreference('glide-key', $glide_key);
4156577bfc3SGreg Roach        }
4166577bfc3SGreg Roach
4176577bfc3SGreg Roach        return md5($glide_key . ':?' . http_build_query($params));
4186577bfc3SGreg Roach    }
4198f5f5da8SGreg Roach}
420