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