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