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