xref: /webtrees/app/Module/MediaListModule.php (revision 7413816e6dd2d50e569034fb804f3dce7471bb94)
167992b6aSRichard Cissee<?php
23976b470SGreg Roach
367992b6aSRichard Cissee/**
467992b6aSRichard Cissee * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
667992b6aSRichard Cissee * This program is free software: you can redistribute it and/or modify
767992b6aSRichard Cissee * it under the terms of the GNU General Public License as published by
867992b6aSRichard Cissee * the Free Software Foundation, either version 3 of the License, or
967992b6aSRichard Cissee * (at your option) any later version.
1067992b6aSRichard Cissee * This program is distributed in the hope that it will be useful,
1167992b6aSRichard Cissee * but WITHOUT ANY WARRANTY; without even the implied warranty of
1267992b6aSRichard Cissee * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1367992b6aSRichard Cissee * GNU General Public License for more details.
1467992b6aSRichard Cissee * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
1667992b6aSRichard Cissee */
17fcfa147eSGreg Roach
1867992b6aSRichard Cisseedeclare(strict_types=1);
1967992b6aSRichard Cissee
2067992b6aSRichard Cisseenamespace Fisharebest\Webtrees\Module;
2167992b6aSRichard Cissee
2206a438b4SGreg Roachuse Fig\Http\Message\RequestMethodInterface;
2367992b6aSRichard Cisseeuse Fisharebest\Webtrees\Auth;
246f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB;
2506a438b4SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
2606a438b4SGreg Roachuse Fisharebest\Webtrees\I18N;
2706a438b4SGreg Roachuse Fisharebest\Webtrees\Media;
284991f205SGreg Roachuse Fisharebest\Webtrees\Registry;
294991f205SGreg Roachuse Fisharebest\Webtrees\Services\LinkedRecordService;
3006a438b4SGreg Roachuse Fisharebest\Webtrees\Tree;
31b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
3206a438b4SGreg Roachuse Illuminate\Database\Query\Builder;
3306a438b4SGreg Roachuse Illuminate\Database\Query\JoinClause;
3406a438b4SGreg Roachuse Illuminate\Support\Collection;
356ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
366ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
3706a438b4SGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
38f3874e19SGreg Roach
3906a438b4SGreg Roachuse function addcslashes;
4006a438b4SGreg Roachuse function array_combine;
4106a438b4SGreg Roachuse function array_unshift;
4206a438b4SGreg Roachuse function dirname;
4306a438b4SGreg Roachuse function max;
4406a438b4SGreg Roachuse function min;
454fbf5c97SGreg Roachuse function redirect;
464fbf5c97SGreg Roachuse function route;
4767992b6aSRichard Cissee
4867992b6aSRichard Cissee/**
4967992b6aSRichard Cissee * Class MediaListModule
5067992b6aSRichard Cissee */
5106a438b4SGreg Roachclass MediaListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface
5267992b6aSRichard Cissee{
5367992b6aSRichard Cissee    use ModuleListTrait;
5467992b6aSRichard Cissee
5506a438b4SGreg Roach    protected const ROUTE_URL = '/tree/{tree}/media-list';
5606a438b4SGreg Roach
574991f205SGreg Roach    private LinkedRecordService $linked_record_service;
584991f205SGreg Roach
594991f205SGreg Roach    /**
604991f205SGreg Roach     * @param LinkedRecordService $linked_record_service
614991f205SGreg Roach     */
624991f205SGreg Roach    public function __construct(LinkedRecordService $linked_record_service)
634991f205SGreg Roach    {
644991f205SGreg Roach        $this->linked_record_service = $linked_record_service;
654991f205SGreg Roach    }
664991f205SGreg Roach
6706a438b4SGreg Roach    /**
6806a438b4SGreg Roach     * Initialization.
6906a438b4SGreg Roach     *
7006a438b4SGreg Roach     * @return void
7106a438b4SGreg Roach     */
7206a438b4SGreg Roach    public function boot(): void
7306a438b4SGreg Roach    {
74158900c2SGreg Roach        Registry::routeFactory()->routeMap()
7506a438b4SGreg Roach            ->get(static::class, static::ROUTE_URL, $this)
7606a438b4SGreg Roach            ->allows(RequestMethodInterface::METHOD_POST);
7706a438b4SGreg Roach    }
7806a438b4SGreg Roach
7967992b6aSRichard Cissee    /**
800cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
8167992b6aSRichard Cissee     *
8267992b6aSRichard Cissee     * @return string
8367992b6aSRichard Cissee     */
8467992b6aSRichard Cissee    public function title(): string
8567992b6aSRichard Cissee    {
8667992b6aSRichard Cissee        /* I18N: Name of a module/list */
8767992b6aSRichard Cissee        return I18N::translate('Media objects');
8867992b6aSRichard Cissee    }
8967992b6aSRichard Cissee
9067992b6aSRichard Cissee    public function description(): string
9167992b6aSRichard Cissee    {
92b5e8e56bSGreg Roach        /* I18N: Description of the “Media objects” module */
9367992b6aSRichard Cissee        return I18N::translate('A list of media objects.');
9467992b6aSRichard Cissee    }
9567992b6aSRichard Cissee
9667992b6aSRichard Cissee    /**
9767992b6aSRichard Cissee     * CSS class for the URL.
9867992b6aSRichard Cissee     *
9967992b6aSRichard Cissee     * @return string
10067992b6aSRichard Cissee     */
10167992b6aSRichard Cissee    public function listMenuClass(): string
10267992b6aSRichard Cissee    {
10367992b6aSRichard Cissee        return 'menu-list-obje';
10467992b6aSRichard Cissee    }
10567992b6aSRichard Cissee
1064db4b4a9SGreg Roach    /**
10706a438b4SGreg Roach     * @param Tree                                      $tree
10876d39c55SGreg Roach     * @param array<bool|int|string|array<string>|null> $parameters
1094db4b4a9SGreg Roach     *
11006a438b4SGreg Roach     * @return string
1114db4b4a9SGreg Roach     */
11206a438b4SGreg Roach    public function listUrl(Tree $tree, array $parameters = []): string
11367992b6aSRichard Cissee    {
11406a438b4SGreg Roach        $parameters['tree'] = $tree->name();
1155229eadeSGreg Roach
11606a438b4SGreg Roach        return route(static::class, $parameters);
1174fbf5c97SGreg Roach    }
1184fbf5c97SGreg Roach
1194fbf5c97SGreg Roach    /**
12024f2a3afSGreg Roach     * @return array<string>
1214db4b4a9SGreg Roach     */
12267992b6aSRichard Cissee    public function listUrlAttributes(): array
12367992b6aSRichard Cissee    {
12467992b6aSRichard Cissee        return [];
12567992b6aSRichard Cissee    }
12667992b6aSRichard Cissee
1274db4b4a9SGreg Roach    /**
1284db4b4a9SGreg Roach     * @param Tree $tree
1294db4b4a9SGreg Roach     *
1304db4b4a9SGreg Roach     * @return bool
1314db4b4a9SGreg Roach     */
13267992b6aSRichard Cissee    public function listIsEmpty(Tree $tree): bool
13367992b6aSRichard Cissee    {
13467992b6aSRichard Cissee        return !DB::table('media')
13567992b6aSRichard Cissee            ->where('m_file', '=', $tree->id())
13667992b6aSRichard Cissee            ->exists();
13767992b6aSRichard Cissee    }
13806a438b4SGreg Roach
13906a438b4SGreg Roach    /**
14006a438b4SGreg Roach     * @param ServerRequestInterface $request
14106a438b4SGreg Roach     *
14206a438b4SGreg Roach     * @return ResponseInterface
14306a438b4SGreg Roach     */
14406a438b4SGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
14506a438b4SGreg Roach    {
146b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->tree();
147b55cbc6bSGreg Roach        $user = Validator::attributes($request)->user();
14806a438b4SGreg Roach
14906a438b4SGreg Roach        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
15006a438b4SGreg Roach
1517cda2f2fSGreg Roach        $formats = Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE')->values();
152748dbe15SGreg Roach
15306a438b4SGreg Roach        // Convert POST requests into GET requests for pretty URLs.
15406a438b4SGreg Roach        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
155748dbe15SGreg Roach            $params = [
156748dbe15SGreg Roach                'go'      => true,
157748dbe15SGreg Roach                'page'    => Validator::parsedBody($request)->integer('page'),
158748dbe15SGreg Roach                'max'     => Validator::parsedBody($request)->integer('max'),
159748dbe15SGreg Roach                'folder'  => Validator::parsedBody($request)->string('folder'),
160748dbe15SGreg Roach                'filter'  => Validator::parsedBody($request)->string('filter'),
161748dbe15SGreg Roach                'subdirs' => Validator::parsedBody($request)->boolean('subdirs', false),
1627cda2f2fSGreg Roach                'format'  => Validator::parsedBody($request)->isInArrayKeys($formats)->string('format'),
163748dbe15SGreg Roach            ];
164748dbe15SGreg Roach
165748dbe15SGreg Roach            return redirect($this->listUrl($tree, $params));
16606a438b4SGreg Roach        }
16706a438b4SGreg Roach
16806a438b4SGreg Roach        $folders = $this->allFolders($tree);
1697cda2f2fSGreg Roach        $go      = Validator::queryParams($request)->boolean('go', false);
1707cda2f2fSGreg Roach        $page    = Validator::queryParams($request)->integer('page', 1);
171e6f6dba5SGreg Roach        $max     = Validator::queryParams($request)->integer('max', 20);
1727cda2f2fSGreg Roach        $folder  = Validator::queryParams($request)->string('folder', '');
1737cda2f2fSGreg Roach        $filter  = Validator::queryParams($request)->string('filter', '');
1747cda2f2fSGreg Roach        $subdirs = Validator::queryParams($request)->boolean('subdirs', false);
1757cda2f2fSGreg Roach        $format  = Validator::queryParams($request)->isInArrayKeys($formats)->string('format', '');
1767cda2f2fSGreg Roach
1777cda2f2fSGreg Roach        if ($go) {
1787cda2f2fSGreg Roach            $media_objects = $this->allMedia($tree, $folder, $subdirs, 'title', $filter, $format);
17906a438b4SGreg Roach        } else {
18006a438b4SGreg Roach            $media_objects = new Collection();
18106a438b4SGreg Roach        }
18206a438b4SGreg Roach
18306a438b4SGreg Roach        // Pagination
18406a438b4SGreg Roach        $count = $media_objects->count();
18506a438b4SGreg Roach        $pages = (int) (($count + $max - 1) / $max);
18606a438b4SGreg Roach        $page  = max(min($page, $pages), 1);
18706a438b4SGreg Roach
18806a438b4SGreg Roach        $media_objects = $media_objects->slice(($page - 1) * $max, $max);
18906a438b4SGreg Roach
19006a438b4SGreg Roach        return $this->viewResponse('modules/media-list/page', [
19106a438b4SGreg Roach            'count'                 => $count,
19206a438b4SGreg Roach            'filter'                => $filter,
19306a438b4SGreg Roach            'folder'                => $folder,
19406a438b4SGreg Roach            'folders'               => $folders,
19506a438b4SGreg Roach            'format'                => $format,
19606a438b4SGreg Roach            'formats'               => $formats,
1974991f205SGreg Roach            'linked_record_service' => $this->linked_record_service,
19806a438b4SGreg Roach            'max'                   => $max,
19906a438b4SGreg Roach            'media_objects'         => $media_objects,
20006a438b4SGreg Roach            'page'                  => $page,
20106a438b4SGreg Roach            'pages'                 => $pages,
20206a438b4SGreg Roach            'subdirs'               => $subdirs,
20306a438b4SGreg Roach            'module'                => $this,
20406a438b4SGreg Roach            'title'                 => I18N::translate('Media'),
20506a438b4SGreg Roach            'tree'                  => $tree,
20606a438b4SGreg Roach        ]);
20706a438b4SGreg Roach    }
20806a438b4SGreg Roach
20906a438b4SGreg Roach    /**
21006a438b4SGreg Roach     * Generate a list of all the folders in a current tree.
21106a438b4SGreg Roach     *
21206a438b4SGreg Roach     * @param Tree $tree
21306a438b4SGreg Roach     *
21424f2a3afSGreg Roach     * @return array<string>
21506a438b4SGreg Roach     */
21606a438b4SGreg Roach    private function allFolders(Tree $tree): array
21706a438b4SGreg Roach    {
21806a438b4SGreg Roach        $folders = DB::table('media_file')
21906a438b4SGreg Roach            ->where('m_file', '=', $tree->id())
22006a438b4SGreg Roach            ->where('multimedia_file_refn', 'NOT LIKE', 'http:%')
22106a438b4SGreg Roach            ->where('multimedia_file_refn', 'NOT LIKE', 'https:%')
22206a438b4SGreg Roach            ->where('multimedia_file_refn', 'LIKE', '%/%')
22306a438b4SGreg Roach            ->pluck('multimedia_file_refn', 'multimedia_file_refn')
224*f25fc0f9SGreg Roach            ->map(static fn (string $path): string => dirname($path))
22506a438b4SGreg Roach            ->uniqueStrict()
22606a438b4SGreg Roach            ->sort()
22706a438b4SGreg Roach            ->all();
22806a438b4SGreg Roach
22906a438b4SGreg Roach        // Ensure we have an empty (top level) folder.
23006a438b4SGreg Roach        array_unshift($folders, '');
23106a438b4SGreg Roach
23206a438b4SGreg Roach        return array_combine($folders, $folders);
23306a438b4SGreg Roach    }
23406a438b4SGreg Roach
23506a438b4SGreg Roach    /**
23606a438b4SGreg Roach     * Generate a list of all the media objects matching the criteria in a current tree.
23706a438b4SGreg Roach     *
23806a438b4SGreg Roach     * @param Tree   $tree       find media in this tree
23906a438b4SGreg Roach     * @param string $folder     folder to search
2407cda2f2fSGreg Roach     * @param bool   $subfolders
24106a438b4SGreg Roach     * @param string $sort       either "file" or "title"
24206a438b4SGreg Roach     * @param string $filter     optional search string
24306a438b4SGreg Roach     * @param string $format     option OBJE/FILE/FORM/TYPE
24406a438b4SGreg Roach     *
24536779af1SGreg Roach     * @return Collection<int,Media>
24606a438b4SGreg Roach     */
2477cda2f2fSGreg Roach    private function allMedia(Tree $tree, string $folder, bool $subfolders, string $sort, string $filter, string $format): Collection
24806a438b4SGreg Roach    {
24906a438b4SGreg Roach        $query = DB::table('media')
25006a438b4SGreg Roach            ->join('media_file', static function (JoinClause $join): void {
25106a438b4SGreg Roach                $join
25206a438b4SGreg Roach                    ->on('media_file.m_file', '=', 'media.m_file')
25306a438b4SGreg Roach                    ->on('media_file.m_id', '=', 'media.m_id');
25406a438b4SGreg Roach            })
25506a438b4SGreg Roach            ->where('media.m_file', '=', $tree->id());
25606a438b4SGreg Roach
25706a438b4SGreg Roach        if ($folder === '') {
25806a438b4SGreg Roach            // Include external URLs in the root folder.
2597cda2f2fSGreg Roach            if (!$subfolders) {
26006a438b4SGreg Roach                $query->where(static function (Builder $query): void {
26106a438b4SGreg Roach                    $query
26206a438b4SGreg Roach                        ->where('multimedia_file_refn', 'NOT LIKE', '%/%')
26306a438b4SGreg Roach                        ->orWhere('multimedia_file_refn', 'LIKE', 'http:%')
26406a438b4SGreg Roach                        ->orWhere('multimedia_file_refn', 'LIKE', 'https:%');
26506a438b4SGreg Roach                });
26606a438b4SGreg Roach            }
26706a438b4SGreg Roach        } else {
26806a438b4SGreg Roach            // Exclude external URLs from the root folder.
26906a438b4SGreg Roach            $query
27006a438b4SGreg Roach                ->where('multimedia_file_refn', 'LIKE', $folder . '/%')
27106a438b4SGreg Roach                ->where('multimedia_file_refn', 'NOT LIKE', 'http:%')
27206a438b4SGreg Roach                ->where('multimedia_file_refn', 'NOT LIKE', 'https:%');
27306a438b4SGreg Roach
2747cda2f2fSGreg Roach            if (!$subfolders) {
27506a438b4SGreg Roach                $query->where('multimedia_file_refn', 'NOT LIKE', $folder . '/%/%');
27606a438b4SGreg Roach            }
27706a438b4SGreg Roach        }
27806a438b4SGreg Roach
27906a438b4SGreg Roach        // Apply search terms
28006a438b4SGreg Roach        if ($filter !== '') {
28106a438b4SGreg Roach            $query->where(static function (Builder $query) use ($filter): void {
28206a438b4SGreg Roach                $like = '%' . addcslashes($filter, '\\%_') . '%';
28306a438b4SGreg Roach                $query
28406a438b4SGreg Roach                    ->where('multimedia_file_refn', 'LIKE', $like)
28506a438b4SGreg Roach                    ->orWhere('descriptive_title', 'LIKE', $like);
28606a438b4SGreg Roach            });
28706a438b4SGreg Roach        }
28806a438b4SGreg Roach
289b6ec1ccfSGreg Roach        if ($format !== '') {
29006a438b4SGreg Roach            $query->where('source_media_type', '=', $format);
29106a438b4SGreg Roach        }
29206a438b4SGreg Roach
29306a438b4SGreg Roach        switch ($sort) {
29406a438b4SGreg Roach            case 'file':
29506a438b4SGreg Roach                $query->orderBy('multimedia_file_refn');
29606a438b4SGreg Roach                break;
29706a438b4SGreg Roach            case 'title':
29806a438b4SGreg Roach                $query->orderBy('descriptive_title');
29906a438b4SGreg Roach                break;
30006a438b4SGreg Roach        }
30106a438b4SGreg Roach
30206a438b4SGreg Roach        return $query
30306a438b4SGreg Roach            ->get()
3046b9cb339SGreg Roach            ->map(Registry::mediaFactory()->mapper($tree))
30506a438b4SGreg Roach            ->uniqueStrict()
30606a438b4SGreg Roach            ->filter(GedcomRecord::accessFilter());
30706a438b4SGreg Roach    }
30867992b6aSRichard Cissee}
309