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