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