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