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