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