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