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