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