xref: /webtrees/app/Module/MediaListModule.php (revision 748dbe155a6d19d66918ad136947fa23ee8f8469)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2022 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\GedcomRecord;
25use Fisharebest\Webtrees\I18N;
26use Fisharebest\Webtrees\Media;
27use Fisharebest\Webtrees\Registry;
28use Fisharebest\Webtrees\Services\LinkedRecordService;
29use Fisharebest\Webtrees\Tree;
30use Fisharebest\Webtrees\Validator;
31use Illuminate\Database\Capsule\Manager as DB;
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    /**
91     * A sentence describing what this module does.
92     *
93     * @return string
94     */
95    public function description(): string
96    {
97        /* I18N: Description of the “Media objects” module */
98        return I18N::translate('A list of media objects.');
99    }
100
101    /**
102     * CSS class for the URL.
103     *
104     * @return string
105     */
106    public function listMenuClass(): string
107    {
108        return 'menu-list-obje';
109    }
110
111    /**
112     * @param Tree                                      $tree
113     * @param array<bool|int|string|array<string>|null> $parameters
114     *
115     * @return string
116     */
117    public function listUrl(Tree $tree, array $parameters = []): string
118    {
119        $parameters['tree'] = $tree->name();
120
121        return route(static::class, $parameters);
122    }
123
124    /**
125     * @return array<string>
126     */
127    public function listUrlAttributes(): array
128    {
129        return [];
130    }
131
132    /**
133     * @param Tree $tree
134     *
135     * @return bool
136     */
137    public function listIsEmpty(Tree $tree): bool
138    {
139        return !DB::table('media')
140            ->where('m_file', '=', $tree->id())
141            ->exists();
142    }
143
144    /**
145     * @param ServerRequestInterface $request
146     *
147     * @return ResponseInterface
148     */
149    public function handle(ServerRequestInterface $request): ResponseInterface
150    {
151        $tree = Validator::attributes($request)->tree();
152        $user = Validator::attributes($request)->user();
153
154        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
155
156        $data_filesystem = Registry::filesystem()->data();
157
158        // Convert POST requests into GET requests for pretty URLs.
159        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
160            $params = [
161                'go'      => true,
162                'page'    => Validator::parsedBody($request)->integer('page'),
163                'max'     => Validator::parsedBody($request)->integer('max'),
164                'folder'  => Validator::parsedBody($request)->string('folder'),
165                'filter'  => Validator::parsedBody($request)->string('filter'),
166                'subdirs' => Validator::parsedBody($request)->boolean('subdirs', false),
167                'format'  => Validator::parsedBody($request)->string('format'),
168            ];
169
170            return redirect($this->listUrl($tree, $params));
171        }
172
173        $params  = $request->getQueryParams();
174        $formats = Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE')->values();
175        $go      = $params['go'] ?? '';
176        $page    = (int) ($params['page'] ?? 1);
177        $max     = (int) ($params['max'] ?? 20);
178        $folder  = $params['folder'] ?? '';
179        $filter  = $params['filter'] ?? '';
180        $subdirs = $params['subdirs'] ?? '';
181        $format  = $params['format'] ?? '';
182
183        $folders = $this->allFolders($tree);
184
185        if ($go === '1') {
186            $media_objects = $this->allMedia(
187                $tree,
188                $folder,
189                $subdirs === '1' ? 'include' : 'exclude',
190                'title',
191                $filter,
192                $format
193            );
194        } else {
195            $media_objects = new Collection();
196        }
197
198        // Pagination
199        $count = $media_objects->count();
200        $pages = (int) (($count + $max - 1) / $max);
201        $page  = max(min($page, $pages), 1);
202
203        $media_objects = $media_objects->slice(($page - 1) * $max, $max);
204
205        return $this->viewResponse('modules/media-list/page', [
206            'count'                 => $count,
207            'filter'                => $filter,
208            'folder'                => $folder,
209            'folders'               => $folders,
210            'format'                => $format,
211            'formats'               => $formats,
212            'linked_record_service' => $this->linked_record_service,
213            'max'                   => $max,
214            'media_objects'         => $media_objects,
215            'page'                  => $page,
216            'pages'                 => $pages,
217            'subdirs'               => $subdirs,
218            'data_filesystem'       => $data_filesystem,
219            'module'                => $this,
220            'title'                 => I18N::translate('Media'),
221            'tree'                  => $tree,
222        ]);
223    }
224
225    /**
226     * Generate a list of all the folders in a current tree.
227     *
228     * @param Tree $tree
229     *
230     * @return array<string>
231     */
232    private function allFolders(Tree $tree): array
233    {
234        $folders = DB::table('media_file')
235            ->where('m_file', '=', $tree->id())
236            ->where('multimedia_file_refn', 'NOT LIKE', 'http:%')
237            ->where('multimedia_file_refn', 'NOT LIKE', 'https:%')
238            ->where('multimedia_file_refn', 'LIKE', '%/%')
239            ->pluck('multimedia_file_refn', 'multimedia_file_refn')
240            ->map(static function (string $path): string {
241                return dirname($path);
242            })
243            ->uniqueStrict()
244            ->sort()
245            ->all();
246
247        // Ensure we have an empty (top level) folder.
248        array_unshift($folders, '');
249
250        return array_combine($folders, $folders);
251    }
252
253    /**
254     * Generate a list of all the media objects matching the criteria in a current tree.
255     *
256     * @param Tree   $tree       find media in this tree
257     * @param string $folder     folder to search
258     * @param string $subfolders either "include" or "exclude"
259     * @param string $sort       either "file" or "title"
260     * @param string $filter     optional search string
261     * @param string $format     option OBJE/FILE/FORM/TYPE
262     *
263     * @return Collection<int,Media>
264     */
265    private function allMedia(Tree $tree, string $folder, string $subfolders, string $sort, string $filter, string $format): Collection
266    {
267        $query = DB::table('media')
268            ->join('media_file', static function (JoinClause $join): void {
269                $join
270                    ->on('media_file.m_file', '=', 'media.m_file')
271                    ->on('media_file.m_id', '=', 'media.m_id');
272            })
273            ->where('media.m_file', '=', $tree->id());
274
275        if ($folder === '') {
276            // Include external URLs in the root folder.
277            if ($subfolders === 'exclude') {
278                $query->where(static function (Builder $query): void {
279                    $query
280                        ->where('multimedia_file_refn', 'NOT LIKE', '%/%')
281                        ->orWhere('multimedia_file_refn', 'LIKE', 'http:%')
282                        ->orWhere('multimedia_file_refn', 'LIKE', 'https:%');
283                });
284            }
285        } else {
286            // Exclude external URLs from the root folder.
287            $query
288                ->where('multimedia_file_refn', 'LIKE', $folder . '/%')
289                ->where('multimedia_file_refn', 'NOT LIKE', 'http:%')
290                ->where('multimedia_file_refn', 'NOT LIKE', 'https:%');
291
292            if ($subfolders === 'exclude') {
293                $query->where('multimedia_file_refn', 'NOT LIKE', $folder . '/%/%');
294            }
295        }
296
297        // Apply search terms
298        if ($filter !== '') {
299            $query->where(static function (Builder $query) use ($filter): void {
300                $like = '%' . addcslashes($filter, '\\%_') . '%';
301                $query
302                    ->where('multimedia_file_refn', 'LIKE', $like)
303                    ->orWhere('descriptive_title', 'LIKE', $like);
304            });
305        }
306
307        if ($format) {
308            $query->where('source_media_type', '=', $format);
309        }
310
311        switch ($sort) {
312            case 'file':
313                $query->orderBy('multimedia_file_refn');
314                break;
315            case 'title':
316                $query->orderBy('descriptive_title');
317                break;
318        }
319
320        return $query
321            ->get()
322            ->map(Registry::mediaFactory()->mapper($tree))
323            ->uniqueStrict()
324            ->filter(GedcomRecord::accessFilter());
325    }
326}
327