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 array<string,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