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\Http\RequestHandlers; 21 22use Fisharebest\Webtrees\Http\ViewResponseTrait; 23use Fisharebest\Webtrees\I18N; 24use Fisharebest\Webtrees\Module\FamilyListModule; 25use Fisharebest\Webtrees\Module\IndividualListModule; 26use Fisharebest\Webtrees\Module\MediaListModule; 27use Fisharebest\Webtrees\Module\ModuleAnalyticsInterface; 28use Fisharebest\Webtrees\Module\ModuleBlockInterface; 29use Fisharebest\Webtrees\Module\ModuleChartInterface; 30use Fisharebest\Webtrees\Module\ModuleCustomInterface; 31use Fisharebest\Webtrees\Module\ModuleDataFixInterface; 32use Fisharebest\Webtrees\Module\ModuleFooterInterface; 33use Fisharebest\Webtrees\Module\ModuleHistoricEventsInterface; 34use Fisharebest\Webtrees\Module\ModuleLanguageInterface; 35use Fisharebest\Webtrees\Module\ModuleListInterface; 36use Fisharebest\Webtrees\Module\ModuleMenuInterface; 37use Fisharebest\Webtrees\Module\ModuleReportInterface; 38use Fisharebest\Webtrees\Module\ModuleShareInterface; 39use Fisharebest\Webtrees\Module\ModuleSidebarInterface; 40use Fisharebest\Webtrees\Module\ModuleTabInterface; 41use Fisharebest\Webtrees\Module\ModuleThemeInterface; 42use Fisharebest\Webtrees\Module\NoteListModule; 43use Fisharebest\Webtrees\Module\RepositoryListModule; 44use Fisharebest\Webtrees\Module\SourceListModule; 45use Fisharebest\Webtrees\Module\SubmitterListModule; 46use Fisharebest\Webtrees\Note; 47use Fisharebest\Webtrees\Registry; 48use Fisharebest\Webtrees\Repository; 49use Fisharebest\Webtrees\Services\AdminService; 50use Fisharebest\Webtrees\Services\HousekeepingService; 51use Fisharebest\Webtrees\Services\ModuleService; 52use Fisharebest\Webtrees\Services\ServerCheckService; 53use Fisharebest\Webtrees\Services\TreeService; 54use Fisharebest\Webtrees\Services\UpgradeService; 55use Fisharebest\Webtrees\Services\UserService; 56use Fisharebest\Webtrees\Submitter; 57use Fisharebest\Webtrees\Webtrees; 58use Illuminate\Database\Capsule\Manager as DB; 59use Illuminate\Database\Query\Expression; 60use Illuminate\Database\Query\JoinClause; 61use Illuminate\Support\Collection; 62use League\Flysystem\Filesystem; 63use League\Flysystem\Local\LocalFilesystemAdapter; 64use Psr\Http\Message\ResponseInterface; 65use Psr\Http\Message\ServerRequestInterface; 66use Psr\Http\Server\RequestHandlerInterface; 67 68/** 69 * The control panel shows a summary of the site and links to admin functions. 70 */ 71class ControlPanel implements RequestHandlerInterface 72{ 73 use ViewResponseTrait; 74 75 /** @var AdminService */ 76 private $admin_service; 77 78 /** @var ModuleService */ 79 private $module_service; 80 81 /** @var HousekeepingService */ 82 private $housekeeping_service; 83 84 /** @var ServerCheckService */ 85 private $server_check_service; 86 87 /** @var TreeService */ 88 private $tree_service; 89 90 /** @var UpgradeService */ 91 private $upgrade_service; 92 93 /** @var UserService */ 94 private $user_service; 95 96 /** 97 * ControlPanel constructor. 98 * 99 * @param AdminService $admin_service 100 * @param HousekeepingService $housekeeping_service 101 * @param ModuleService $module_service 102 * @param ServerCheckService $server_check_service 103 * @param TreeService $tree_service 104 * @param UpgradeService $upgrade_service 105 * @param UserService $user_service 106 */ 107 public function __construct( 108 AdminService $admin_service, 109 HousekeepingService $housekeeping_service, 110 ModuleService $module_service, 111 ServerCheckService $server_check_service, 112 TreeService $tree_service, 113 UpgradeService $upgrade_service, 114 UserService $user_service 115 ) { 116 $this->admin_service = $admin_service; 117 $this->housekeeping_service = $housekeeping_service; 118 $this->module_service = $module_service; 119 $this->server_check_service = $server_check_service; 120 $this->tree_service = $tree_service; 121 $this->upgrade_service = $upgrade_service; 122 $this->user_service = $user_service; 123 } 124 125 /** 126 * @param ServerRequestInterface $request 127 * 128 * @return ResponseInterface 129 */ 130 public function handle(ServerRequestInterface $request): ResponseInterface 131 { 132 $this->layout = 'layouts/administration'; 133 134 $filesystem = new Filesystem(new LocalFilesystemAdapter(Webtrees::ROOT_DIR)); 135 $files_to_delete = $this->housekeeping_service->deleteOldWebtreesFiles($filesystem); 136 137 $custom_updates = $this->module_service 138 ->findByInterface(ModuleCustomInterface::class) 139 ->filter(static function (ModuleCustomInterface $module): bool { 140 return version_compare($module->customModuleLatestVersion(), $module->customModuleVersion()) > 0; 141 }); 142 143 $multiple_tree_threshold = $this->admin_service->multipleTreeThreshold(); 144 $gedcom_file_count = $this->admin_service->gedcomFiles(Registry::filesystem()->data())->count(); 145 146 return $this->viewResponse('admin/control-panel', [ 147 'title' => I18N::translate('Control panel'), 148 'server_errors' => $this->server_check_service->serverErrors(), 149 'server_warnings' => $this->server_check_service->serverWarnings(), 150 'latest_version' => $this->upgrade_service->latestVersion(), 151 'all_users' => $this->user_service->all(), 152 'administrators' => $this->user_service->administrators(), 153 'managers' => $this->user_service->managers(), 154 'moderators' => $this->user_service->moderators(), 155 'unapproved' => $this->user_service->unapproved(), 156 'unverified' => $this->user_service->unverified(), 157 'all_trees' => $this->tree_service->all(), 158 'changes' => $this->totalChanges(), 159 'individuals' => $this->totalIndividuals(), 160 'families' => $this->totalFamilies(), 161 'sources' => $this->totalSources(), 162 'media' => $this->totalMediaObjects(), 163 'repositories' => $this->totalRepositories(), 164 'notes' => $this->totalNotes(), 165 'submitters' => $this->totalSubmitters(), 166 'individual_list_module' => $this->module_service->findByInterface(IndividualListModule::class)->last(), 167 'family_list_module' => $this->module_service->findByInterface(FamilyListModule::class)->first(), 168 'media_list_module' => $this->module_service->findByInterface(MediaListModule::class)->first(), 169 'note_list_module' => $this->module_service->findByInterface(NoteListModule::class)->first(), 170 'repository_list_module' => $this->module_service->findByInterface(RepositoryListModule::class)->first(), 171 'source_list_module' => $this->module_service->findByInterface(SourceListModule::class)->first(), 172 'submitter_list_module' => $this->module_service->findByInterface(SubmitterListModule::class)->first(), 173 'files_to_delete' => $files_to_delete, 174 'all_modules_disabled' => $this->module_service->all(true), 175 'all_modules_enabled' => $this->module_service->all(), 176 'deleted_modules' => $this->module_service->deletedModules(), 177 'analytics_modules_disabled' => $this->module_service->findByInterface(ModuleAnalyticsInterface::class, true), 178 'analytics_modules_enabled' => $this->module_service->findByInterface(ModuleAnalyticsInterface::class), 179 'block_modules_disabled' => $this->module_service->findByInterface(ModuleBlockInterface::class, true), 180 'block_modules_enabled' => $this->module_service->findByInterface(ModuleBlockInterface::class), 181 'chart_modules_disabled' => $this->module_service->findByInterface(ModuleChartInterface::class, true), 182 'chart_modules_enabled' => $this->module_service->findByInterface(ModuleChartInterface::class), 183 'custom_updates' => $custom_updates, 184 'data_fix_modules_disabled' => $this->module_service->findByInterface(ModuleDataFixInterface::class, true), 185 'data_fix_modules_enabled' => $this->module_service->findByInterface(ModuleDataFixInterface::class), 186 'other_modules' => $this->module_service->otherModules(true), 187 'footer_modules_disabled' => $this->module_service->findByInterface(ModuleFooterInterface::class, true), 188 'footer_modules_enabled' => $this->module_service->findByInterface(ModuleFooterInterface::class), 189 'history_modules_disabled' => $this->module_service->findByInterface(ModuleHistoricEventsInterface::class, true), 190 'history_modules_enabled' => $this->module_service->findByInterface(ModuleHistoricEventsInterface::class), 191 'language_modules_disabled' => $this->module_service->findByInterface(ModuleLanguageInterface::class, true), 192 'language_modules_enabled' => $this->module_service->findByInterface(ModuleLanguageInterface::class), 193 'list_modules_disabled' => $this->module_service->findByInterface(ModuleListInterface::class, true), 194 'list_modules_enabled' => $this->module_service->findByInterface(ModuleListInterface::class), 195 'menu_modules_disabled' => $this->module_service->findByInterface(ModuleMenuInterface::class, true), 196 'menu_modules_enabled' => $this->module_service->findByInterface(ModuleMenuInterface::class), 197 'report_modules_disabled' => $this->module_service->findByInterface(ModuleReportInterface::class, true), 198 'report_modules_enabled' => $this->module_service->findByInterface(ModuleReportInterface::class), 199 'share_modules_disabled' => $this->module_service->findByInterface(ModuleShareInterface::class, true), 200 'share_modules_enabled' => $this->module_service->findByInterface(ModuleShareInterface::class), 201 'sidebar_modules_disabled' => $this->module_service->findByInterface(ModuleSidebarInterface::class, true), 202 'sidebar_modules_enabled' => $this->module_service->findByInterface(ModuleSidebarInterface::class), 203 'tab_modules_disabled' => $this->module_service->findByInterface(ModuleTabInterface::class, true), 204 'tab_modules_enabled' => $this->module_service->findByInterface(ModuleTabInterface::class), 205 'theme_modules_disabled' => $this->module_service->findByInterface(ModuleThemeInterface::class, true), 206 'theme_modules_enabled' => $this->module_service->findByInterface(ModuleThemeInterface::class), 207 'show_synchronize' => $gedcom_file_count >= $multiple_tree_threshold, 208 ]); 209 } 210 211 /** 212 * Count the number of pending changes in each tree. 213 * 214 * @return array<string> 215 */ 216 private function totalChanges(): array 217 { 218 return DB::table('gedcom') 219 ->leftJoin('change', static function (JoinClause $join): void { 220 $join 221 ->on('change.gedcom_id', '=', 'gedcom.gedcom_id') 222 ->where('change.status', '=', 'pending'); 223 }) 224 ->groupBy(['gedcom.gedcom_id']) 225 ->pluck(new Expression('COUNT(change_id) AS aggregate'), 'gedcom.gedcom_id') 226 ->all(); 227 } 228 229 /** 230 * Count the number of individuals in each tree. 231 * 232 * @return Collection<string,int> 233 */ 234 private function totalIndividuals(): Collection 235 { 236 return DB::table('gedcom') 237 ->leftJoin('individuals', 'i_file', '=', 'gedcom_id') 238 ->groupBy(['gedcom_id']) 239 ->pluck(new Expression('COUNT(i_id) AS aggregate'), 'gedcom_id') 240 ->map(static function (string $count) { 241 return (int) $count; 242 }); 243 } 244 245 /** 246 * Count the number of families in each tree. 247 * 248 * @return Collection<string,int> 249 */ 250 private function totalFamilies(): Collection 251 { 252 return DB::table('gedcom') 253 ->leftJoin('families', 'f_file', '=', 'gedcom_id') 254 ->groupBy(['gedcom_id']) 255 ->pluck(new Expression('COUNT(f_id) AS aggregate'), 'gedcom_id') 256 ->map(static function (string $count) { 257 return (int) $count; 258 }); 259 } 260 261 /** 262 * Count the number of sources in each tree. 263 * 264 * @return Collection<string,int> 265 */ 266 private function totalSources(): Collection 267 { 268 return DB::table('gedcom') 269 ->leftJoin('sources', 's_file', '=', 'gedcom_id') 270 ->groupBy(['gedcom_id']) 271 ->pluck(new Expression('COUNT(s_id) AS aggregate'), 'gedcom_id') 272 ->map(static function (string $count) { 273 return (int) $count; 274 }); 275 } 276 277 /** 278 * Count the number of media objects in each tree. 279 * 280 * @return Collection<string,int> 281 */ 282 private function totalMediaObjects(): Collection 283 { 284 return DB::table('gedcom') 285 ->leftJoin('media', 'm_file', '=', 'gedcom_id') 286 ->groupBy(['gedcom_id']) 287 ->pluck(new Expression('COUNT(m_id) AS aggregate'), 'gedcom_id') 288 ->map(static function (string $count) { 289 return (int) $count; 290 }); 291 } 292 293 /** 294 * Count the number of repositorie in each tree. 295 * 296 * @return Collection<string,int> 297 */ 298 private function totalRepositories(): Collection 299 { 300 return DB::table('gedcom') 301 ->leftJoin('other', static function (JoinClause $join): void { 302 $join 303 ->on('o_file', '=', 'gedcom_id') 304 ->where('o_type', '=', Repository::RECORD_TYPE); 305 }) 306 ->groupBy(['gedcom_id']) 307 ->pluck(new Expression('COUNT(o_id) AS aggregate'), 'gedcom_id') 308 ->map(static function (string $count) { 309 return (int) $count; 310 }); 311 } 312 313 /** 314 * Count the number of notes in each tree. 315 * 316 * @return Collection<string,int> 317 */ 318 private function totalNotes(): Collection 319 { 320 return DB::table('gedcom') 321 ->leftJoin('other', static function (JoinClause $join): void { 322 $join 323 ->on('o_file', '=', 'gedcom_id') 324 ->where('o_type', '=', Note::RECORD_TYPE); 325 }) 326 ->groupBy(['gedcom_id']) 327 ->pluck(new Expression('COUNT(o_id) AS aggregate'), 'gedcom_id') 328 ->map(static function (string $count) { 329 return (int) $count; 330 }); 331 } 332 333 /** 334 * Count the number of submitters in each tree. 335 * 336 * @return Collection<string,int> 337 */ 338 private function totalSubmitters(): Collection 339 { 340 return DB::table('gedcom') 341 ->leftJoin('other', static function (JoinClause $join): void { 342 $join 343 ->on('o_file', '=', 'gedcom_id') 344 ->where('o_type', '=', Submitter::RECORD_TYPE); 345 }) 346 ->groupBy(['gedcom_id']) 347 ->pluck(new Expression('COUNT(o_id) AS aggregate'), 'gedcom_id') 348 ->map(static function (string $count) { 349 return (int) $count; 350 }); 351 } 352} 353