xref: /webtrees/app/Services/HomePageService.php (revision 61275c55e2688551b99836d7f49f54ed2b7e5788)
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\Services;
21
22use Fisharebest\Webtrees\Auth;
23use Fisharebest\Webtrees\Contracts\UserInterface;
24use Fisharebest\Webtrees\Exceptions\HttpAccessDeniedException;
25use Fisharebest\Webtrees\Exceptions\HttpNotFoundException;
26use Fisharebest\Webtrees\Module\ModuleBlockInterface;
27use Fisharebest\Webtrees\Module\ModuleInterface;
28use Fisharebest\Webtrees\Tree;
29use Illuminate\Database\Capsule\Manager as DB;
30use Illuminate\Support\Collection;
31use Psr\Http\Message\ServerRequestInterface;
32use stdClass;
33
34use function assert;
35use function is_numeric;
36
37/**
38 * Logic and content for the home-page blocks.
39 */
40class HomePageService
41{
42    private ModuleService $module_service;
43
44    /**
45     * HomePageController constructor.
46     *
47     * @param ModuleService $module_service
48     */
49    public function __construct(ModuleService $module_service)
50    {
51        $this->module_service = $module_service;
52    }
53
54    /**
55     * Load a tree block.
56     *
57     * @param ServerRequestInterface $request
58     *
59     * @return ModuleBlockInterface
60     */
61    public function treeBlock(ServerRequestInterface $request): ModuleBlockInterface
62    {
63        $tree = $request->getAttribute('tree');
64        assert($tree instanceof Tree);
65
66        $block_id = (int) $request->getQueryParams()['block_id'];
67
68        $block = DB::table('block')
69            ->where('block_id', '=', $block_id)
70            ->where('gedcom_id', '=', $tree->id())
71            ->whereNull('user_id')
72            ->first();
73
74        if ($block instanceof stdClass) {
75            $module = $this->module_service->findByName($block->module_name);
76
77            if ($module instanceof ModuleBlockInterface) {
78                return $module;
79            }
80        }
81
82        throw new HttpNotFoundException();
83    }
84
85    /**
86     * Load a user block.
87     *
88     * @param ServerRequestInterface $request
89     * @param UserInterface          $user
90     *
91     * @return ModuleBlockInterface
92     */
93    public function userBlock(ServerRequestInterface $request, UserInterface $user): ModuleBlockInterface
94    {
95        $block_id = (int) $request->getQueryParams()['block_id'];
96
97        $block = DB::table('block')
98            ->where('block_id', '=', $block_id)
99            ->where('user_id', '=', $user->id())
100            ->whereNull('gedcom_id')
101            ->first();
102
103        if ($block instanceof stdClass) {
104            $module = $this->module_service->findByName($block->module_name);
105
106            if ($module instanceof ModuleBlockInterface) {
107                return $module;
108            }
109        }
110
111        throw new HttpNotFoundException();
112    }
113
114    /**
115     * Get a specific block.
116     *
117     * @param Tree $tree
118     * @param int  $block_id
119     *
120     * @return ModuleBlockInterface
121     */
122    public function getBlockModule(Tree $tree, int $block_id): ModuleBlockInterface
123    {
124        $active_blocks = $this->module_service->findByComponent(ModuleBlockInterface::class, $tree, Auth::user());
125
126        $module_name = DB::table('block')
127            ->where('block_id', '=', $block_id)
128            ->value('module_name');
129
130        $block = $active_blocks->first(static function (ModuleInterface $module) use ($module_name): bool {
131            return $module->name() === $module_name;
132        });
133
134        if ($block instanceof ModuleBlockInterface) {
135            return $block;
136        }
137
138        throw new HttpNotFoundException('Block not found');
139    }
140
141    /**
142     * Get all the available blocks for a tree page.
143     *
144     * @param Tree          $tree
145     * @param UserInterface $user
146     *
147     * @return Collection<string,ModuleBlockInterface>
148     */
149    public function availableTreeBlocks(Tree $tree, UserInterface $user): Collection
150    {
151        return $this->module_service->findByComponent(ModuleBlockInterface::class, $tree, $user)
152            ->filter(static function (ModuleBlockInterface $block): bool {
153                return $block->isTreeBlock();
154            })
155            ->mapWithKeys(static function (ModuleBlockInterface $block): array {
156                return [$block->name() => $block];
157            });
158    }
159
160    /**
161     * Get all the available blocks for a user page.
162     *
163     * @param Tree          $tree
164     * @param UserInterface $user
165     *
166     * @return Collection<string,ModuleBlockInterface>
167     */
168    public function availableUserBlocks(Tree $tree, UserInterface $user): Collection
169    {
170        return $this->module_service->findByComponent(ModuleBlockInterface::class, $tree, $user)
171            ->filter(static function (ModuleBlockInterface $block): bool {
172                return $block->isUserBlock();
173            })
174            ->mapWithKeys(static function (ModuleBlockInterface $block): array {
175                return [$block->name() => $block];
176            });
177    }
178
179    /**
180     * Get the blocks for a specified tree.
181     *
182     * @param Tree          $tree
183     * @param UserInterface $user
184     * @param string        $location "main" or "side"
185     *
186     * @return Collection<int,ModuleBlockInterface>
187     */
188    public function treeBlocks(Tree $tree, UserInterface $user, string $location): Collection
189    {
190        $rows = DB::table('block')
191            ->where('gedcom_id', '=', $tree->id())
192            ->where('location', '=', $location)
193            ->orderBy('block_order')
194            ->pluck('module_name', 'block_id');
195
196        return $this->filterActiveBlocks($rows, $this->availableTreeBlocks($tree, $user));
197    }
198
199    /**
200     * Make sure that default blocks exist for a tree.
201     *
202     * @return void
203     */
204    public function checkDefaultTreeBlocksExist(): void
205    {
206        $has_blocks = DB::table('block')
207            ->where('gedcom_id', '=', -1)
208            ->exists();
209
210        // No default settings?  Create them.
211        if (!$has_blocks) {
212            foreach ([ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS] as $location) {
213                foreach (ModuleBlockInterface::DEFAULT_TREE_PAGE_BLOCKS[$location] as $block_order => $class) {
214                    $module = $this->module_service->findByInterface($class)->first();
215
216                    if ($module instanceof ModuleInterface) {
217                        DB::table('block')->insert([
218                            'gedcom_id'   => -1,
219                            'location'    => $location,
220                            'block_order' => $block_order,
221                            'module_name' => $module->name(),
222                        ]);
223                    }
224                }
225            }
226        }
227    }
228
229    /**
230     * Get the blocks for a specified user.
231     *
232     * @param Tree          $tree
233     * @param UserInterface $user
234     * @param string        $location "main" or "side"
235     *
236     * @return Collection<int,ModuleBlockInterface>
237     */
238    public function userBlocks(Tree $tree, UserInterface $user, string $location): Collection
239    {
240        $rows = DB::table('block')
241            ->where('user_id', '=', $user->id())
242            ->where('location', '=', $location)
243            ->orderBy('block_order')
244            ->pluck('module_name', 'block_id');
245
246        return $this->filterActiveBlocks($rows, $this->availableUserBlocks($tree, $user));
247    }
248
249    /**
250     * Make sure that default blocks exist for a user.
251     *
252     * @return void
253     */
254    public function checkDefaultUserBlocksExist(): void
255    {
256        $has_blocks = DB::table('block')
257            ->where('user_id', '=', -1)
258            ->exists();
259
260        // No default settings?  Create them.
261        if (!$has_blocks) {
262            foreach ([ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS] as $location) {
263                foreach (ModuleBlockInterface::DEFAULT_USER_PAGE_BLOCKS[$location] as $block_order => $class) {
264                    $module = $this->module_service->findByInterface($class)->first();
265
266                    if ($module instanceof ModuleBlockInterface) {
267                        DB::table('block')->insert([
268                            'user_id'     => -1,
269                            'location'    => $location,
270                            'block_order' => $block_order,
271                            'module_name' => $module->name(),
272                        ]);
273                    }
274                }
275            }
276        }
277    }
278
279    /**
280     * Save the updated blocks for a user.
281     *
282     * @param int                $user_id
283     * @param Collection<string> $main_block_ids
284     * @param Collection<string> $side_block_ids
285     *
286     * @return void
287     */
288    public function updateUserBlocks(int $user_id, Collection $main_block_ids, Collection $side_block_ids): void
289    {
290        $existing_block_ids = DB::table('block')
291            ->where('user_id', '=', $user_id)
292            ->whereIn('location', [ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS])
293            ->pluck('block_id');
294
295        // Deleted blocks
296        foreach ($existing_block_ids as $existing_block_id) {
297            if (!$main_block_ids->contains($existing_block_id) && !$side_block_ids->contains($existing_block_id)) {
298                DB::table('block_setting')
299                    ->where('block_id', '=', $existing_block_id)
300                    ->delete();
301
302                DB::table('block')
303                    ->where('block_id', '=', $existing_block_id)
304                    ->delete();
305            }
306        }
307
308        $updates = [
309            ModuleBlockInterface::MAIN_BLOCKS => $main_block_ids,
310            ModuleBlockInterface::SIDE_BLOCKS => $side_block_ids,
311        ];
312
313        foreach ($updates as $location => $updated_blocks) {
314            foreach ($updated_blocks as $block_order => $block_id) {
315                if (is_numeric($block_id)) {
316                    // Updated block
317                    DB::table('block')
318                        ->where('block_id', '=', $block_id)
319                        ->update([
320                            'block_order' => $block_order,
321                            'location'    => $location,
322                        ]);
323                } else {
324                    // New block
325                    DB::table('block')->insert([
326                        'user_id'     => $user_id,
327                        'location'    => $location,
328                        'block_order' => $block_order,
329                        'module_name' => $block_id,
330                    ]);
331                }
332            }
333        }
334    }
335
336    /**
337     * Save the updated blocks for a tree.
338     *
339     * @param int                $tree_id
340     * @param Collection<string> $main_block_ids
341     * @param Collection<string> $side_block_ids
342     *
343     * @return void
344     */
345    public function updateTreeBlocks(int $tree_id, Collection $main_block_ids, Collection $side_block_ids): void
346    {
347        $existing_block_ids = DB::table('block')
348            ->where('gedcom_id', '=', $tree_id)
349            ->whereIn('location', [ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS])
350            ->pluck('block_id');
351
352        // Deleted blocks
353        foreach ($existing_block_ids as $existing_block_id) {
354            if (!$main_block_ids->contains($existing_block_id) && !$side_block_ids->contains($existing_block_id)) {
355                DB::table('block_setting')
356                    ->where('block_id', '=', $existing_block_id)
357                    ->delete();
358
359                DB::table('block')
360                    ->where('block_id', '=', $existing_block_id)
361                    ->delete();
362            }
363        }
364
365        $updates = [
366            ModuleBlockInterface::MAIN_BLOCKS => $main_block_ids,
367            ModuleBlockInterface::SIDE_BLOCKS => $side_block_ids,
368        ];
369
370        foreach ($updates as $location => $updated_blocks) {
371            foreach ($updated_blocks as $block_order => $block_id) {
372                if (is_numeric($block_id)) {
373                    // Updated block
374                    DB::table('block')
375                        ->where('block_id', '=', $block_id)
376                        ->update([
377                            'block_order' => $block_order,
378                            'location'    => $location,
379                        ]);
380                } else {
381                    // New block
382                    DB::table('block')->insert([
383                        'gedcom_id'   => $tree_id,
384                        'location'    => $location,
385                        'block_order' => $block_order,
386                        'module_name' => $block_id,
387                    ]);
388                }
389            }
390        }
391    }
392
393    /**
394     * Take a list of block names, and return block (module) objects.
395     *
396     * @param Collection<string>                   $blocks
397     * @param Collection<int,ModuleBlockInterface> $active_blocks
398     *
399     * @return Collection<int,ModuleBlockInterface>
400     */
401    private function filterActiveBlocks(Collection $blocks, Collection $active_blocks): Collection
402    {
403        return $blocks->map(static function (string $block_name) use ($active_blocks): ?ModuleBlockInterface {
404            return $active_blocks->filter(static function (ModuleInterface $block) use ($block_name): bool {
405                return $block->name() === $block_name;
406            })->first();
407        })->filter();
408    }
409}
410