1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 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 Fig\Http\Message\RequestMethodInterface; 23use Fisharebest\Webtrees\Auth; 24use Fisharebest\Webtrees\DB; 25use Fisharebest\Webtrees\GedcomRecord; 26use Fisharebest\Webtrees\I18N; 27use Fisharebest\Webtrees\Media; 28use Fisharebest\Webtrees\Registry; 29use Fisharebest\Webtrees\Services\LinkedRecordService; 30use Fisharebest\Webtrees\Tree; 31use Fisharebest\Webtrees\Validator; 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 array_combine; 41use function array_unshift; 42use function dirname; 43use function max; 44use function min; 45use function redirect; 46use function route; 47 48/** 49 * Class MediaListModule 50 */ 51class MediaListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface 52{ 53 use ModuleListTrait; 54 55 protected const ROUTE_URL = '/tree/{tree}/media-list'; 56 57 private LinkedRecordService $linked_record_service; 58 59 /** 60 * @param LinkedRecordService $linked_record_service 61 */ 62 public function __construct(LinkedRecordService $linked_record_service) 63 { 64 $this->linked_record_service = $linked_record_service; 65 } 66 67 /** 68 * Initialization. 69 * 70 * @return void 71 */ 72 public function boot(): void 73 { 74 Registry::routeFactory()->routeMap() 75 ->get(static::class, static::ROUTE_URL, $this) 76 ->allows(RequestMethodInterface::METHOD_POST); 77 } 78 79 /** 80 * How should this module be identified in the control panel, etc.? 81 * 82 * @return string 83 */ 84 public function title(): string 85 { 86 /* I18N: Name of a module/list */ 87 return I18N::translate('Media objects'); 88 } 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 * @param ServerRequestInterface $request 141 * 142 * @return ResponseInterface 143 */ 144 public function handle(ServerRequestInterface $request): ResponseInterface 145 { 146 $tree = Validator::attributes($request)->tree(); 147 $user = Validator::attributes($request)->user(); 148 149 Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user); 150 151 $formats = Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE')->values(); 152 153 // Convert POST requests into GET requests for pretty URLs. 154 if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 155 $params = [ 156 'go' => true, 157 'page' => Validator::parsedBody($request)->integer('page'), 158 'max' => Validator::parsedBody($request)->integer('max'), 159 'folder' => Validator::parsedBody($request)->string('folder'), 160 'filter' => Validator::parsedBody($request)->string('filter'), 161 'subdirs' => Validator::parsedBody($request)->boolean('subdirs', false), 162 'format' => Validator::parsedBody($request)->isInArrayKeys($formats)->string('format'), 163 ]; 164 165 return redirect($this->listUrl($tree, $params)); 166 } 167 168 $folders = $this->allFolders($tree); 169 $go = Validator::queryParams($request)->boolean('go', false); 170 $page = Validator::queryParams($request)->integer('page', 1); 171 $max = Validator::queryParams($request)->integer('max', 20); 172 $folder = Validator::queryParams($request)->string('folder', ''); 173 $filter = Validator::queryParams($request)->string('filter', ''); 174 $subdirs = Validator::queryParams($request)->boolean('subdirs', false); 175 $format = Validator::queryParams($request)->isInArrayKeys($formats)->string('format', ''); 176 177 if ($go) { 178 $media_objects = $this->allMedia($tree, $folder, $subdirs, 'title', $filter, $format); 179 } else { 180 $media_objects = new Collection(); 181 } 182 183 // Pagination 184 $count = $media_objects->count(); 185 $pages = (int) (($count + $max - 1) / $max); 186 $page = max(min($page, $pages), 1); 187 188 $media_objects = $media_objects->slice(($page - 1) * $max, $max); 189 190 return $this->viewResponse('modules/media-list/page', [ 191 'count' => $count, 192 'filter' => $filter, 193 'folder' => $folder, 194 'folders' => $folders, 195 'format' => $format, 196 'formats' => $formats, 197 'linked_record_service' => $this->linked_record_service, 198 'max' => $max, 199 'media_objects' => $media_objects, 200 'page' => $page, 201 'pages' => $pages, 202 'subdirs' => $subdirs, 203 'module' => $this, 204 'title' => I18N::translate('Media'), 205 'tree' => $tree, 206 ]); 207 } 208 209 /** 210 * Generate a list of all the folders in a current tree. 211 * 212 * @param Tree $tree 213 * 214 * @return array<string> 215 */ 216 private function allFolders(Tree $tree): array 217 { 218 $folders = DB::table('media_file') 219 ->where('m_file', '=', $tree->id()) 220 ->where('multimedia_file_refn', 'NOT LIKE', 'http:%') 221 ->where('multimedia_file_refn', 'NOT LIKE', 'https:%') 222 ->where('multimedia_file_refn', 'LIKE', '%/%') 223 ->pluck('multimedia_file_refn', 'multimedia_file_refn') 224 ->map(static fn (string $path): string => dirname($path)) 225 ->uniqueStrict() 226 ->sort() 227 ->all(); 228 229 // Ensure we have an empty (top level) folder. 230 array_unshift($folders, ''); 231 232 return array_combine($folders, $folders); 233 } 234 235 /** 236 * Generate a list of all the media objects matching the criteria in a current tree. 237 * 238 * @param Tree $tree find media in this tree 239 * @param string $folder folder to search 240 * @param bool $subfolders 241 * @param string $sort either "file" or "title" 242 * @param string $filter optional search string 243 * @param string $format option OBJE/FILE/FORM/TYPE 244 * 245 * @return Collection<int,Media> 246 */ 247 private function allMedia(Tree $tree, string $folder, bool $subfolders, string $sort, string $filter, string $format): Collection 248 { 249 $query = DB::table('media') 250 ->join('media_file', static function (JoinClause $join): void { 251 $join 252 ->on('media_file.m_file', '=', 'media.m_file') 253 ->on('media_file.m_id', '=', 'media.m_id'); 254 }) 255 ->where('media.m_file', '=', $tree->id()); 256 257 if ($folder === '') { 258 // Include external URLs in the root folder. 259 if (!$subfolders) { 260 $query->where(static function (Builder $query): void { 261 $query 262 ->where('multimedia_file_refn', 'NOT LIKE', '%/%') 263 ->orWhere('multimedia_file_refn', 'LIKE', 'http:%') 264 ->orWhere('multimedia_file_refn', 'LIKE', 'https:%'); 265 }); 266 } 267 } else { 268 // Exclude external URLs from the root folder. 269 $query 270 ->where('multimedia_file_refn', 'LIKE', $folder . '/%') 271 ->where('multimedia_file_refn', 'NOT LIKE', 'http:%') 272 ->where('multimedia_file_refn', 'NOT LIKE', 'https:%'); 273 274 if (!$subfolders) { 275 $query->where('multimedia_file_refn', 'NOT LIKE', $folder . '/%/%'); 276 } 277 } 278 279 // Apply search terms 280 if ($filter !== '') { 281 $query->where(static function (Builder $query) use ($filter): void { 282 $like = '%' . addcslashes($filter, '\\%_') . '%'; 283 $query 284 ->where('multimedia_file_refn', 'LIKE', $like) 285 ->orWhere('descriptive_title', 'LIKE', $like); 286 }); 287 } 288 289 if ($format !== '') { 290 $query->where('source_media_type', '=', $format); 291 } 292 293 switch ($sort) { 294 case 'file': 295 $query->orderBy('multimedia_file_refn'); 296 break; 297 case 'title': 298 $query->orderBy('descriptive_title'); 299 break; 300 } 301 302 return $query 303 ->get() 304 ->map(Registry::mediaFactory()->mapper($tree)) 305 ->uniqueStrict() 306 ->filter(GedcomRecord::accessFilter()); 307 } 308} 309