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\Contracts\UserInterface; 26use Fisharebest\Webtrees\Registry; 27use Fisharebest\Webtrees\GedcomRecord; 28use Fisharebest\Webtrees\I18N; 29use Fisharebest\Webtrees\Media; 30use Fisharebest\Webtrees\Tree; 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 return redirect($this->listUrl($request->getAttribute('tree'), $request->getQueryParams())); 149 } 150 151 /** 152 * @param ServerRequestInterface $request 153 * 154 * @return ResponseInterface 155 */ 156 public function handle(ServerRequestInterface $request): ResponseInterface 157 { 158 $tree = $request->getAttribute('tree'); 159 assert($tree instanceof Tree); 160 161 $user = $request->getAttribute('user'); 162 assert($user instanceof UserInterface); 163 164 $data_filesystem = Registry::filesystem()->data(); 165 166 Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user); 167 168 // Convert POST requests into GET requests for pretty URLs. 169 if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 170 return redirect($this->listUrl($tree, (array) $request->getParsedBody())); 171 } 172 173 $params = $request->getQueryParams(); 174 $formats = Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE')->values(); 175 $go = $params['go'] ?? ''; 176 $page = (int) ($params['page'] ?? 1); 177 $max = (int) ($params['max'] ?? 20); 178 $folder = $params['folder'] ?? ''; 179 $filter = $params['filter'] ?? ''; 180 $subdirs = $params['subdirs'] ?? ''; 181 $format = $params['format'] ?? ''; 182 183 $folders = $this->allFolders($tree); 184 185 if ($go === '1') { 186 $media_objects = $this->allMedia( 187 $tree, 188 $folder, 189 $subdirs === '1' ? 'include' : 'exclude', 190 'title', 191 $filter, 192 $format 193 ); 194 } else { 195 $media_objects = new Collection(); 196 } 197 198 // Pagination 199 $count = $media_objects->count(); 200 $pages = (int) (($count + $max - 1) / $max); 201 $page = max(min($page, $pages), 1); 202 203 $media_objects = $media_objects->slice(($page - 1) * $max, $max); 204 205 return $this->viewResponse('modules/media-list/page', [ 206 'count' => $count, 207 'filter' => $filter, 208 'folder' => $folder, 209 'folders' => $folders, 210 'format' => $format, 211 'formats' => $formats, 212 'max' => $max, 213 'media_objects' => $media_objects, 214 'page' => $page, 215 'pages' => $pages, 216 'subdirs' => $subdirs, 217 'data_filesystem' => $data_filesystem, 218 'module' => $this, 219 'title' => I18N::translate('Media'), 220 'tree' => $tree, 221 ]); 222 } 223 224 /** 225 * Generate a list of all the folders in a current tree. 226 * 227 * @param Tree $tree 228 * 229 * @return array<string> 230 */ 231 private function allFolders(Tree $tree): array 232 { 233 $folders = DB::table('media_file') 234 ->where('m_file', '=', $tree->id()) 235 ->where('multimedia_file_refn', 'NOT LIKE', 'http:%') 236 ->where('multimedia_file_refn', 'NOT LIKE', 'https:%') 237 ->where('multimedia_file_refn', 'LIKE', '%/%') 238 ->pluck('multimedia_file_refn', 'multimedia_file_refn') 239 ->map(static function (string $path): string { 240 return dirname($path); 241 }) 242 ->uniqueStrict() 243 ->sort() 244 ->all(); 245 246 // Ensure we have an empty (top level) folder. 247 array_unshift($folders, ''); 248 249 return array_combine($folders, $folders); 250 } 251 252 /** 253 * Generate a list of all the media objects matching the criteria in a current tree. 254 * 255 * @param Tree $tree find media in this tree 256 * @param string $folder folder to search 257 * @param string $subfolders either "include" or "exclude" 258 * @param string $sort either "file" or "title" 259 * @param string $filter optional search string 260 * @param string $format option OBJE/FILE/FORM/TYPE 261 * 262 * @return Collection<int,Media> 263 */ 264 private function allMedia(Tree $tree, string $folder, string $subfolders, string $sort, string $filter, string $format): Collection 265 { 266 $query = DB::table('media') 267 ->join('media_file', static function (JoinClause $join): void { 268 $join 269 ->on('media_file.m_file', '=', 'media.m_file') 270 ->on('media_file.m_id', '=', 'media.m_id'); 271 }) 272 ->where('media.m_file', '=', $tree->id()); 273 274 if ($folder === '') { 275 // Include external URLs in the root folder. 276 if ($subfolders === 'exclude') { 277 $query->where(static function (Builder $query): void { 278 $query 279 ->where('multimedia_file_refn', 'NOT LIKE', '%/%') 280 ->orWhere('multimedia_file_refn', 'LIKE', 'http:%') 281 ->orWhere('multimedia_file_refn', 'LIKE', 'https:%'); 282 }); 283 } 284 } else { 285 // Exclude external URLs from the root folder. 286 $query 287 ->where('multimedia_file_refn', 'LIKE', $folder . '/%') 288 ->where('multimedia_file_refn', 'NOT LIKE', 'http:%') 289 ->where('multimedia_file_refn', 'NOT LIKE', 'https:%'); 290 291 if ($subfolders === 'exclude') { 292 $query->where('multimedia_file_refn', 'NOT LIKE', $folder . '/%/%'); 293 } 294 } 295 296 // Apply search terms 297 if ($filter !== '') { 298 $query->where(static function (Builder $query) use ($filter): void { 299 $like = '%' . addcslashes($filter, '\\%_') . '%'; 300 $query 301 ->where('multimedia_file_refn', 'LIKE', $like) 302 ->orWhere('descriptive_title', 'LIKE', $like); 303 }); 304 } 305 306 if ($format) { 307 $query->where('source_media_type', '=', $format); 308 } 309 310 switch ($sort) { 311 case 'file': 312 $query->orderBy('multimedia_file_refn'); 313 break; 314 case 'title': 315 $query->orderBy('descriptive_title'); 316 break; 317 } 318 319 return $query 320 ->get() 321 ->map(Registry::mediaFactory()->mapper($tree)) 322 ->uniqueStrict() 323 ->filter(GedcomRecord::accessFilter()); 324 } 325} 326