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 fn (ModuleInterface $module): bool => $module->name() === $module_name); 126 127 if ($block instanceof ModuleBlockInterface) { 128 return $block; 129 } 130 131 throw new HttpNotFoundException('Block not found'); 132 } 133 134 /** 135 * Get all the available blocks for a tree page. 136 * 137 * @param Tree $tree 138 * @param UserInterface $user 139 * 140 * @return Collection<string,ModuleBlockInterface> 141 */ 142 public function availableTreeBlocks(Tree $tree, UserInterface $user): Collection 143 { 144 return $this->module_service->findByComponent(ModuleBlockInterface::class, $tree, $user) 145 ->filter(static fn (ModuleBlockInterface $block): bool => $block->isTreeBlock()) 146 ->mapWithKeys(static fn (ModuleBlockInterface $block): array => [$block->name() => $block]); 147 } 148 149 /** 150 * Get all the available blocks for a user page. 151 * 152 * @param Tree $tree 153 * @param UserInterface $user 154 * 155 * @return Collection<string,ModuleBlockInterface> 156 */ 157 public function availableUserBlocks(Tree $tree, UserInterface $user): Collection 158 { 159 return $this->module_service->findByComponent(ModuleBlockInterface::class, $tree, $user) 160 ->filter(static fn (ModuleBlockInterface $block): bool => $block->isUserBlock()) 161 ->mapWithKeys(static fn (ModuleBlockInterface $block): array => [$block->name() => $block]); 162 } 163 164 /** 165 * Get the blocks for a specified tree. 166 * 167 * @param Tree $tree 168 * @param UserInterface $user 169 * @param string $location "main" or "side" 170 * 171 * @return Collection<int,ModuleBlockInterface> 172 */ 173 public function treeBlocks(Tree $tree, UserInterface $user, string $location): Collection 174 { 175 $rows = DB::table('block') 176 ->where('gedcom_id', '=', $tree->id()) 177 ->where('location', '=', $location) 178 ->orderBy('block_order') 179 ->pluck('module_name', 'block_id'); 180 181 return $this->filterActiveBlocks($rows, $this->availableTreeBlocks($tree, $user)); 182 } 183 184 /** 185 * Make sure that default blocks exist for a tree. 186 * 187 * @return void 188 */ 189 public function checkDefaultTreeBlocksExist(): void 190 { 191 $has_blocks = DB::table('block') 192 ->where('gedcom_id', '=', -1) 193 ->exists(); 194 195 // No default settings? Create them. 196 if (!$has_blocks) { 197 foreach ([ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS] as $location) { 198 foreach (ModuleBlockInterface::DEFAULT_TREE_PAGE_BLOCKS[$location] as $block_order => $class) { 199 $module = $this->module_service->findByInterface($class)->first(); 200 201 if ($module instanceof ModuleInterface) { 202 DB::table('block')->insert([ 203 'gedcom_id' => -1, 204 'location' => $location, 205 'block_order' => $block_order, 206 'module_name' => $module->name(), 207 ]); 208 } 209 } 210 } 211 } 212 } 213 214 /** 215 * Get the blocks for a specified user. 216 * 217 * @param Tree $tree 218 * @param UserInterface $user 219 * @param string $location "main" or "side" 220 * 221 * @return Collection<int,ModuleBlockInterface> 222 */ 223 public function userBlocks(Tree $tree, UserInterface $user, string $location): Collection 224 { 225 $rows = DB::table('block') 226 ->where('user_id', '=', $user->id()) 227 ->where('location', '=', $location) 228 ->orderBy('block_order') 229 ->pluck('module_name', 'block_id'); 230 231 return $this->filterActiveBlocks($rows, $this->availableUserBlocks($tree, $user)); 232 } 233 234 /** 235 * Make sure that default blocks exist for a user. 236 * 237 * @return void 238 */ 239 public function checkDefaultUserBlocksExist(): void 240 { 241 $has_blocks = DB::table('block') 242 ->where('user_id', '=', -1) 243 ->exists(); 244 245 // No default settings? Create them. 246 if (!$has_blocks) { 247 foreach ([ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS] as $location) { 248 foreach (ModuleBlockInterface::DEFAULT_USER_PAGE_BLOCKS[$location] as $block_order => $class) { 249 $module = $this->module_service->findByInterface($class)->first(); 250 251 if ($module instanceof ModuleBlockInterface) { 252 DB::table('block')->insert([ 253 'user_id' => -1, 254 'location' => $location, 255 'block_order' => $block_order, 256 'module_name' => $module->name(), 257 ]); 258 } 259 } 260 } 261 } 262 } 263 264 /** 265 * Save the updated blocks for a user. 266 * 267 * @param int $user_id 268 * @param Collection<int,string> $main_block_ids 269 * @param Collection<int,string> $side_block_ids 270 * 271 * @return void 272 */ 273 public function updateUserBlocks(int $user_id, Collection $main_block_ids, Collection $side_block_ids): void 274 { 275 $existing_block_ids = DB::table('block') 276 ->where('user_id', '=', $user_id) 277 ->whereIn('location', [ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS]) 278 ->pluck('block_id'); 279 280 // Deleted blocks 281 foreach ($existing_block_ids as $existing_block_id) { 282 if (!$main_block_ids->contains($existing_block_id) && !$side_block_ids->contains($existing_block_id)) { 283 DB::table('block_setting') 284 ->where('block_id', '=', $existing_block_id) 285 ->delete(); 286 287 DB::table('block') 288 ->where('block_id', '=', $existing_block_id) 289 ->delete(); 290 } 291 } 292 293 $updates = [ 294 ModuleBlockInterface::MAIN_BLOCKS => $main_block_ids, 295 ModuleBlockInterface::SIDE_BLOCKS => $side_block_ids, 296 ]; 297 298 foreach ($updates as $location => $updated_blocks) { 299 foreach ($updated_blocks as $block_order => $block_id) { 300 if (is_numeric($block_id)) { 301 // Updated block 302 DB::table('block') 303 ->where('block_id', '=', $block_id) 304 ->update([ 305 'block_order' => $block_order, 306 'location' => $location, 307 ]); 308 } else { 309 // New block 310 DB::table('block')->insert([ 311 'user_id' => $user_id, 312 'location' => $location, 313 'block_order' => $block_order, 314 'module_name' => $block_id, 315 ]); 316 } 317 } 318 } 319 } 320 321 /** 322 * Save the updated blocks for a tree. 323 * 324 * @param int $tree_id 325 * @param Collection<int,string> $main_block_ids 326 * @param Collection<int,string> $side_block_ids 327 * 328 * @return void 329 */ 330 public function updateTreeBlocks(int $tree_id, Collection $main_block_ids, Collection $side_block_ids): void 331 { 332 $existing_block_ids = DB::table('block') 333 ->where('gedcom_id', '=', $tree_id) 334 ->whereIn('location', [ModuleBlockInterface::MAIN_BLOCKS, ModuleBlockInterface::SIDE_BLOCKS]) 335 ->pluck('block_id'); 336 337 // Deleted blocks 338 foreach ($existing_block_ids as $existing_block_id) { 339 if (!$main_block_ids->contains($existing_block_id) && !$side_block_ids->contains($existing_block_id)) { 340 DB::table('block_setting') 341 ->where('block_id', '=', $existing_block_id) 342 ->delete(); 343 344 DB::table('block') 345 ->where('block_id', '=', $existing_block_id) 346 ->delete(); 347 } 348 } 349 350 $updates = [ 351 ModuleBlockInterface::MAIN_BLOCKS => $main_block_ids, 352 ModuleBlockInterface::SIDE_BLOCKS => $side_block_ids, 353 ]; 354 355 foreach ($updates as $location => $updated_blocks) { 356 foreach ($updated_blocks as $block_order => $block_id) { 357 if (is_numeric($block_id)) { 358 // Updated block 359 DB::table('block') 360 ->where('block_id', '=', $block_id) 361 ->update([ 362 'block_order' => $block_order, 363 'location' => $location, 364 ]); 365 } else { 366 // New block 367 DB::table('block')->insert([ 368 'gedcom_id' => $tree_id, 369 'location' => $location, 370 'block_order' => $block_order, 371 'module_name' => $block_id, 372 ]); 373 } 374 } 375 } 376 } 377 378 /** 379 * Take a list of block names, and return block (module) objects. 380 * 381 * @param Collection<int,string> $blocks 382 * @param Collection<int,ModuleBlockInterface> $active_blocks 383 * 384 * @return Collection<int,ModuleBlockInterface> 385 */ 386 private function filterActiveBlocks(Collection $blocks, Collection $active_blocks): Collection 387 { 388 return $blocks->map(static fn (string $block_name): ModuleBlockInterface|null => $active_blocks->filter(static fn (ModuleInterface $block): bool => $block->name() === $block_name)->first()) 389 ->filter(); 390 } 391} 392