xref: /webtrees/app/Module/MediaListModule.php (revision e873f434551745f888937263ff89e80db3b0f785)
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 string 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