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<string,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<string,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<string,ModuleBlockInterface> $active_blocks 414 * 415 * @return Collection<string,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