xref: /webtrees/app/Module/MediaListModule.php (revision 2ebcf907ed34213f816592af04e6c160335d6311)
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\I18N;
29use Fisharebest\Webtrees\Media;
30use Fisharebest\Webtrees\Tree;
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|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        return redirect($this->listUrl($request->getAttribute('tree'), $request->getQueryParams()));
149    }
150
151    /**
152     * @param ServerRequestInterface $request
153     *
154     * @return ResponseInterface
155     */
156    public function handle(ServerRequestInterface $request): ResponseInterface
157    {
158        $tree = $request->getAttribute('tree');
159        assert($tree instanceof Tree);
160
161        $user = $request->getAttribute('user');
162        assert($user instanceof UserInterface);
163
164        $data_filesystem = Registry::filesystem()->data();
165
166        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
167
168        // Convert POST requests into GET requests for pretty URLs.
169        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
170            return redirect($this->listUrl($tree, (array) $request->getParsedBody()));
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            'max'             => $max,
213            'media_objects'   => $media_objects,
214            'page'            => $page,
215            'pages'           => $pages,
216            'subdirs'         => $subdirs,
217            'data_filesystem' => $data_filesystem,
218            'module'          => $this,
219            'title'           => I18N::translate('Media'),
220            'tree'            => $tree,
221        ]);
222    }
223
224    /**
225     * Generate a list of all the folders in a current tree.
226     *
227     * @param Tree $tree
228     *
229     * @return array<string>
230     */
231    private function allFolders(Tree $tree): array
232    {
233        $folders = DB::table('media_file')
234            ->where('m_file', '=', $tree->id())
235            ->where('multimedia_file_refn', 'NOT LIKE', 'http:%')
236            ->where('multimedia_file_refn', 'NOT LIKE', 'https:%')
237            ->where('multimedia_file_refn', 'LIKE', '%/%')
238            ->pluck('multimedia_file_refn', 'multimedia_file_refn')
239            ->map(static function (string $path): string {
240                return dirname($path);
241            })
242            ->uniqueStrict()
243            ->sort()
244            ->all();
245
246        // Ensure we have an empty (top level) folder.
247        array_unshift($folders, '');
248
249        return array_combine($folders, $folders);
250    }
251
252    /**
253     * Generate a list of all the media objects matching the criteria in a current tree.
254     *
255     * @param Tree   $tree       find media in this tree
256     * @param string $folder     folder to search
257     * @param string $subfolders either "include" or "exclude"
258     * @param string $sort       either "file" or "title"
259     * @param string $filter     optional search string
260     * @param string $format     option OBJE/FILE/FORM/TYPE
261     *
262     * @return Collection<Media>
263     */
264    private function allMedia(Tree $tree, string $folder, string $subfolders, string $sort, string $filter, string $format): Collection
265    {
266        $query = DB::table('media')
267            ->join('media_file', static function (JoinClause $join): void {
268                $join
269                    ->on('media_file.m_file', '=', 'media.m_file')
270                    ->on('media_file.m_id', '=', 'media.m_id');
271            })
272            ->where('media.m_file', '=', $tree->id());
273
274        if ($folder === '') {
275            // Include external URLs in the root folder.
276            if ($subfolders === 'exclude') {
277                $query->where(static function (Builder $query): void {
278                    $query
279                        ->where('multimedia_file_refn', 'NOT LIKE', '%/%')
280                        ->orWhere('multimedia_file_refn', 'LIKE', 'http:%')
281                        ->orWhere('multimedia_file_refn', 'LIKE', 'https:%');
282                });
283            }
284        } else {
285            // Exclude external URLs from the root folder.
286            $query
287                ->where('multimedia_file_refn', 'LIKE', $folder . '/%')
288                ->where('multimedia_file_refn', 'NOT LIKE', 'http:%')
289                ->where('multimedia_file_refn', 'NOT LIKE', 'https:%');
290
291            if ($subfolders === 'exclude') {
292                $query->where('multimedia_file_refn', 'NOT LIKE', $folder . '/%/%');
293            }
294        }
295
296        // Apply search terms
297        if ($filter !== '') {
298            $query->where(static function (Builder $query) use ($filter): void {
299                $like = '%' . addcslashes($filter, '\\%_') . '%';
300                $query
301                    ->where('multimedia_file_refn', 'LIKE', $like)
302                    ->orWhere('descriptive_title', 'LIKE', $like);
303            });
304        }
305
306        if ($format) {
307            $query->where('source_media_type', '=', $format);
308        }
309
310        switch ($sort) {
311            case 'file':
312                $query->orderBy('multimedia_file_refn');
313                break;
314            case 'title':
315                $query->orderBy('descriptive_title');
316                break;
317        }
318
319        return $query
320            ->get()
321            ->map(Registry::mediaFactory()->mapper($tree))
322            ->uniqueStrict()
323            ->filter(GedcomRecord::accessFilter());
324    }
325}
326