1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 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 <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Module; 21 22use Aura\Router\RouterContainer; 23use Fig\Http\Message\RequestMethodInterface; 24use Fisharebest\Webtrees\Auth; 25use Fisharebest\Webtrees\Registry; 26use Fisharebest\Webtrees\GedcomRecord; 27use Fisharebest\Webtrees\I18N; 28use Fisharebest\Webtrees\Media; 29use Fisharebest\Webtrees\Tree; 30use Fisharebest\Webtrees\Validator; 31use Illuminate\Database\Capsule\Manager as DB; 32use Illuminate\Database\Query\Builder; 33use Illuminate\Database\Query\JoinClause; 34use Illuminate\Support\Collection; 35use Psr\Http\Message\ResponseInterface; 36use Psr\Http\Message\ServerRequestInterface; 37use Psr\Http\Server\RequestHandlerInterface; 38 39use function addcslashes; 40use function app; 41use function array_combine; 42use function array_unshift; 43use function assert; 44use function dirname; 45use function max; 46use function min; 47use function redirect; 48use function route; 49 50/** 51 * Class MediaListModule 52 */ 53class MediaListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface 54{ 55 use ModuleListTrait; 56 57 protected const ROUTE_URL = '/tree/{tree}/media-list'; 58 59 /** 60 * Initialization. 61 * 62 * @return void 63 */ 64 public function boot(): void 65 { 66 $router_container = app(RouterContainer::class); 67 assert($router_container instanceof RouterContainer); 68 69 $router_container->getMap() 70 ->get(static::class, static::ROUTE_URL, $this) 71 ->allows(RequestMethodInterface::METHOD_POST); 72 } 73 74 /** 75 * How should this module be identified in the control panel, etc.? 76 * 77 * @return string 78 */ 79 public function title(): string 80 { 81 /* I18N: Name of a module/list */ 82 return I18N::translate('Media objects'); 83 } 84 85 /** 86 * A sentence describing what this module does. 87 * 88 * @return string 89 */ 90 public function description(): string 91 { 92 /* I18N: Description of the “Media objects” module */ 93 return I18N::translate('A list of media objects.'); 94 } 95 96 /** 97 * CSS class for the URL. 98 * 99 * @return string 100 */ 101 public function listMenuClass(): string 102 { 103 return 'menu-list-obje'; 104 } 105 106 /** 107 * @param Tree $tree 108 * @param array<bool|int|string|array<string>|null> $parameters 109 * 110 * @return string 111 */ 112 public function listUrl(Tree $tree, array $parameters = []): string 113 { 114 $parameters['tree'] = $tree->name(); 115 116 return route(static::class, $parameters); 117 } 118 119 /** 120 * @return array<string> 121 */ 122 public function listUrlAttributes(): array 123 { 124 return []; 125 } 126 127 /** 128 * @param Tree $tree 129 * 130 * @return bool 131 */ 132 public function listIsEmpty(Tree $tree): bool 133 { 134 return !DB::table('media') 135 ->where('m_file', '=', $tree->id()) 136 ->exists(); 137 } 138 139 /** 140 * Handle URLs generated by older versions of webtrees 141 * 142 * @param ServerRequestInterface $request 143 * 144 * @return ResponseInterface 145 */ 146 public function getListAction(ServerRequestInterface $request): ResponseInterface 147 { 148 $tree = Validator::attributes($request)->tree(); 149 150 return redirect($this->listUrl($tree, $request->getQueryParams())); 151 } 152 153 /** 154 * @param ServerRequestInterface $request 155 * 156 * @return ResponseInterface 157 */ 158 public function handle(ServerRequestInterface $request): ResponseInterface 159 { 160 $tree = Validator::attributes($request)->tree(); 161 $user = Validator::attributes($request)->user(); 162 163 $data_filesystem = Registry::filesystem()->data(); 164 165 Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user); 166 167 // Convert POST requests into GET requests for pretty URLs. 168 if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 169 return redirect($this->listUrl($tree, (array) $request->getParsedBody())); 170 } 171 172 $params = $request->getQueryParams(); 173 $formats = Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE')->values(); 174 $go = $params['go'] ?? ''; 175 $page = (int) ($params['page'] ?? 1); 176 $max = (int) ($params['max'] ?? 20); 177 $folder = $params['folder'] ?? ''; 178 $filter = $params['filter'] ?? ''; 179 $subdirs = $params['subdirs'] ?? ''; 180 $format = $params['format'] ?? ''; 181 182 $folders = $this->allFolders($tree); 183 184 if ($go === '1') { 185 $media_objects = $this->allMedia( 186 $tree, 187 $folder, 188 $subdirs === '1' ? 'include' : 'exclude', 189 'title', 190 $filter, 191 $format 192 ); 193 } else { 194 $media_objects = new Collection(); 195 } 196 197 // Pagination 198 $count = $media_objects->count(); 199 $pages = (int) (($count + $max - 1) / $max); 200 $page = max(min($page, $pages), 1); 201 202 $media_objects = $media_objects->slice(($page - 1) * $max, $max); 203 204 return $this->viewResponse('modules/media-list/page', [ 205 'count' => $count, 206 'filter' => $filter, 207 'folder' => $folder, 208 'folders' => $folders, 209 'format' => $format, 210 'formats' => $formats, 211 'max' => $max, 212 'media_objects' => $media_objects, 213 'page' => $page, 214 'pages' => $pages, 215 'subdirs' => $subdirs, 216 'data_filesystem' => $data_filesystem, 217 'module' => $this, 218 'title' => I18N::translate('Media'), 219 'tree' => $tree, 220 ]); 221 } 222 223 /** 224 * Generate a list of all the folders in a current tree. 225 * 226 * @param Tree $tree 227 * 228 * @return array<string> 229 */ 230 private function allFolders(Tree $tree): array 231 { 232 $folders = DB::table('media_file') 233 ->where('m_file', '=', $tree->id()) 234 ->where('multimedia_file_refn', 'NOT LIKE', 'http:%') 235 ->where('multimedia_file_refn', 'NOT LIKE', 'https:%') 236 ->where('multimedia_file_refn', 'LIKE', '%/%') 237 ->pluck('multimedia_file_refn', 'multimedia_file_refn') 238 ->map(static function (string $path): string { 239 return dirname($path); 240 }) 241 ->uniqueStrict() 242 ->sort() 243 ->all(); 244 245 // Ensure we have an empty (top level) folder. 246 array_unshift($folders, ''); 247 248 return array_combine($folders, $folders); 249 } 250 251 /** 252 * Generate a list of all the media objects matching the criteria in a current tree. 253 * 254 * @param Tree $tree find media in this tree 255 * @param string $folder folder to search 256 * @param string $subfolders either "include" or "exclude" 257 * @param string $sort either "file" or "title" 258 * @param string $filter optional search string 259 * @param string $format option OBJE/FILE/FORM/TYPE 260 * 261 * @return Collection<int,Media> 262 */ 263 private function allMedia(Tree $tree, string $folder, string $subfolders, string $sort, string $filter, string $format): Collection 264 { 265 $query = DB::table('media') 266 ->join('media_file', static function (JoinClause $join): void { 267 $join 268 ->on('media_file.m_file', '=', 'media.m_file') 269 ->on('media_file.m_id', '=', 'media.m_id'); 270 }) 271 ->where('media.m_file', '=', $tree->id()); 272 273 if ($folder === '') { 274 // Include external URLs in the root folder. 275 if ($subfolders === 'exclude') { 276 $query->where(static function (Builder $query): void { 277 $query 278 ->where('multimedia_file_refn', 'NOT LIKE', '%/%') 279 ->orWhere('multimedia_file_refn', 'LIKE', 'http:%') 280 ->orWhere('multimedia_file_refn', 'LIKE', 'https:%'); 281 }); 282 } 283 } else { 284 // Exclude external URLs from the root folder. 285 $query 286 ->where('multimedia_file_refn', 'LIKE', $folder . '/%') 287 ->where('multimedia_file_refn', 'NOT LIKE', 'http:%') 288 ->where('multimedia_file_refn', 'NOT LIKE', 'https:%'); 289 290 if ($subfolders === 'exclude') { 291 $query->where('multimedia_file_refn', 'NOT LIKE', $folder . '/%/%'); 292 } 293 } 294 295 // Apply search terms 296 if ($filter !== '') { 297 $query->where(static function (Builder $query) use ($filter): void { 298 $like = '%' . addcslashes($filter, '\\%_') . '%'; 299 $query 300 ->where('multimedia_file_refn', 'LIKE', $like) 301 ->orWhere('descriptive_title', 'LIKE', $like); 302 }); 303 } 304 305 if ($format) { 306 $query->where('source_media_type', '=', $format); 307 } 308 309 switch ($sort) { 310 case 'file': 311 $query->orderBy('multimedia_file_refn'); 312 break; 313 case 'title': 314 $query->orderBy('descriptive_title'); 315 break; 316 } 317 318 return $query 319 ->get() 320 ->map(Registry::mediaFactory()->mapper($tree)) 321 ->uniqueStrict() 322 ->filter(GedcomRecord::accessFilter()); 323 } 324} 325