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