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