. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Module; use Fig\Http\Message\RequestMethodInterface; use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\DB; use Fisharebest\Webtrees\GedcomRecord; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Media; use Fisharebest\Webtrees\Registry; use Fisharebest\Webtrees\Services\LinkedRecordService; use Fisharebest\Webtrees\Tree; use Fisharebest\Webtrees\Validator; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use function addcslashes; use function array_combine; use function array_unshift; use function dirname; use function max; use function min; use function redirect; use function route; /** * Class MediaListModule */ class MediaListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface { use ModuleListTrait; protected const ROUTE_URL = '/tree/{tree}/media-list'; private LinkedRecordService $linked_record_service; /** * @param LinkedRecordService $linked_record_service */ public function __construct(LinkedRecordService $linked_record_service) { $this->linked_record_service = $linked_record_service; } /** * Initialization. * * @return void */ public function boot(): void { Registry::routeFactory()->routeMap() ->get(static::class, static::ROUTE_URL, $this) ->allows(RequestMethodInterface::METHOD_POST); } /** * How should this module be identified in the control panel, etc.? * * @return string */ public function title(): string { /* I18N: Name of a module/list */ return I18N::translate('Media objects'); } /** * A sentence describing what this module does. * * @return string */ public function description(): string { /* I18N: Description of the “Media objects” module */ return I18N::translate('A list of media objects.'); } /** * CSS class for the URL. * * @return string */ public function listMenuClass(): string { return 'menu-list-obje'; } /** * @param Tree $tree * @param array|null> $parameters * * @return string */ public function listUrl(Tree $tree, array $parameters = []): string { $parameters['tree'] = $tree->name(); return route(static::class, $parameters); } /** * @return array */ public function listUrlAttributes(): array { return []; } /** * @param Tree $tree * * @return bool */ public function listIsEmpty(Tree $tree): bool { return !DB::table('media') ->where('m_file', '=', $tree->id()) ->exists(); } /** * @param ServerRequestInterface $request * * @return ResponseInterface */ public function handle(ServerRequestInterface $request): ResponseInterface { $tree = Validator::attributes($request)->tree(); $user = Validator::attributes($request)->user(); Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user); $formats = Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE')->values(); // Convert POST requests into GET requests for pretty URLs. if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { $params = [ 'go' => true, 'page' => Validator::parsedBody($request)->integer('page'), 'max' => Validator::parsedBody($request)->integer('max'), 'folder' => Validator::parsedBody($request)->string('folder'), 'filter' => Validator::parsedBody($request)->string('filter'), 'subdirs' => Validator::parsedBody($request)->boolean('subdirs', false), 'format' => Validator::parsedBody($request)->isInArrayKeys($formats)->string('format'), ]; return redirect($this->listUrl($tree, $params)); } $folders = $this->allFolders($tree); $go = Validator::queryParams($request)->boolean('go', false); $page = Validator::queryParams($request)->integer('page', 1); $max = Validator::queryParams($request)->integer('max', 20); $folder = Validator::queryParams($request)->string('folder', ''); $filter = Validator::queryParams($request)->string('filter', ''); $subdirs = Validator::queryParams($request)->boolean('subdirs', false); $format = Validator::queryParams($request)->isInArrayKeys($formats)->string('format', ''); if ($go) { $media_objects = $this->allMedia($tree, $folder, $subdirs, 'title', $filter, $format); } else { $media_objects = new Collection(); } // Pagination $count = $media_objects->count(); $pages = (int) (($count + $max - 1) / $max); $page = max(min($page, $pages), 1); $media_objects = $media_objects->slice(($page - 1) * $max, $max); return $this->viewResponse('modules/media-list/page', [ 'count' => $count, 'filter' => $filter, 'folder' => $folder, 'folders' => $folders, 'format' => $format, 'formats' => $formats, 'linked_record_service' => $this->linked_record_service, 'max' => $max, 'media_objects' => $media_objects, 'page' => $page, 'pages' => $pages, 'subdirs' => $subdirs, 'module' => $this, 'title' => I18N::translate('Media'), 'tree' => $tree, ]); } /** * Generate a list of all the folders in a current tree. * * @param Tree $tree * * @return array */ private function allFolders(Tree $tree): array { $folders = DB::table('media_file') ->where('m_file', '=', $tree->id()) ->where('multimedia_file_refn', 'NOT LIKE', 'http:%') ->where('multimedia_file_refn', 'NOT LIKE', 'https:%') ->where('multimedia_file_refn', 'LIKE', '%/%') ->pluck('multimedia_file_refn', 'multimedia_file_refn') ->map(static fn(string $path): string => dirname($path)) ->uniqueStrict() ->sort() ->all(); // Ensure we have an empty (top level) folder. array_unshift($folders, ''); return array_combine($folders, $folders); } /** * Generate a list of all the media objects matching the criteria in a current tree. * * @param Tree $tree find media in this tree * @param string $folder folder to search * @param bool $subfolders * @param string $sort either "file" or "title" * @param string $filter optional search string * @param string $format option OBJE/FILE/FORM/TYPE * * @return Collection */ private function allMedia(Tree $tree, string $folder, bool $subfolders, string $sort, string $filter, string $format): Collection { $query = DB::table('media') ->join('media_file', static function (JoinClause $join): void { $join ->on('media_file.m_file', '=', 'media.m_file') ->on('media_file.m_id', '=', 'media.m_id'); }) ->where('media.m_file', '=', $tree->id()); if ($folder === '') { // Include external URLs in the root folder. if (!$subfolders) { $query->where(static function (Builder $query): void { $query ->where('multimedia_file_refn', 'NOT LIKE', '%/%') ->orWhere('multimedia_file_refn', 'LIKE', 'http:%') ->orWhere('multimedia_file_refn', 'LIKE', 'https:%'); }); } } else { // Exclude external URLs from the root folder. $query ->where('multimedia_file_refn', 'LIKE', $folder . '/%') ->where('multimedia_file_refn', 'NOT LIKE', 'http:%') ->where('multimedia_file_refn', 'NOT LIKE', 'https:%'); if (!$subfolders) { $query->where('multimedia_file_refn', 'NOT LIKE', $folder . '/%/%'); } } // Apply search terms if ($filter !== '') { $query->where(static function (Builder $query) use ($filter): void { $like = '%' . addcslashes($filter, '\\%_') . '%'; $query ->where('multimedia_file_refn', 'LIKE', $like) ->orWhere('descriptive_title', 'LIKE', $like); }); } if ($format !== '') { $query->where('source_media_type', '=', $format); } switch ($sort) { case 'file': $query->orderBy('multimedia_file_refn'); break; case 'title': $query->orderBy('descriptive_title'); break; } return $query ->get() ->map(Registry::mediaFactory()->mapper($tree)) ->uniqueStrict() ->filter(GedcomRecord::accessFilter()); } }