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