xref: /webtrees/app/Services/HomePageService.php (revision 449b311ecf65f677a2595e1e29f712d11ef22f34)
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 fn (ModuleInterface $module): bool => $module->name() === $module_name);
126
127        if ($block instanceof ModuleBlockInterface) {
128            return $block;
129        }
130
131        throw new HttpNotFoundException('Block not found');
132    }
133
134    /**
135     * Get all the available blocks for a tree page.
136     *
137     * @param Tree          $tree
138     * @param UserInterface $user
139     *
140     * @return Collection<string,ModuleBlockInterface>
141     */
142    public function availableTreeBlocks(Tree $tree, UserInterface $user): Collection
143    {
144        return $this->module_service->findByComponent(ModuleBlockInterface::class, $tree, $user)
145            ->filter(static fn (ModuleBlockInterface $block): bool => $block->isTreeBlock())
146            ->mapWithKeys(static fn (ModuleBlockInterface $block): array => [$block->name() => $block]);
147    }
148
149    /**
150     * Get all the available blocks for a user page.
151     *
152     * @param Tree          $tree
153     * @param UserInterface $user
154     *
155     * @return Collection<string,ModuleBlockInterface>
156     */
157    public function availableUserBlocks(Tree $tree, UserInterface $user): Collection
158    {
159        return $this->module_service->findByComponent(ModuleBlockInterface::class, $tree, $user)
160            ->filter(static fn (ModuleBlockInterface $block): bool => $block->isUserBlock())
161            ->mapWithKeys(static fn (ModuleBlockInterface $block): array => [$block->name() => $block]);
162    }
163
164    /**
165     * Get the blocks for a specified tree.
166     *
167     * @param Tree          $tree
168     * @param UserInterface $user
169     * @param string        $location "main" or "side"
170     *
171     * @return Collection<int,ModuleBlockInterface>
172     */
173    public function treeBlocks(Tree $tree, UserInterface $user, string $location): Collection
174    {
175        $rows = DB::table('block')
176            ->where('gedcom_id', '=', $tree->id())
177            ->where('location', '=', $location)
178            ->orderBy('block_order')
179            ->pluck('module_name', 'block_id');
180
181        return $this->filterActiveBlocks($rows, $this->availableTreeBlocks($tree, $user));
182    }
183
184    /**
185     * Make sure that default blocks exist for a tree.
186     *
187     * @return void
188     */
189    public function checkDefaultTreeBlocksExist(): void
190    {
191        $has_blocks = DB::table('block')
192            ->where('gedcom_id', '=', -1)
193            ->exists();
194
195        // No default settings?  Create them.
196        if (!$has_blocks) {
197            foreach ([ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS] as $location) {
198                foreach (ModuleBlockInterface::DEFAULT_TREE_PAGE_BLOCKS[$location] as $block_order => $class) {
199                    $module = $this->module_service->findByInterface($class)->first();
200
201                    if ($module instanceof ModuleInterface) {
202                        DB::table('block')->insert([
203                            'gedcom_id'   => -1,
204                            'location'    => $location,
205                            'block_order' => $block_order,
206                            'module_name' => $module->name(),
207                        ]);
208                    }
209                }
210            }
211        }
212    }
213
214    /**
215     * Get the blocks for a specified user.
216     *
217     * @param Tree          $tree
218     * @param UserInterface $user
219     * @param string        $location "main" or "side"
220     *
221     * @return Collection<int,ModuleBlockInterface>
222     */
223    public function userBlocks(Tree $tree, UserInterface $user, string $location): Collection
224    {
225        $rows = DB::table('block')
226            ->where('user_id', '=', $user->id())
227            ->where('location', '=', $location)
228            ->orderBy('block_order')
229            ->pluck('module_name', 'block_id');
230
231        return $this->filterActiveBlocks($rows, $this->availableUserBlocks($tree, $user));
232    }
233
234    /**
235     * Make sure that default blocks exist for a user.
236     *
237     * @return void
238     */
239    public function checkDefaultUserBlocksExist(): void
240    {
241        $has_blocks = DB::table('block')
242            ->where('user_id', '=', -1)
243            ->exists();
244
245        // No default settings?  Create them.
246        if (!$has_blocks) {
247            foreach ([ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS] as $location) {
248                foreach (ModuleBlockInterface::DEFAULT_USER_PAGE_BLOCKS[$location] as $block_order => $class) {
249                    $module = $this->module_service->findByInterface($class)->first();
250
251                    if ($module instanceof ModuleBlockInterface) {
252                        DB::table('block')->insert([
253                            'user_id'     => -1,
254                            'location'    => $location,
255                            'block_order' => $block_order,
256                            'module_name' => $module->name(),
257                        ]);
258                    }
259                }
260            }
261        }
262    }
263
264    /**
265     * Save the updated blocks for a user.
266     *
267     * @param int                    $user_id
268     * @param Collection<int,string> $main_block_ids
269     * @param Collection<int,string> $side_block_ids
270     *
271     * @return void
272     */
273    public function updateUserBlocks(int $user_id, Collection $main_block_ids, Collection $side_block_ids): void
274    {
275        $existing_block_ids = DB::table('block')
276            ->where('user_id', '=', $user_id)
277            ->whereIn('location', [ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS])
278            ->pluck('block_id');
279
280        // Deleted blocks
281        foreach ($existing_block_ids as $existing_block_id) {
282            if (!$main_block_ids->contains($existing_block_id) && !$side_block_ids->contains($existing_block_id)) {
283                DB::table('block_setting')
284                    ->where('block_id', '=', $existing_block_id)
285                    ->delete();
286
287                DB::table('block')
288                    ->where('block_id', '=', $existing_block_id)
289                    ->delete();
290            }
291        }
292
293        $updates = [
294            ModuleBlockInterface::MAIN_BLOCKS => $main_block_ids,
295            ModuleBlockInterface::SIDE_BLOCKS => $side_block_ids,
296        ];
297
298        foreach ($updates as $location => $updated_blocks) {
299            foreach ($updated_blocks as $block_order => $block_id) {
300                if (is_numeric($block_id)) {
301                    // Updated block
302                    DB::table('block')
303                        ->where('block_id', '=', $block_id)
304                        ->update([
305                            'block_order' => $block_order,
306                            'location'    => $location,
307                        ]);
308                } else {
309                    // New block
310                    DB::table('block')->insert([
311                        'user_id'     => $user_id,
312                        'location'    => $location,
313                        'block_order' => $block_order,
314                        'module_name' => $block_id,
315                    ]);
316                }
317            }
318        }
319    }
320
321    /**
322     * Save the updated blocks for a tree.
323     *
324     * @param int                    $tree_id
325     * @param Collection<int,string> $main_block_ids
326     * @param Collection<int,string> $side_block_ids
327     *
328     * @return void
329     */
330    public function updateTreeBlocks(int $tree_id, Collection $main_block_ids, Collection $side_block_ids): void
331    {
332        $existing_block_ids = DB::table('block')
333            ->where('gedcom_id', '=', $tree_id)
334            ->whereIn('location', [ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS])
335            ->pluck('block_id');
336
337        // Deleted blocks
338        foreach ($existing_block_ids as $existing_block_id) {
339            if (!$main_block_ids->contains($existing_block_id) && !$side_block_ids->contains($existing_block_id)) {
340                DB::table('block_setting')
341                    ->where('block_id', '=', $existing_block_id)
342                    ->delete();
343
344                DB::table('block')
345                    ->where('block_id', '=', $existing_block_id)
346                    ->delete();
347            }
348        }
349
350        $updates = [
351            ModuleBlockInterface::MAIN_BLOCKS => $main_block_ids,
352            ModuleBlockInterface::SIDE_BLOCKS => $side_block_ids,
353        ];
354
355        foreach ($updates as $location => $updated_blocks) {
356            foreach ($updated_blocks as $block_order => $block_id) {
357                if (is_numeric($block_id)) {
358                    // Updated block
359                    DB::table('block')
360                        ->where('block_id', '=', $block_id)
361                        ->update([
362                            'block_order' => $block_order,
363                            'location'    => $location,
364                        ]);
365                } else {
366                    // New block
367                    DB::table('block')->insert([
368                        'gedcom_id'   => $tree_id,
369                        'location'    => $location,
370                        'block_order' => $block_order,
371                        'module_name' => $block_id,
372                    ]);
373                }
374            }
375        }
376    }
377
378    /**
379     * Take a list of block names, and return block (module) objects.
380     *
381     * @param Collection<int,string>               $blocks
382     * @param Collection<int,ModuleBlockInterface> $active_blocks
383     *
384     * @return Collection<int,ModuleBlockInterface>
385     */
386    private function filterActiveBlocks(Collection $blocks, Collection $active_blocks): Collection
387    {
388        return $blocks->map(static fn (string $block_name): ModuleBlockInterface|null => $active_blocks->filter(static fn (ModuleInterface $block): bool => $block->name() === $block_name)->first())
389            ->filter();
390    }
391}
392