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; 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 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($file); 194 } 195 196 $content = file_get_contents($file); 197 $extension = pathinfo($asset, PATHINFO_EXTENSION); 198 199 $mime_types = [ 200 'css' => 'text/css', 201 'gif' => 'image/gif', 202 'js' => 'application/javascript', 203 'jpg' => 'image/jpeg', 204 'jpeg' => 'image/jpeg', 205 'json' => 'application/json', 206 'png' => 'image/png', 207 'txt' => 'text/plain', 208 ]; 209 210 $mime_type = $mime_types[$extension] ?? 'application/octet-stream'; 211 212 $headers = [ 213 'Content-Type' => $mime_type, 214 'Cache-Control' => 'max-age=31536000, public', 215 'Content-Length' => strlen($content), 216 'Expires' => Carbon::now()->addYears(10)->toRfc7231String(), 217 ]; 218 219 return response($content, StatusCodeInterface::STATUS_OK, $headers); 220 } 221} 222