xref: /webtrees/app/Module/ModuleCustomTrait.php (revision 1293b98145ba478ac4640f77adba7cf9246f91dd)
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
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Module;
21
22use Fig\Http\Message\StatusCodeInterface;
23use Fisharebest\Webtrees\Cache;
24use Fisharebest\Webtrees\Exceptions\HttpAccessDeniedException;
25use Fisharebest\Webtrees\Exceptions\HttpNotFoundException;
26use Fisharebest\Webtrees\Mime;
27use GuzzleHttp\Client;
28use GuzzleHttp\Exception\RequestException;
29use Psr\Http\Message\ResponseInterface;
30use Psr\Http\Message\ServerRequestInterface;
31
32use function app;
33use function assert;
34use function str_contains;
35use function strlen;
36use function strtolower;
37
38/**
39 * Trait ModuleCustomTrait - default implementation of ModuleCustomInterface
40 */
41trait ModuleCustomTrait
42{
43    /**
44     * The person or organisation who created this module.
45     *
46     * @return string
47     */
48    public function customModuleAuthorName(): string
49    {
50        return '';
51    }
52
53    /**
54     * The version of this module.
55     *
56     * @return string  e.g. '1.2.3'
57     */
58    public function customModuleVersion(): string
59    {
60        return '';
61    }
62
63    /**
64     * A URL that will provide the latest version of this module.
65     *
66     * @return string
67     */
68    public function customModuleLatestVersionUrl(): string
69    {
70        return '';
71    }
72
73    /**
74     * Fetch the latest version of this module.
75     *
76     * @return string
77     */
78    public function customModuleLatestVersion(): string
79    {
80        // No update URL provided.
81        if ($this->customModuleLatestVersionUrl() === '') {
82            return $this->customModuleVersion();
83        }
84
85        $cache = app('cache.files');
86        assert($cache instanceof Cache);
87
88        return $cache->remember($this->name() . '-latest-version', function () {
89            try {
90                $client = new Client([
91                    'timeout' => 3,
92                ]);
93
94                $response = $client->get($this->customModuleLatestVersionUrl());
95
96                if ($response->getStatusCode() === StatusCodeInterface::STATUS_OK) {
97                    $version = $response->getBody()->getContents();
98
99                    // Does the response look like a version?
100                    if (preg_match('/^\d+\.\d+\.\d+/', $version)) {
101                        return $version;
102                    }
103                }
104            } catch (RequestException $ex) {
105                // Can't connect to the server?
106            }
107
108            return $this->customModuleVersion();
109        }, 86400);
110    }
111
112    /**
113     * Where to get support for this module.  Perhaps a github repository?
114     *
115     * @return string
116     */
117    public function customModuleSupportUrl(): string
118    {
119        return '';
120    }
121
122    /**
123     * Additional/updated translations.
124     *
125     * @param string $language
126     *
127     * @return array<string,string>
128     */
129    public function customTranslations(string $language): array
130    {
131        return [];
132    }
133
134    /**
135     * Create a URL for an asset.
136     *
137     * @param string $asset e.g. "css/theme.css" or "img/banner.png"
138     *
139     * @return string
140     */
141    public function assetUrl(string $asset): string
142    {
143        $file = $this->resourcesFolder() . $asset;
144
145        // Add the file's modification time to the URL, so we can set long expiry cache headers.
146        $hash = filemtime($file);
147
148        return route('module', [
149            'module' => $this->name(),
150            'action' => 'Asset',
151            'asset'  => $asset,
152            'hash'   => $hash,
153        ]);
154    }
155
156    /**
157     * Serve a CSS/JS file.
158     *
159     * @param ServerRequestInterface $request
160     *
161     * @return ResponseInterface
162     */
163    public function getAssetAction(ServerRequestInterface $request): ResponseInterface
164    {
165        // The file being requested.  e.g. "css/theme.css"
166        $asset = $request->getQueryParams()['asset'];
167
168        // Do not allow requests that try to access parent folders.
169        if (str_contains($asset, '..')) {
170            throw new HttpAccessDeniedException($asset);
171        }
172
173        // Find the file for this asset.
174        // Note that we could also generate CSS files using views/templates.
175        // e.g. $file = view(....)
176        $file = $this->resourcesFolder() . $asset;
177
178        if (!file_exists($file)) {
179            throw new HttpNotFoundException(e($file));
180        }
181
182        $content   = file_get_contents($file);
183        $extension = strtolower(pathinfo($asset, PATHINFO_EXTENSION));
184        $mime_type = Mime::TYPES[$extension] ?? Mime::DEFAULT_TYPE;
185
186        return response($content, StatusCodeInterface::STATUS_OK)
187            ->withHeader('Cache-Control', 'public,max-age=31536000')
188            ->withHeader('Content-Length', (string) strlen($content))
189            ->withHeader('Content-Type', $mime_type);
190    }
191}
192