xref: /webtrees/app/Module/MediaListModule.php (revision 1a2589c705c5f3df3b73b8df4b5a0f19e787de5b)
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        $router_container = app(RouterContainer::class);
67        assert($router_container instanceof RouterContainer);
68
69        $router_container->getMap()
70            ->get(static::class, static::ROUTE_URL, $this)
71            ->allows(RequestMethodInterface::METHOD_POST);
72    }
73
74    /**
75     * How should this module be identified in the control panel, etc.?
76     *
77     * @return string
78     */
79    public function title(): string
80    {
81        /* I18N: Name of a module/list */
82        return I18N::translate('Media objects');
83    }
84
85    /**
86     * A sentence describing what this module does.
87     *
88     * @return string
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     * Handle URLs generated by older versions of webtrees
141     *
142     * @param ServerRequestInterface $request
143     *
144     * @return ResponseInterface
145     */
146    public function getListAction(ServerRequestInterface $request): ResponseInterface
147    {
148        $tree = Validator::attributes($request)->tree();
149
150        return redirect($this->listUrl($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 = Validator::attributes($request)->tree();
161        $user = Validator::attributes($request)->user();
162
163        $data_filesystem = Registry::filesystem()->data();
164
165        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
166
167        // Convert POST requests into GET requests for pretty URLs.
168        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
169            return redirect($this->listUrl($tree, (array) $request->getParsedBody()));
170        }
171
172        $params  = $request->getQueryParams();
173        $formats = Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE')->values();
174        $go      = $params['go'] ?? '';
175        $page    = (int) ($params['page'] ?? 1);
176        $max     = (int) ($params['max'] ?? 20);
177        $folder  = $params['folder'] ?? '';
178        $filter  = $params['filter'] ?? '';
179        $subdirs = $params['subdirs'] ?? '';
180        $format  = $params['format'] ?? '';
181
182        $folders = $this->allFolders($tree);
183
184        if ($go === '1') {
185            $media_objects = $this->allMedia(
186                $tree,
187                $folder,
188                $subdirs === '1' ? 'include' : 'exclude',
189                'title',
190                $filter,
191                $format
192            );
193        } else {
194            $media_objects = new Collection();
195        }
196
197        // Pagination
198        $count = $media_objects->count();
199        $pages = (int) (($count + $max - 1) / $max);
200        $page  = max(min($page, $pages), 1);
201
202        $media_objects = $media_objects->slice(($page - 1) * $max, $max);
203
204        return $this->viewResponse('modules/media-list/page', [
205            'count'           => $count,
206            'filter'          => $filter,
207            'folder'          => $folder,
208            'folders'         => $folders,
209            'format'          => $format,
210            'formats'         => $formats,
211            'max'             => $max,
212            'media_objects'   => $media_objects,
213            'page'            => $page,
214            'pages'           => $pages,
215            'subdirs'         => $subdirs,
216            'data_filesystem' => $data_filesystem,
217            'module'          => $this,
218            'title'           => I18N::translate('Media'),
219            'tree'            => $tree,
220        ]);
221    }
222
223    /**
224     * Generate a list of all the folders in a current tree.
225     *
226     * @param Tree $tree
227     *
228     * @return array<string>
229     */
230    private function allFolders(Tree $tree): array
231    {
232        $folders = DB::table('media_file')
233            ->where('m_file', '=', $tree->id())
234            ->where('multimedia_file_refn', 'NOT LIKE', 'http:%')
235            ->where('multimedia_file_refn', 'NOT LIKE', 'https:%')
236            ->where('multimedia_file_refn', 'LIKE', '%/%')
237            ->pluck('multimedia_file_refn', 'multimedia_file_refn')
238            ->map(static function (string $path): string {
239                return dirname($path);
240            })
241            ->uniqueStrict()
242            ->sort()
243            ->all();
244
245        // Ensure we have an empty (top level) folder.
246        array_unshift($folders, '');
247
248        return array_combine($folders, $folders);
249    }
250
251    /**
252     * Generate a list of all the media objects matching the criteria in a current tree.
253     *
254     * @param Tree   $tree       find media in this tree
255     * @param string $folder     folder to search
256     * @param string $subfolders either "include" or "exclude"
257     * @param string $sort       either "file" or "title"
258     * @param string $filter     optional search string
259     * @param string $format     option OBJE/FILE/FORM/TYPE
260     *
261     * @return Collection<int,Media>
262     */
263    private function allMedia(Tree $tree, string $folder, string $subfolders, string $sort, string $filter, string $format): Collection
264    {
265        $query = DB::table('media')
266            ->join('media_file', static function (JoinClause $join): void {
267                $join
268                    ->on('media_file.m_file', '=', 'media.m_file')
269                    ->on('media_file.m_id', '=', 'media.m_id');
270            })
271            ->where('media.m_file', '=', $tree->id());
272
273        if ($folder === '') {
274            // Include external URLs in the root folder.
275            if ($subfolders === 'exclude') {
276                $query->where(static function (Builder $query): void {
277                    $query
278                        ->where('multimedia_file_refn', 'NOT LIKE', '%/%')
279                        ->orWhere('multimedia_file_refn', 'LIKE', 'http:%')
280                        ->orWhere('multimedia_file_refn', 'LIKE', 'https:%');
281                });
282            }
283        } else {
284            // Exclude external URLs from the root folder.
285            $query
286                ->where('multimedia_file_refn', 'LIKE', $folder . '/%')
287                ->where('multimedia_file_refn', 'NOT LIKE', 'http:%')
288                ->where('multimedia_file_refn', 'NOT LIKE', 'https:%');
289
290            if ($subfolders === 'exclude') {
291                $query->where('multimedia_file_refn', 'NOT LIKE', $folder . '/%/%');
292            }
293        }
294
295        // Apply search terms
296        if ($filter !== '') {
297            $query->where(static function (Builder $query) use ($filter): void {
298                $like = '%' . addcslashes($filter, '\\%_') . '%';
299                $query
300                    ->where('multimedia_file_refn', 'LIKE', $like)
301                    ->orWhere('descriptive_title', 'LIKE', $like);
302            });
303        }
304
305        if ($format) {
306            $query->where('source_media_type', '=', $format);
307        }
308
309        switch ($sort) {
310            case 'file':
311                $query->orderBy('multimedia_file_refn');
312                break;
313            case 'title':
314                $query->orderBy('descriptive_title');
315                break;
316        }
317
318        return $query
319            ->get()
320            ->map(Registry::mediaFactory()->mapper($tree))
321            ->uniqueStrict()
322            ->filter(GedcomRecord::accessFilter());
323    }
324}
325