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