xref: /webtrees/app/Module/ModuleCustomTrait.php (revision cf7c85896a54223772c76826381a9151d4a21e10)
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 Illuminate\Support\Str;
30use Psr\Http\Message\ResponseInterface;
31use Psr\Http\Message\ServerRequestInterface;
32
33use function app;
34use function assert;
35use function strlen;
36use function strtolower;
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(e($file));
194        }
195
196        $content   = file_get_contents($file);
197        $extension = strtolower(pathinfo($asset, PATHINFO_EXTENSION));
198        $mime_type = Mime::TYPES[$extension] ?? Mime::DEFAULT_TYPE;
199
200        return response($content, StatusCodeInterface::STATUS_OK)
201            ->withHeader('Cache-Control', 'max-age=31536000, public')
202            ->withHeader('Content-Length', (string) strlen($content))
203            ->withHeader('Content-Type', $mime_type);
204    }
205}
206