xref: /webtrees/app/Module/MediaListModule.php (revision 89f7189b61a494347591c99bdb92afb7d8b66e1b)
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\Contracts\UserInterface;
26use Fisharebest\Webtrees\Registry;
27use Fisharebest\Webtrees\GedcomRecord;
28use Fisharebest\Webtrees\GedcomTag;
29use Fisharebest\Webtrees\I18N;
30use Fisharebest\Webtrees\Media;
31use Fisharebest\Webtrees\Tree;
32use Illuminate\Database\Capsule\Manager as DB;
33use Illuminate\Database\Query\Builder;
34use Illuminate\Database\Query\JoinClause;
35use Illuminate\Support\Collection;
36use Psr\Http\Message\ResponseInterface;
37use Psr\Http\Message\ServerRequestInterface;
38use Psr\Http\Server\RequestHandlerInterface;
39
40use function addcslashes;
41use function app;
42use function array_combine;
43use function array_unshift;
44use function assert;
45use function dirname;
46use function max;
47use function min;
48use function redirect;
49use function route;
50
51/**
52 * Class MediaListModule
53 */
54class MediaListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface
55{
56    use ModuleListTrait;
57
58    protected const ROUTE_URL = '/tree/{tree}/media-list';
59
60    /**
61     * Initialization.
62     *
63     * @return void
64     */
65    public function boot(): void
66    {
67        $router_container = app(RouterContainer::class);
68        assert($router_container instanceof RouterContainer);
69
70        $router_container->getMap()
71            ->get(static::class, static::ROUTE_URL, $this)
72            ->allows(RequestMethodInterface::METHOD_POST);
73    }
74
75    /**
76     * How should this module be identified in the control panel, etc.?
77     *
78     * @return string
79     */
80    public function title(): string
81    {
82        /* I18N: Name of a module/list */
83        return I18N::translate('Media objects');
84    }
85
86    /**
87     * A sentence describing what this module does.
88     *
89     * @return string
90     */
91    public function description(): string
92    {
93        /* I18N: Description of the “Media objects” module */
94        return I18N::translate('A list of media objects.');
95    }
96
97    /**
98     * CSS class for the URL.
99     *
100     * @return string
101     */
102    public function listMenuClass(): string
103    {
104        return 'menu-list-obje';
105    }
106
107    /**
108     * @param Tree    $tree
109     * @param mixed[] $parameters
110     *
111     * @return string
112     */
113    public function listUrl(Tree $tree, array $parameters = []): string
114    {
115        $parameters['tree'] = $tree->name();
116
117        return route(static::class, $parameters);
118    }
119
120    /**
121     * @return string[]
122     */
123    public function listUrlAttributes(): array
124    {
125        return [];
126    }
127
128    /**
129     * @param Tree $tree
130     *
131     * @return bool
132     */
133    public function listIsEmpty(Tree $tree): bool
134    {
135        return !DB::table('media')
136            ->where('m_file', '=', $tree->id())
137            ->exists();
138    }
139
140    /**
141     * Handle URLs generated by older versions of webtrees
142     *
143     * @param ServerRequestInterface $request
144     *
145     * @return ResponseInterface
146     */
147    public function getListAction(ServerRequestInterface $request): ResponseInterface
148    {
149        return redirect($this->listUrl($request->getAttribute('tree'), $request->getQueryParams()));
150    }
151
152    /**
153     * @param ServerRequestInterface $request
154     *
155     * @return ResponseInterface
156     */
157    public function handle(ServerRequestInterface $request): ResponseInterface
158    {
159        $tree = $request->getAttribute('tree');
160        assert($tree instanceof Tree);
161
162        $user = $request->getAttribute('user');
163        assert($user instanceof UserInterface);
164
165        $data_filesystem = Registry::filesystem()->data();
166
167        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
168
169        // Convert POST requests into GET requests for pretty URLs.
170        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
171            return redirect($this->listUrl($tree, (array) $request->getParsedBody()));
172        }
173
174        $params  = $request->getQueryParams();
175        $formats = GedcomTag::getFileFormTypes();
176        $go      = $params['go'] ?? '';
177        $page    = (int) ($params['page'] ?? 1);
178        $max     = (int) ($params['max'] ?? 20);
179        $folder  = $params['folder'] ?? '';
180        $filter  = $params['filter'] ?? '';
181        $subdirs = $params['subdirs'] ?? '';
182        $format  = $params['format'] ?? '';
183
184        $folders = $this->allFolders($tree);
185
186        if ($go === '1') {
187            $media_objects = $this->allMedia(
188                $tree,
189                $folder,
190                $subdirs === '1' ? 'include' : 'exclude',
191                'title',
192                $filter,
193                $format
194            );
195        } else {
196            $media_objects = new Collection();
197        }
198
199        // Pagination
200        $count = $media_objects->count();
201        $pages = (int) (($count + $max - 1) / $max);
202        $page  = max(min($page, $pages), 1);
203
204        $media_objects = $media_objects->slice(($page - 1) * $max, $max);
205
206        return $this->viewResponse('modules/media-list/page', [
207            'count'           => $count,
208            'filter'          => $filter,
209            'folder'          => $folder,
210            'folders'         => $folders,
211            'format'          => $format,
212            'formats'         => $formats,
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 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<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