1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees\Services; 19 20use Closure; 21use Fisharebest\Webtrees\Auth; 22use Fisharebest\Webtrees\FlashMessages; 23use Fisharebest\Webtrees\I18N; 24use Fisharebest\Webtrees\Module\AhnentafelReportModule; 25use Fisharebest\Webtrees\Module\AlbumModule; 26use Fisharebest\Webtrees\Module\AncestorsChartModule; 27use Fisharebest\Webtrees\Module\BatchUpdateModule; 28use Fisharebest\Webtrees\Module\BingWebmasterToolsModule; 29use Fisharebest\Webtrees\Module\BirthDeathMarriageReportModule; 30use Fisharebest\Webtrees\Module\BirthReportModule; 31use Fisharebest\Webtrees\Module\CalendarMenuModule; 32use Fisharebest\Webtrees\Module\CemeteryReportModule; 33use Fisharebest\Webtrees\Module\CensusAssistantModule; 34use Fisharebest\Webtrees\Module\ChangeReportModule; 35use Fisharebest\Webtrees\Module\ChartsBlockModule; 36use Fisharebest\Webtrees\Module\ChartsMenuModule; 37use Fisharebest\Webtrees\Module\CkeditorModule; 38use Fisharebest\Webtrees\Module\ClippingsCartModule; 39use Fisharebest\Webtrees\Module\CloudsTheme; 40use Fisharebest\Webtrees\Module\ColorsTheme; 41use Fisharebest\Webtrees\Module\CompactTreeChartModule; 42use Fisharebest\Webtrees\Module\ContactsFooterModule; 43use Fisharebest\Webtrees\Module\CookieWarningModule; 44use Fisharebest\Webtrees\Module\DeathReportModule; 45use Fisharebest\Webtrees\Module\DescendancyChartModule; 46use Fisharebest\Webtrees\Module\DescendancyModule; 47use Fisharebest\Webtrees\Module\DescendancyReportModule; 48use Fisharebest\Webtrees\Module\ExtraInformationModule; 49use Fisharebest\Webtrees\Module\FabTheme; 50use Fisharebest\Webtrees\Module\FactSourcesReportModule; 51use Fisharebest\Webtrees\Module\FamilyBookChartModule; 52use Fisharebest\Webtrees\Module\FamilyGroupReportModule; 53use Fisharebest\Webtrees\Module\FamilyNavigatorModule; 54use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule; 55use Fisharebest\Webtrees\Module\FamilyTreeNewsModule; 56use Fisharebest\Webtrees\Module\FamilyTreeStatisticsModule; 57use Fisharebest\Webtrees\Module\FanChartModule; 58use Fisharebest\Webtrees\Module\FrequentlyAskedQuestionsModule; 59use Fisharebest\Webtrees\Module\GoogleAnalyticsModule; 60use Fisharebest\Webtrees\Module\GoogleWebmasterToolsModule; 61use Fisharebest\Webtrees\Module\HitCountFooterModule; 62use Fisharebest\Webtrees\Module\HourglassChartModule; 63use Fisharebest\Webtrees\Module\HtmlBlockModule; 64use Fisharebest\Webtrees\Module\IndividualFactsTabModule; 65use Fisharebest\Webtrees\Module\IndividualFamiliesReportModule; 66use Fisharebest\Webtrees\Module\IndividualReportModule; 67use Fisharebest\Webtrees\Module\InteractiveTreeModule; 68use Fisharebest\Webtrees\Module\LifespansChartModule; 69use Fisharebest\Webtrees\Module\ListsMenuModule; 70use Fisharebest\Webtrees\Module\LoggedInUsersModule; 71use Fisharebest\Webtrees\Module\LoginBlockModule; 72use Fisharebest\Webtrees\Module\MarriageReportModule; 73use Fisharebest\Webtrees\Module\MatomoAnalyticsModule; 74use Fisharebest\Webtrees\Module\MediaTabModule; 75use Fisharebest\Webtrees\Module\MinimalTheme; 76use Fisharebest\Webtrees\Module\MissingFactsReportModule; 77use Fisharebest\Webtrees\Module\ModuleBlockInterface; 78use Fisharebest\Webtrees\Module\ModuleChartInterface; 79use Fisharebest\Webtrees\Module\ModuleCustomInterface; 80use Fisharebest\Webtrees\Module\ModuleFooterInterface; 81use Fisharebest\Webtrees\Module\ModuleInterface; 82use Fisharebest\Webtrees\Module\ModuleMenuInterface; 83use Fisharebest\Webtrees\Module\ModuleReportInterface; 84use Fisharebest\Webtrees\Module\ModuleSidebarInterface; 85use Fisharebest\Webtrees\Module\ModuleTabInterface; 86use Fisharebest\Webtrees\Module\NotesTabModule; 87use Fisharebest\Webtrees\Module\OccupationReportModule; 88use Fisharebest\Webtrees\Module\OnThisDayModule; 89use Fisharebest\Webtrees\Module\PedigreeChartModule; 90use Fisharebest\Webtrees\Module\PedigreeMapModule; 91use Fisharebest\Webtrees\Module\PedigreeReportModule; 92use Fisharebest\Webtrees\Module\PlacesModule; 93use Fisharebest\Webtrees\Module\PoweredByWebtreesModule; 94use Fisharebest\Webtrees\Module\RecentChangesModule; 95use Fisharebest\Webtrees\Module\RelatedIndividualsReportModule; 96use Fisharebest\Webtrees\Module\RelationshipsChartModule; 97use Fisharebest\Webtrees\Module\RelativesTabModule; 98use Fisharebest\Webtrees\Module\ReportsMenuModule; 99use Fisharebest\Webtrees\Module\ResearchTaskModule; 100use Fisharebest\Webtrees\Module\ReviewChangesModule; 101use Fisharebest\Webtrees\Module\SearchMenuModule; 102use Fisharebest\Webtrees\Module\SiteMapModule; 103use Fisharebest\Webtrees\Module\SlideShowModule; 104use Fisharebest\Webtrees\Module\SourcesTabModule; 105use Fisharebest\Webtrees\Module\StatcounterModule; 106use Fisharebest\Webtrees\Module\StatisticsChartModule; 107use Fisharebest\Webtrees\Module\StoriesModule; 108use Fisharebest\Webtrees\Module\ThemeSelectModule; 109use Fisharebest\Webtrees\Module\TimelineChartModule; 110use Fisharebest\Webtrees\Module\TopGivenNamesModule; 111use Fisharebest\Webtrees\Module\TopPageViewsModule; 112use Fisharebest\Webtrees\Module\TopSurnamesModule; 113use Fisharebest\Webtrees\Module\TreesMenuModule; 114use Fisharebest\Webtrees\Module\UpcomingAnniversariesModule; 115use Fisharebest\Webtrees\Module\UserFavoritesModule; 116use Fisharebest\Webtrees\Module\UserJournalModule; 117use Fisharebest\Webtrees\Module\UserMessagesModule; 118use Fisharebest\Webtrees\Module\UserWelcomeModule; 119use Fisharebest\Webtrees\Module\WebtreesTheme; 120use Fisharebest\Webtrees\Module\WelcomeBlockModule; 121use Fisharebest\Webtrees\Module\XeneaTheme; 122use Fisharebest\Webtrees\Module\YahrzeitModule; 123use Fisharebest\Webtrees\Tree; 124use Fisharebest\Webtrees\User; 125use Fisharebest\Webtrees\Webtrees; 126use Illuminate\Database\Capsule\Manager as DB; 127use Illuminate\Support\Collection; 128use Illuminate\Support\Str; 129use stdClass; 130use Throwable; 131 132/** 133 * Functions for managing and maintaining modules. 134 */ 135class ModuleService 136{ 137 // Some types of module have different access levels in different trees. 138 private const COMPONENTS = [ 139 'block' => ModuleBlockInterface::class, 140 'chart' => ModuleChartInterface::class, 141 'menu' => ModuleMenuInterface::class, 142 'report' => ModuleReportInterface::class, 143 'sidebar' => ModuleSidebarInterface::class, 144 'tab' => ModuleTabInterface::class, 145 ]; 146 147 // Array keys are module names, and should match module names from earlier versions of webtrees. 148 private const CORE_MODULES = [ 149 'GEDFact_assistant' => CensusAssistantModule::class, 150 'ahnentafel_report' => AhnentafelReportModule::class, 151 'ancestors_chart' => AncestorsChartModule::class, 152 'batch_update' => BatchUpdateModule::class, 153 'bdm_report' => BirthDeathMarriageReportModule::class, 154 'bing-webmaster-tools' => BingWebmasterToolsModule::class, 155 'birth_report' => BirthReportModule::class, 156 'calendar-menu' => CalendarMenuModule::class, 157 'cemetery_report' => CemeteryReportModule::class, 158 'change_report' => ChangeReportModule::class, 159 'charts' => ChartsBlockModule::class, 160 'charts-menu' => ChartsMenuModule::class, 161 'ckeditor' => CkeditorModule::class, 162 'clippings' => ClippingsCartModule::class, 163 'clouds' => CloudsTheme::class, 164 'colors' => ColorsTheme::class, 165 'compact-chart' => CompactTreeChartModule::class, 166 'contact-links' => ContactsFooterModule::class, 167 'cookie-warning' => CookieWarningModule::class, 168 'death_report' => DeathReportModule::class, 169 'descendancy' => DescendancyModule::class, 170 'descendancy_chart' => DescendancyChartModule::class, 171 'descendancy_report' => DescendancyReportModule::class, 172 'extra_info' => ExtraInformationModule::class, 173 'fab' => FabTheme::class, 174 'fact_sources' => FactSourcesReportModule::class, 175 'family_book_chart' => FamilyBookChartModule::class, 176 'family_group_report' => FamilyGroupReportModule::class, 177 'family_nav' => FamilyNavigatorModule::class, 178 'fan_chart' => FanChartModule::class, 179 'faq' => FrequentlyAskedQuestionsModule::class, 180 'gedcom_block' => WelcomeBlockModule::class, 181 'gedcom_favorites' => FamilyTreeFavoritesModule::class, 182 'gedcom_news' => FamilyTreeNewsModule::class, 183 'gedcom_stats' => FamilyTreeStatisticsModule::class, 184 'google-analytics' => GoogleAnalyticsModule::class, 185 'google-webmaster-tools' => GoogleWebmasterToolsModule::class, 186 'hit-counter' => HitCountFooterModule::class, 187 'hourglass_chart' => HourglassChartModule::class, 188 'html' => HtmlBlockModule::class, 189 'individual_ext_report' => IndividualFamiliesReportModule::class, 190 'individual_report' => IndividualReportModule::class, 191 'lifespans_chart' => LifespansChartModule::class, 192 'lightbox' => AlbumModule::class, 193 'lists-menu' => ListsMenuModule::class, 194 'logged_in' => LoggedInUsersModule::class, 195 'login_block' => LoginBlockModule::class, 196 'marriage_report' => MarriageReportModule::class, 197 'matomo-analytics' => MatomoAnalyticsModule::class, 198 'media' => MediaTabModule::class, 199 'minimal' => MinimalTheme::class, 200 'missing_facts_report' => MissingFactsReportModule::class, 201 'notes' => NotesTabModule::class, 202 'occupation_report' => OccupationReportModule::class, 203 'pedigree-map' => PedigreeMapModule::class, 204 'pedigree_chart' => PedigreeChartModule::class, 205 'pedigree_report' => PedigreeReportModule::class, 206 'personal_facts' => IndividualFactsTabModule::class, 207 'places' => PlacesModule::class, 208 'powered-by-webtrees' => PoweredByWebtreesModule::class, 209 'random_media' => SlideShowModule::class, 210 'recent_changes' => RecentChangesModule::class, 211 'relationships_chart' => RelationshipsChartModule::class, 212 'relative_ext_report' => RelatedIndividualsReportModule::class, 213 'relatives' => RelativesTabModule::class, 214 'reports-menu' => ReportsMenuModule::class, 215 'review_changes' => ReviewChangesModule::class, 216 'search-menu' => SearchMenuModule::class, 217 'sitemap' => SiteMapModule::class, 218 'sources_tab' => SourcesTabModule::class, 219 'statcounter' => StatcounterModule::class, 220 'statistics_chart' => StatisticsChartModule::class, 221 'stories' => StoriesModule::class, 222 'theme_select' => ThemeSelectModule::class, 223 'timeline_chart' => TimelineChartModule::class, 224 'todays_events' => OnThisDayModule::class, 225 'todo' => ResearchTaskModule::class, 226 'top10_givnnames' => TopGivenNamesModule::class, 227 'top10_pageviews' => TopPageViewsModule::class, 228 'top10_surnames' => TopSurnamesModule::class, 229 'tree' => InteractiveTreeModule::class, 230 'trees-menu' => TreesMenuModule::class, 231 'upcoming_events' => UpcomingAnniversariesModule::class, 232 'user_blog' => UserJournalModule::class, 233 'user_favorites' => UserFavoritesModule::class, 234 'user_messages' => UserMessagesModule::class, 235 'user_welcome' => UserWelcomeModule::class, 236 'webtrees' => WebtreesTheme::class, 237 'xenea' => XeneaTheme::class, 238 'yahrzeit' => YahrzeitModule::class, 239 ]; 240 241 /** 242 * All core modules in the system. 243 * 244 * @return Collection 245 */ 246 private function coreModules(): Collection 247 { 248 $modules = new Collection(self::CORE_MODULES); 249 250 return $modules->map(function (string $class, string $name): ModuleInterface { 251 $module = app()->make($class); 252 253 $module->setName($name); 254 255 return $module; 256 }); 257 } 258 259 /** 260 * All custom modules in the system. Custom modules are defined in modules_v4/ 261 * 262 * @return Collection 263 */ 264 private function customModules(): Collection 265 { 266 $pattern = WT_ROOT . Webtrees::MODULES_PATH . '*/module.php'; 267 $filenames = glob($pattern); 268 269 return (new Collection($filenames)) 270 ->filter(function (string $filename): bool { 271 // Special characters will break PHP variable names. 272 // This also allows us to ignore modules called "foo.example" and "foo.disable" 273 $module_name = basename(dirname($filename)); 274 275 return !Str::contains($module_name, ['.', ' ', '[', ']']) && Str::length($module_name) <= 30; 276 }) 277 ->map(function (string $filename): ?ModuleCustomInterface { 278 try { 279 $module = self::load($filename); 280 281 if ($module instanceof ModuleCustomInterface) { 282 $module_name = '_' . basename(dirname($filename)) . '_'; 283 284 $module->setName($module_name); 285 } else { 286 return null; 287 } 288 289 return $module; 290 } catch (Throwable $ex) { 291 $message = '<pre>' . e($ex->getMessage()) . "\n" . e($ex->getTraceAsString()) . '</pre>'; 292 FlashMessages::addMessage($message, 'danger'); 293 294 return null; 295 } 296 }) 297 ->filter(); 298 } 299 300 /** 301 * All modules. 302 * 303 * @return Collection|ModuleInterface[] 304 */ 305 public function all(): Collection 306 { 307 return app('cache.array')->rememberForever('all_modules', function (): Collection { 308 // Modules have a default status, order etc. 309 // We can override these from database settings. 310 $module_info = DB::table('module') 311 ->get() 312 ->mapWithKeys(function (stdClass $row): array { 313 return [$row->module_name => $row]; 314 }); 315 316 return self::coreModules() 317 ->merge(self::customModules()) 318 ->map(function (ModuleInterface $module) use ($module_info): ModuleInterface { 319 $info = $module_info->get($module->name()); 320 321 if ($info instanceof stdClass) { 322 $module->setEnabled($info->status === 'enabled'); 323 324 if ($module instanceof ModuleFooterInterface && $info->footer_order !== null) { 325 $module->setFooterOrder((int) $info->footer_order); 326 } 327 328 if ($module instanceof ModuleMenuInterface && $info->menu_order !== null) { 329 $module->setMenuOrder((int) $info->menu_order); 330 } 331 332 if ($module instanceof ModuleSidebarInterface && $info->sidebar_order !== null) { 333 $module->setSidebarOrder((int) $info->sidebar_order); 334 } 335 336 if ($module instanceof ModuleTabInterface && $info->tab_order !== null) { 337 $module->setTabOrder((int) $info->tab_order); 338 } 339 } else { 340 DB::table('module')->insert(['module_name' => $module->name()]); 341 } 342 343 return $module; 344 }) 345 ->sort(self::moduleSorter()); 346 }); 347 } 348 349 /** 350 * Load a module in a separate scope, to prevent it from modifying local variables. 351 * 352 * @param string $filename 353 * 354 * @return mixed 355 */ 356 private function load(string $filename) 357 { 358 return include $filename; 359 } 360 361 /** 362 * A function to sort modules by name 363 * 364 * @return Closure 365 */ 366 private function moduleSorter(): Closure 367 { 368 return function (ModuleInterface $x, ModuleInterface $y): int { 369 return I18N::strcasecmp($x->title(), $y->title()); 370 }; 371 } 372 373 /** 374 * A function to sort footers 375 * 376 * @return Closure 377 */ 378 private function footerSorter(): Closure 379 { 380 return function (ModuleFooterInterface $x, ModuleFooterInterface $y): int { 381 return $x->getFooterOrder() <=> $y->getFooterOrder(); 382 }; 383 } 384 385 /** 386 * A function to sort menus 387 * 388 * @return Closure 389 */ 390 private function menuSorter(): Closure 391 { 392 return function (ModuleMenuInterface $x, ModuleMenuInterface $y): int { 393 return $x->getMenuOrder() <=> $y->getMenuOrder(); 394 }; 395 } 396 397 /** 398 * A function to sort menus 399 * 400 * @return Closure 401 */ 402 private function sidebarSorter(): Closure 403 { 404 return function (ModuleSidebarInterface $x, ModuleSidebarInterface $y): int { 405 return $x->getSidebarOrder() <=> $y->getSidebarOrder(); 406 }; 407 } 408 409 /** 410 * A function to sort menus 411 * 412 * @return Closure 413 */ 414 private function tabSorter(): Closure 415 { 416 return function (ModuleTabInterface $x, ModuleTabInterface $y): int { 417 return $x->getTabOrder() <=> $y->getTabOrder(); 418 }; 419 } 420 421 /** 422 * Modules which (a) provide a specific function and (b) we have permission to see. 423 * 424 * @param string $component 425 * @param Tree $tree 426 * @param User $user 427 * 428 * @return Collection|ModuleInterface[] 429 */ 430 public function findByComponent(string $component, Tree $tree, User $user): Collection 431 { 432 $interface = self::COMPONENTS[$component]; 433 434 return self::findByInterface($interface) 435 ->filter(function (ModuleInterface $module) use ($component, $tree, $user): bool { 436 return $module->accessLevel($tree, $component) >= Auth::accessLevel($tree, $user); 437 }); 438 } 439 440 /** 441 * All modules which provide a specific function. 442 * 443 * @param string $interface 444 * @param bool $include_disabled 445 * 446 * @return Collection|ModuleInterface[] 447 */ 448 public function findByInterface(string $interface, $include_disabled = false): Collection 449 { 450 $modules = self::all() 451 ->filter(function (ModuleInterface $module) use ($interface): bool { 452 return $module instanceof $interface; 453 }) 454 ->filter(function (ModuleInterface $module) use ($include_disabled): bool { 455 return $include_disabled || $module->isEnabled(); 456 }); 457 458 switch ($interface) { 459 case ModuleFooterInterface::class: 460 return $modules->sort(self::footerSorter()); 461 462 case ModuleMenuInterface::class: 463 return $modules->sort(self::menuSorter()); 464 465 case ModuleSidebarInterface::class: 466 return $modules->sort(self::sidebarSorter()); 467 468 case ModuleTabInterface::class: 469 return $modules->sort(self::tabSorter()); 470 } 471 472 return $modules; 473 } 474 475 /** 476 * Find a specified module, if it is currently active. 477 * 478 * @param string $module_name 479 * 480 * @return ModuleInterface|null 481 */ 482 public function findByName(string $module_name): ?ModuleInterface 483 { 484 return self::all() 485 ->filter(function (ModuleInterface $module) use ($module_name): bool { 486 return $module->isEnabled() && $module->name() === $module_name; 487 }) 488 ->first(); 489 } 490 491 /** 492 * Find a specified module, if it is currently active. 493 * 494 * @param string $class_name 495 * 496 * @return ModuleInterface|null 497 */ 498 public function findByClass(string $class_name): ?ModuleInterface 499 { 500 return self::all() 501 ->filter(function (ModuleInterface $module) use ($class_name): bool { 502 return $module->isEnabled() && $module instanceof $class_name; 503 }) 504 ->first(); 505 } 506} 507