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