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