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