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