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