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