xref: /webtrees/app/Module/MediaListModule.php (revision d11be7027e34e3121be11cc025421873364403f9)
167992b6aSRichard Cissee<?php
23976b470SGreg Roach
367992b6aSRichard Cissee/**
467992b6aSRichard Cissee * webtrees: online genealogy
5*d11be702SGreg 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;
2406a438b4SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
2506a438b4SGreg Roachuse Fisharebest\Webtrees\I18N;
2606a438b4SGreg Roachuse Fisharebest\Webtrees\Media;
274991f205SGreg Roachuse Fisharebest\Webtrees\Registry;
284991f205SGreg Roachuse Fisharebest\Webtrees\Services\LinkedRecordService;
2906a438b4SGreg Roachuse Fisharebest\Webtrees\Tree;
30b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
3167992b6aSRichard Cisseeuse Illuminate\Database\Capsule\Manager as DB;
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    /**
9167992b6aSRichard Cissee     * A sentence describing what this module does.
9267992b6aSRichard Cissee     *
9367992b6aSRichard Cissee     * @return string
9467992b6aSRichard Cissee     */
9567992b6aSRichard Cissee    public function description(): string
9667992b6aSRichard Cissee    {
97b5e8e56bSGreg Roach        /* I18N: Description of the “Media objects” module */
9867992b6aSRichard Cissee        return I18N::translate('A list of media objects.');
9967992b6aSRichard Cissee    }
10067992b6aSRichard Cissee
10167992b6aSRichard Cissee    /**
10267992b6aSRichard Cissee     * CSS class for the URL.
10367992b6aSRichard Cissee     *
10467992b6aSRichard Cissee     * @return string
10567992b6aSRichard Cissee     */
10667992b6aSRichard Cissee    public function listMenuClass(): string
10767992b6aSRichard Cissee    {
10867992b6aSRichard Cissee        return 'menu-list-obje';
10967992b6aSRichard Cissee    }
11067992b6aSRichard Cissee
1114db4b4a9SGreg Roach    /**
11206a438b4SGreg Roach     * @param Tree                                      $tree
11376d39c55SGreg Roach     * @param array<bool|int|string|array<string>|null> $parameters
1144db4b4a9SGreg Roach     *
11506a438b4SGreg Roach     * @return string
1164db4b4a9SGreg Roach     */
11706a438b4SGreg Roach    public function listUrl(Tree $tree, array $parameters = []): string
11867992b6aSRichard Cissee    {
11906a438b4SGreg Roach        $parameters['tree'] = $tree->name();
1205229eadeSGreg Roach
12106a438b4SGreg Roach        return route(static::class, $parameters);
1224fbf5c97SGreg Roach    }
1234fbf5c97SGreg Roach
1244fbf5c97SGreg Roach    /**
12524f2a3afSGreg Roach     * @return array<string>
1264db4b4a9SGreg Roach     */
12767992b6aSRichard Cissee    public function listUrlAttributes(): array
12867992b6aSRichard Cissee    {
12967992b6aSRichard Cissee        return [];
13067992b6aSRichard Cissee    }
13167992b6aSRichard Cissee
1324db4b4a9SGreg Roach    /**
1334db4b4a9SGreg Roach     * @param Tree $tree
1344db4b4a9SGreg Roach     *
1354db4b4a9SGreg Roach     * @return bool
1364db4b4a9SGreg Roach     */
13767992b6aSRichard Cissee    public function listIsEmpty(Tree $tree): bool
13867992b6aSRichard Cissee    {
13967992b6aSRichard Cissee        return !DB::table('media')
14067992b6aSRichard Cissee            ->where('m_file', '=', $tree->id())
14167992b6aSRichard Cissee            ->exists();
14267992b6aSRichard Cissee    }
14306a438b4SGreg Roach
14406a438b4SGreg Roach    /**
14506a438b4SGreg Roach     * @param ServerRequestInterface $request
14606a438b4SGreg Roach     *
14706a438b4SGreg Roach     * @return ResponseInterface
14806a438b4SGreg Roach     */
14906a438b4SGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
15006a438b4SGreg Roach    {
151b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->tree();
152b55cbc6bSGreg Roach        $user = Validator::attributes($request)->user();
15306a438b4SGreg Roach
15406a438b4SGreg Roach        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
15506a438b4SGreg Roach
1567cda2f2fSGreg Roach        $formats = Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE')->values();
157748dbe15SGreg Roach
15806a438b4SGreg Roach        // Convert POST requests into GET requests for pretty URLs.
15906a438b4SGreg Roach        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
160748dbe15SGreg Roach            $params = [
161748dbe15SGreg Roach                'go'      => true,
162748dbe15SGreg Roach                'page'    => Validator::parsedBody($request)->integer('page'),
163748dbe15SGreg Roach                'max'     => Validator::parsedBody($request)->integer('max'),
164748dbe15SGreg Roach                'folder'  => Validator::parsedBody($request)->string('folder'),
165748dbe15SGreg Roach                'filter'  => Validator::parsedBody($request)->string('filter'),
166748dbe15SGreg Roach                'subdirs' => Validator::parsedBody($request)->boolean('subdirs', false),
1677cda2f2fSGreg Roach                'format'  => Validator::parsedBody($request)->isInArrayKeys($formats)->string('format'),
168748dbe15SGreg Roach            ];
169748dbe15SGreg Roach
170748dbe15SGreg Roach            return redirect($this->listUrl($tree, $params));
17106a438b4SGreg Roach        }
17206a438b4SGreg Roach
17306a438b4SGreg Roach        $folders = $this->allFolders($tree);
1747cda2f2fSGreg Roach        $go      = Validator::queryParams($request)->boolean('go', false);
1757cda2f2fSGreg Roach        $page    = Validator::queryParams($request)->integer('page', 1);
176e6f6dba5SGreg Roach        $max     = Validator::queryParams($request)->integer('max', 20);
1777cda2f2fSGreg Roach        $folder  = Validator::queryParams($request)->string('folder', '');
1787cda2f2fSGreg Roach        $filter  = Validator::queryParams($request)->string('filter', '');
1797cda2f2fSGreg Roach        $subdirs = Validator::queryParams($request)->boolean('subdirs', false);
1807cda2f2fSGreg Roach        $format  = Validator::queryParams($request)->isInArrayKeys($formats)->string('format', '');
1817cda2f2fSGreg Roach
1827cda2f2fSGreg Roach        if ($go) {
1837cda2f2fSGreg Roach            $media_objects = $this->allMedia($tree, $folder, $subdirs, 'title', $filter, $format);
18406a438b4SGreg Roach        } else {
18506a438b4SGreg Roach            $media_objects = new Collection();
18606a438b4SGreg Roach        }
18706a438b4SGreg Roach
18806a438b4SGreg Roach        // Pagination
18906a438b4SGreg Roach        $count = $media_objects->count();
19006a438b4SGreg Roach        $pages = (int) (($count + $max - 1) / $max);
19106a438b4SGreg Roach        $page  = max(min($page, $pages), 1);
19206a438b4SGreg Roach
19306a438b4SGreg Roach        $media_objects = $media_objects->slice(($page - 1) * $max, $max);
19406a438b4SGreg Roach
19506a438b4SGreg Roach        return $this->viewResponse('modules/media-list/page', [
19606a438b4SGreg Roach            'count'                 => $count,
19706a438b4SGreg Roach            'filter'                => $filter,
19806a438b4SGreg Roach            'folder'                => $folder,
19906a438b4SGreg Roach            'folders'               => $folders,
20006a438b4SGreg Roach            'format'                => $format,
20106a438b4SGreg Roach            'formats'               => $formats,
2024991f205SGreg Roach            'linked_record_service' => $this->linked_record_service,
20306a438b4SGreg Roach            'max'                   => $max,
20406a438b4SGreg Roach            'media_objects'         => $media_objects,
20506a438b4SGreg Roach            'page'                  => $page,
20606a438b4SGreg Roach            'pages'                 => $pages,
20706a438b4SGreg Roach            'subdirs'               => $subdirs,
20806a438b4SGreg Roach            'module'                => $this,
20906a438b4SGreg Roach            'title'                 => I18N::translate('Media'),
21006a438b4SGreg Roach            'tree'                  => $tree,
21106a438b4SGreg Roach        ]);
21206a438b4SGreg Roach    }
21306a438b4SGreg Roach
21406a438b4SGreg Roach    /**
21506a438b4SGreg Roach     * Generate a list of all the folders in a current tree.
21606a438b4SGreg Roach     *
21706a438b4SGreg Roach     * @param Tree $tree
21806a438b4SGreg Roach     *
21924f2a3afSGreg Roach     * @return array<string>
22006a438b4SGreg Roach     */
22106a438b4SGreg Roach    private function allFolders(Tree $tree): array
22206a438b4SGreg Roach    {
22306a438b4SGreg Roach        $folders = DB::table('media_file')
22406a438b4SGreg Roach            ->where('m_file', '=', $tree->id())
22506a438b4SGreg Roach            ->where('multimedia_file_refn', 'NOT LIKE', 'http:%')
22606a438b4SGreg Roach            ->where('multimedia_file_refn', 'NOT LIKE', 'https:%')
22706a438b4SGreg Roach            ->where('multimedia_file_refn', 'LIKE', '%/%')
22806a438b4SGreg Roach            ->pluck('multimedia_file_refn', 'multimedia_file_refn')
22906a438b4SGreg Roach            ->map(static function (string $path): string {
23006a438b4SGreg Roach                return dirname($path);
23106a438b4SGreg Roach            })
23206a438b4SGreg Roach            ->uniqueStrict()
23306a438b4SGreg Roach            ->sort()
23406a438b4SGreg Roach            ->all();
23506a438b4SGreg Roach
23606a438b4SGreg Roach        // Ensure we have an empty (top level) folder.
23706a438b4SGreg Roach        array_unshift($folders, '');
23806a438b4SGreg Roach
23906a438b4SGreg Roach        return array_combine($folders, $folders);
24006a438b4SGreg Roach    }
24106a438b4SGreg Roach
24206a438b4SGreg Roach    /**
24306a438b4SGreg Roach     * Generate a list of all the media objects matching the criteria in a current tree.
24406a438b4SGreg Roach     *
24506a438b4SGreg Roach     * @param Tree   $tree       find media in this tree
24606a438b4SGreg Roach     * @param string $folder     folder to search
2477cda2f2fSGreg Roach     * @param bool   $subfolders
24806a438b4SGreg Roach     * @param string $sort       either "file" or "title"
24906a438b4SGreg Roach     * @param string $filter     optional search string
25006a438b4SGreg Roach     * @param string $format     option OBJE/FILE/FORM/TYPE
25106a438b4SGreg Roach     *
25236779af1SGreg Roach     * @return Collection<int,Media>
25306a438b4SGreg Roach     */
2547cda2f2fSGreg Roach    private function allMedia(Tree $tree, string $folder, bool $subfolders, string $sort, string $filter, string $format): Collection
25506a438b4SGreg Roach    {
25606a438b4SGreg Roach        $query = DB::table('media')
25706a438b4SGreg Roach            ->join('media_file', static function (JoinClause $join): void {
25806a438b4SGreg Roach                $join
25906a438b4SGreg Roach                    ->on('media_file.m_file', '=', 'media.m_file')
26006a438b4SGreg Roach                    ->on('media_file.m_id', '=', 'media.m_id');
26106a438b4SGreg Roach            })
26206a438b4SGreg Roach            ->where('media.m_file', '=', $tree->id());
26306a438b4SGreg Roach
26406a438b4SGreg Roach        if ($folder === '') {
26506a438b4SGreg Roach            // Include external URLs in the root folder.
2667cda2f2fSGreg Roach            if (!$subfolders) {
26706a438b4SGreg Roach                $query->where(static function (Builder $query): void {
26806a438b4SGreg Roach                    $query
26906a438b4SGreg Roach                        ->where('multimedia_file_refn', 'NOT LIKE', '%/%')
27006a438b4SGreg Roach                        ->orWhere('multimedia_file_refn', 'LIKE', 'http:%')
27106a438b4SGreg Roach                        ->orWhere('multimedia_file_refn', 'LIKE', 'https:%');
27206a438b4SGreg Roach                });
27306a438b4SGreg Roach            }
27406a438b4SGreg Roach        } else {
27506a438b4SGreg Roach            // Exclude external URLs from the root folder.
27606a438b4SGreg Roach            $query
27706a438b4SGreg Roach                ->where('multimedia_file_refn', 'LIKE', $folder . '/%')
27806a438b4SGreg Roach                ->where('multimedia_file_refn', 'NOT LIKE', 'http:%')
27906a438b4SGreg Roach                ->where('multimedia_file_refn', 'NOT LIKE', 'https:%');
28006a438b4SGreg Roach
2817cda2f2fSGreg Roach            if (!$subfolders) {
28206a438b4SGreg Roach                $query->where('multimedia_file_refn', 'NOT LIKE', $folder . '/%/%');
28306a438b4SGreg Roach            }
28406a438b4SGreg Roach        }
28506a438b4SGreg Roach
28606a438b4SGreg Roach        // Apply search terms
28706a438b4SGreg Roach        if ($filter !== '') {
28806a438b4SGreg Roach            $query->where(static function (Builder $query) use ($filter): void {
28906a438b4SGreg Roach                $like = '%' . addcslashes($filter, '\\%_') . '%';
29006a438b4SGreg Roach                $query
29106a438b4SGreg Roach                    ->where('multimedia_file_refn', 'LIKE', $like)
29206a438b4SGreg Roach                    ->orWhere('descriptive_title', 'LIKE', $like);
29306a438b4SGreg Roach            });
29406a438b4SGreg Roach        }
29506a438b4SGreg Roach
29606a438b4SGreg Roach        if ($format) {
29706a438b4SGreg Roach            $query->where('source_media_type', '=', $format);
29806a438b4SGreg Roach        }
29906a438b4SGreg Roach
30006a438b4SGreg Roach        switch ($sort) {
30106a438b4SGreg Roach            case 'file':
30206a438b4SGreg Roach                $query->orderBy('multimedia_file_refn');
30306a438b4SGreg Roach                break;
30406a438b4SGreg Roach            case 'title':
30506a438b4SGreg Roach                $query->orderBy('descriptive_title');
30606a438b4SGreg Roach                break;
30706a438b4SGreg Roach        }
30806a438b4SGreg Roach
30906a438b4SGreg Roach        return $query
31006a438b4SGreg Roach            ->get()
3116b9cb339SGreg Roach            ->map(Registry::mediaFactory()->mapper($tree))
31206a438b4SGreg Roach            ->uniqueStrict()
31306a438b4SGreg Roach            ->filter(GedcomRecord::accessFilter());
31406a438b4SGreg Roach    }
31567992b6aSRichard Cissee}
316