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