1<?php 2/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ 3 4namespace Icinga\Application\Modules; 5 6use Exception; 7use Icinga\Application\ApplicationBootstrap; 8use Icinga\Application\Config; 9use Icinga\Application\Hook; 10use Icinga\Application\Icinga; 11use Icinga\Application\Logger; 12use Icinga\Exception\IcingaException; 13use Icinga\Exception\ProgrammingError; 14use Icinga\Module\Setup\SetupWizard; 15use Icinga\Util\File; 16use Icinga\Util\Translator; 17use Icinga\Web\Navigation\Navigation; 18use Icinga\Web\Widget; 19use RecursiveDirectoryIterator; 20use RecursiveIteratorIterator; 21use Zend_Controller_Router_Route; 22use Zend_Controller_Router_Route_Abstract; 23use Zend_Controller_Router_Route_Regex; 24 25/** 26 * Module handling 27 * 28 * Register modules and initialize it 29 */ 30class Module 31{ 32 /** 33 * Module name 34 * 35 * @var string 36 */ 37 private $name; 38 39 /** 40 * Base directory of module 41 * 42 * @var string 43 */ 44 private $basedir; 45 46 /** 47 * Directory for assets 48 * 49 * @var string 50 */ 51 private $assetDir; 52 53 /** 54 * Directory for styles 55 * 56 * @var string 57 */ 58 private $cssdir; 59 60 /** 61 * Base application directory 62 * 63 * @var string 64 */ 65 private $appdir; 66 67 /** 68 * Library directory 69 * 70 * @var string 71 */ 72 private $libdir; 73 74 /** 75 * Directory containing translations 76 * 77 * @var string 78 */ 79 private $localedir; 80 81 /** 82 * Directory where controllers reside 83 * 84 * @var string 85 */ 86 private $controllerdir; 87 88 /** 89 * Directory containing form implementations 90 * 91 * @var string 92 */ 93 private $formdir; 94 95 /** 96 * Module bootstrapping script 97 * 98 * @var string 99 */ 100 private $runScript; 101 102 /** 103 * Module configuration script 104 * 105 * @var string 106 */ 107 private $configScript; 108 109 /** 110 * Module metadata filename 111 * 112 * @var string 113 */ 114 private $metadataFile; 115 116 /** 117 * Module metadata (version...) 118 * 119 * @var object 120 */ 121 private $metadata; 122 123 /** 124 * Whether we already tried to include the module configuration script 125 * 126 * @var bool 127 */ 128 private $triedToLaunchConfigScript = false; 129 130 /** 131 * Whether the module's namespaces have been registered on our autoloader 132 * 133 * @var bool 134 */ 135 protected $registeredAutoloader = false; 136 137 /** 138 * Whether this module has been registered 139 * 140 * @var bool 141 */ 142 private $registered = false; 143 144 /** 145 * Provided permissions 146 * 147 * @var array 148 */ 149 private $permissionList = array(); 150 151 /** 152 * Provided restrictions 153 * 154 * @var array 155 */ 156 private $restrictionList = array(); 157 158 /** 159 * Provided config tabs 160 * 161 * @var array 162 */ 163 private $configTabs = array(); 164 165 /** 166 * Provided setup wizard 167 * 168 * @var string 169 */ 170 private $setupWizard; 171 172 /** 173 * Icinga application 174 * 175 * @var \Icinga\Application\Web 176 */ 177 private $app; 178 179 /** 180 * The CSS/LESS files this module provides 181 * 182 * @var array 183 */ 184 protected $cssFiles = array(); 185 186 /** 187 * The Javascript files this module provides 188 * 189 * @var array 190 */ 191 protected $jsFiles = array(); 192 193 /** 194 * Globally provided CSS assets 195 * 196 * @var array 197 */ 198 protected $cssAssets = []; 199 200 /** 201 * Globally provided JS assets 202 * 203 * @var array 204 */ 205 protected $jsAssets = []; 206 207 /** 208 * Required CSS assets 209 * 210 * @var array 211 */ 212 protected $cssRequires = []; 213 214 /** 215 * Required JS assets 216 * 217 * @var array 218 */ 219 protected $jsRequires = []; 220 221 /** 222 * Routes to add to the route chain 223 * 224 * @var array Array of name-route pairs 225 * 226 * @see addRoute() 227 */ 228 protected $routes = array(); 229 230 /** 231 * A set of menu elements 232 * 233 * @var MenuItemContainer[] 234 */ 235 protected $menuItems = array(); 236 237 /** 238 * A set of Pane elements 239 * 240 * @var array 241 */ 242 protected $paneItems = array(); 243 244 /** 245 * A set of objects representing a searchUrl configuration 246 * 247 * @var array 248 */ 249 protected $searchUrls = array(); 250 251 /** 252 * This module's user backends providing several authentication mechanisms 253 * 254 * @var array 255 */ 256 protected $userBackends = array(); 257 258 /** 259 * This module's user group backends 260 * 261 * @var array 262 */ 263 protected $userGroupBackends = array(); 264 265 /** 266 * This module's configurable navigation items 267 * 268 * @var array 269 */ 270 protected $navigationItems = array(); 271 272 /** 273 * Create a new module object 274 * 275 * @param ApplicationBootstrap $app 276 * @param string $name 277 * @param string $basedir 278 */ 279 public function __construct(ApplicationBootstrap $app, $name, $basedir) 280 { 281 $this->app = $app; 282 $this->name = $name; 283 $this->basedir = $basedir; 284 $this->assetDir = $basedir . '/asset'; 285 $this->cssdir = $basedir . '/public/css'; 286 $this->jsdir = $basedir . '/public/js'; 287 $this->libdir = $basedir . '/library'; 288 $this->configdir = $app->getConfigDir('modules/' . $name); 289 $this->appdir = $basedir . '/application'; 290 $this->localedir = $basedir . '/application/locale'; 291 $this->formdir = $basedir . '/application/forms'; 292 $this->controllerdir = $basedir . '/application/controllers'; 293 $this->runScript = $basedir . '/run.php'; 294 $this->configScript = $basedir . '/configuration.php'; 295 $this->metadataFile = $basedir . '/module.info'; 296 } 297 298 /** 299 * Provide a search URL 300 * 301 * @param string $title 302 * @param string $url 303 * @param int $priority 304 * 305 * @return $this 306 */ 307 public function provideSearchUrl($title, $url, $priority = 0) 308 { 309 $this->searchUrls[] = (object) array( 310 'title' => (string) $title, 311 'url' => (string) $url, 312 'priority' => (int) $priority 313 ); 314 315 return $this; 316 } 317 318 /** 319 * Get this module's search urls 320 * 321 * @return array 322 */ 323 public function getSearchUrls() 324 { 325 $this->launchConfigScript(); 326 return $this->searchUrls; 327 } 328 329 /** 330 * Return this module's dashboard 331 * 332 * @return Navigation 333 */ 334 public function getDashboard() 335 { 336 $this->launchConfigScript(); 337 return $this->createDashboard($this->paneItems); 338 } 339 340 /** 341 * Create and return a new navigation for the given dashboard panes 342 * 343 * @param DashboardContainer[] $panes 344 * 345 * @return Navigation 346 */ 347 public function createDashboard(array $panes) 348 { 349 $navigation = new Navigation(); 350 foreach ($panes as $pane) { 351 /** @var DashboardContainer $pane */ 352 $dashlets = []; 353 foreach ($pane->getDashlets() as $dashletName => $dashletConfig) { 354 $dashlets[$dashletName] = [ 355 'label' => $this->translate($dashletName), 356 'url' => $dashletConfig['url'], 357 'priority' => $dashletConfig['priority'] 358 ]; 359 } 360 361 $navigation->addItem( 362 $pane->getName(), 363 array_merge( 364 $pane->getProperties(), 365 array( 366 'label' => $this->translate($pane->getName()), 367 'type' => 'dashboard-pane', 368 'children' => $dashlets 369 ) 370 ) 371 ); 372 } 373 374 return $navigation; 375 } 376 377 /** 378 * Add or get a dashboard pane 379 * 380 * @param string $name 381 * @param array $properties 382 * 383 * @return DashboardContainer 384 */ 385 protected function dashboard($name, array $properties = array()) 386 { 387 if (array_key_exists($name, $this->paneItems)) { 388 $this->paneItems[$name]->setProperties($properties); 389 } else { 390 $this->paneItems[$name] = new DashboardContainer($name, $properties); 391 } 392 393 return $this->paneItems[$name]; 394 } 395 396 /** 397 * Return this module's menu 398 * 399 * @return Navigation 400 */ 401 public function getMenu() 402 { 403 $this->launchConfigScript(); 404 return Navigation::fromArray($this->createMenu($this->menuItems)); 405 } 406 407 /** 408 * Create and return an array structure for the given menu items 409 * 410 * @param MenuItemContainer[] $items 411 * 412 * @return array 413 */ 414 private function createMenu(array $items) 415 { 416 $navigation = array(); 417 foreach ($items as $item) { 418 /** @var MenuItemContainer $item */ 419 $properties = $item->getProperties(); 420 $properties['children'] = $this->createMenu($item->getChildren()); 421 if (! isset($properties['label'])) { 422 $properties['label'] = $this->translate($item->getName()); 423 } 424 425 $navigation[$item->getName()] = $properties; 426 } 427 428 return $navigation; 429 } 430 431 /** 432 * Add or get a menu section 433 * 434 * @param string $name 435 * @param array $properties 436 * 437 * @return MenuItemContainer 438 */ 439 protected function menuSection($name, array $properties = array()) 440 { 441 if (array_key_exists($name, $this->menuItems)) { 442 $this->menuItems[$name]->setProperties($properties); 443 } else { 444 $this->menuItems[$name] = new MenuItemContainer($name, $properties); 445 } 446 447 return $this->menuItems[$name]; 448 } 449 450 /** 451 * Register module 452 * 453 * @return bool 454 */ 455 public function register() 456 { 457 if ($this->registered) { 458 return true; 459 } 460 461 $this->registerAutoloader(); 462 try { 463 $this->launchRunScript(); 464 } catch (Exception $e) { 465 Logger::warning( 466 'Launching the run script %s for module %s failed with the following exception: %s', 467 $this->runScript, 468 $this->name, 469 $e->getMessage() 470 ); 471 return false; 472 } 473 $this->registerWebIntegration(); 474 $this->registerAssets(); 475 $this->registered = true; 476 477 return true; 478 } 479 480 /** 481 * Get whether this module has been registered 482 * 483 * @return bool 484 */ 485 public function isRegistered() 486 { 487 return $this->registered; 488 } 489 490 /** 491 * Test for an enabled module by name 492 * 493 * @param string $name 494 * 495 * @return bool 496 */ 497 public static function exists($name) 498 { 499 return Icinga::app()->getModuleManager()->hasEnabled($name); 500 } 501 502 /** 503 * Get a module by name 504 * 505 * @param string $name 506 * @param bool $autoload 507 * 508 * @return self 509 * 510 * @throws ProgrammingError When the module is not yet loaded 511 */ 512 public static function get($name, $autoload = false) 513 { 514 $manager = Icinga::app()->getModuleManager(); 515 if (!$manager->hasLoaded($name)) { 516 if ($autoload === true && $manager->hasEnabled($name)) { 517 $manager->loadModule($name); 518 } 519 } 520 // Throws ProgrammingError when the module is not yet loaded 521 return $manager->getModule($name); 522 } 523 524 /** 525 * Provide a static CSS asset which can be required by other modules 526 * 527 * @param string $path The path, relative to the module's base 528 * 529 * @return $this 530 */ 531 protected function provideCssAsset($path) 532 { 533 $fullPath = join(DIRECTORY_SEPARATOR, [$this->basedir, $path]); 534 $this->cssAssets[] = $fullPath; 535 $this->cssRequires[] = $fullPath; // A module should not have to require its own assets 536 537 return $this; 538 } 539 540 /** 541 * Get the CSS assets provided by this module 542 * 543 * @return array 544 */ 545 public function getCssAssets() 546 { 547 return $this->cssAssets; 548 } 549 550 /** 551 * Require CSS from a different module 552 * 553 * @param string $path The file's path, relative to the module's asset or base directory 554 * @param string $from The module's name 555 * 556 * @return $this 557 */ 558 protected function requireCssFile($path, $from) 559 { 560 $module = self::get($from); 561 $cssAssetDir = join(DIRECTORY_SEPARATOR, [$module->assetDir, 'css']); 562 foreach ($module->getCssAssets() as $assetPath) { 563 if (substr($assetPath, 0, strlen($cssAssetDir)) === $cssAssetDir) { 564 $relativePath = ltrim(substr($assetPath, strlen($cssAssetDir)), '/\\'); 565 } else { 566 $relativePath = ltrim(substr($assetPath, strlen($module->basedir)), '/\\'); 567 } 568 569 if ($path === $relativePath) { 570 $this->cssRequires[] = $assetPath; 571 break; // Exact match, won't match again.. 572 } elseif (fnmatch($path, $relativePath)) { 573 $this->cssRequires[] = $assetPath; 574 } 575 } 576 577 return $this; 578 } 579 580 /** 581 * Check whether this module requires CSS from a different module 582 * 583 * @return bool 584 */ 585 public function requiresCss() 586 { 587 $this->launchConfigScript(); 588 return ! empty($this->cssRequires); 589 } 590 591 /** 592 * List the CSS assets required by this module 593 * 594 * @return array 595 */ 596 public function getCssRequires() 597 { 598 $this->launchConfigScript(); 599 return $this->cssRequires; 600 } 601 602 /** 603 * Provide a static Javascript asset which can be required by other modules 604 * 605 * @param string $path The path, relative to the module's base 606 * 607 * @return $this 608 */ 609 protected function provideJsAsset($path) 610 { 611 $fullPath = join(DIRECTORY_SEPARATOR, [$this->basedir, $path]); 612 $this->jsAssets[] = $fullPath; 613 $this->jsRequires[] = $fullPath; // A module should not have to require its own assets 614 615 return $this; 616 } 617 618 /** 619 * Get the Javascript assets provided by this module 620 * 621 * @return array 622 */ 623 public function getJsAssets() 624 { 625 return $this->jsAssets; 626 } 627 628 /** 629 * Require Javascript from a different module 630 * 631 * @param string $path The file's path, relative to the module's asset or base directory 632 * @param string $from The module's name 633 * 634 * @return $this 635 */ 636 protected function requireJsFile($path, $from) 637 { 638 $module = self::get($from); 639 $jsAssetDir = join(DIRECTORY_SEPARATOR, [$module->assetDir, 'js']); 640 foreach ($module->getJsAssets() as $assetPath) { 641 if (substr($assetPath, 0, strlen($jsAssetDir)) === $jsAssetDir) { 642 $relativePath = ltrim(substr($assetPath, strlen($jsAssetDir)), '/\\'); 643 } else { 644 $relativePath = ltrim(substr($assetPath, strlen($module->basedir)), '/\\'); 645 } 646 647 if ($path === $relativePath) { 648 $this->jsRequires[] = $assetPath; 649 break; // Exact match, won't match again.. 650 } elseif (fnmatch($path, $relativePath)) { 651 $this->jsRequires[] = $assetPath; 652 } 653 } 654 655 return $this; 656 } 657 658 /** 659 * Check whether this module requires Javascript from a different module 660 * 661 * @return bool 662 */ 663 public function requiresJs() 664 { 665 $this->launchConfigScript(); 666 return ! empty($this->jsRequires); 667 } 668 669 /** 670 * List the Javascript assets required by this module 671 * 672 * @return array 673 */ 674 public function getJsRequires() 675 { 676 $this->launchConfigScript(); 677 return $this->jsRequires; 678 } 679 680 /** 681 * Provide an additional CSS/LESS file 682 * 683 * @param string $path The path to the file, relative to self::$cssdir 684 * 685 * @return $this 686 */ 687 protected function provideCssFile($path) 688 { 689 $this->cssFiles[] = $this->cssdir . DIRECTORY_SEPARATOR . $path; 690 return $this; 691 } 692 693 /** 694 * Test if module provides css 695 * 696 * @return bool 697 */ 698 public function hasCss() 699 { 700 if (file_exists($this->getCssFilename())) { 701 return true; 702 } 703 704 $this->launchConfigScript(); 705 return !empty($this->cssFiles); 706 } 707 708 /** 709 * Returns the complete less file name 710 * 711 * @return string 712 */ 713 public function getCssFilename() 714 { 715 return $this->cssdir . '/module.less'; 716 } 717 718 /** 719 * Return the CSS/LESS files this module provides 720 * 721 * @return array 722 */ 723 public function getCssFiles() 724 { 725 $this->launchConfigScript(); 726 $files = $this->cssFiles; 727 if (file_exists($this->getCssFilename())) { 728 $files[] = $this->getCssFilename(); 729 } 730 return $files; 731 } 732 733 /** 734 * Provide an additional Javascript file 735 * 736 * @param string $path The path to the file, relative to self::$jsdir 737 * 738 * @return $this 739 */ 740 protected function provideJsFile($path) 741 { 742 $this->jsFiles[] = $this->jsdir . DIRECTORY_SEPARATOR . $path; 743 return $this; 744 } 745 746 /** 747 * Test if module provides js 748 * 749 * @return bool 750 */ 751 public function hasJs() 752 { 753 if (file_exists($this->getJsFilename())) { 754 return true; 755 } 756 757 $this->launchConfigScript(); 758 return !empty($this->jsFiles); 759 } 760 761 /** 762 * Returns the complete js file name 763 * 764 * @return string 765 */ 766 public function getJsFilename() 767 { 768 return $this->jsdir . '/module.js'; 769 } 770 771 /** 772 * Return the Javascript files this module provides 773 * 774 * @return array 775 */ 776 public function getJsFiles() 777 { 778 $this->launchConfigScript(); 779 $files = $this->jsFiles; 780 $files[] = $this->getJsFilename(); 781 return $files; 782 } 783 784 /** 785 * Get the module name 786 * 787 * @return string 788 */ 789 public function getName() 790 { 791 return $this->name; 792 } 793 794 /** 795 * Get the module namespace 796 * 797 * @return string 798 */ 799 public function getNamespace() 800 { 801 return 'Icinga\\Module\\' . ucfirst($this->getName()); 802 } 803 804 /** 805 * Get the module version 806 * 807 * @return string 808 */ 809 public function getVersion() 810 { 811 return $this->metadata()->version; 812 } 813 814 /** 815 * Get the module description 816 * 817 * @return string 818 */ 819 public function getDescription() 820 { 821 return $this->metadata()->description; 822 } 823 824 /** 825 * Get the module title (short description) 826 * 827 * @return string 828 */ 829 public function getTitle() 830 { 831 return $this->metadata()->title; 832 } 833 834 /** 835 * Get the module dependencies 836 * 837 * @return array 838 */ 839 public function getDependencies() 840 { 841 return $this->metadata()->depends; 842 } 843 844 /** 845 * Fetch module metadata 846 * 847 * @return object 848 */ 849 protected function metadata() 850 { 851 if ($this->metadata === null) { 852 $metadata = (object) array( 853 'name' => $this->getName(), 854 'version' => '0.0.0', 855 'title' => null, 856 'description' => '', 857 'depends' => array(), 858 ); 859 860 if (file_exists($this->metadataFile)) { 861 $key = null; 862 $file = new File($this->metadataFile, 'r'); 863 foreach ($file as $lineno => $line) { 864 $line = rtrim($line); 865 866 if ($key === 'description') { 867 if (empty($line)) { 868 $metadata->description .= "\n"; 869 continue; 870 } elseif ($line[0] === ' ') { 871 $metadata->description .= $line; 872 continue; 873 } 874 } elseif (empty($line)) { 875 continue; 876 } 877 878 if (strpos($line, ':') === false) { 879 Logger::debug( 880 $this->translate( 881 "Can't process line %d in %s: Line does not specify a key:value pair" 882 . " nor is it part of the description (indented with a single space)" 883 ), 884 $lineno, 885 $this->metadataFile 886 ); 887 888 break; 889 } 890 891 list($key, $val) = preg_split('/:\s+/', $line, 2); 892 $key = lcfirst($key); 893 894 switch ($key) { 895 case 'depends': 896 if (strpos($val, ' ') === false) { 897 $metadata->depends[$val] = true; 898 continue 2; 899 } 900 901 $parts = preg_split('/,\s+/', $val); 902 foreach ($parts as $part) { 903 if (preg_match('/^(\w+)\s+\((.+)\)$/', $part, $m)) { 904 $metadata->depends[$m[1]] = $m[2]; 905 } else { 906 // TODO: FAIL? 907 continue; 908 } 909 } 910 break; 911 912 case 'description': 913 if ($metadata->title === null) { 914 $metadata->title = $val; 915 } else { 916 $metadata->description = $val; 917 } 918 break; 919 920 default: 921 $metadata->{$key} = $val; 922 } 923 } 924 } 925 926 if ($metadata->title === null) { 927 $metadata->title = $this->getName(); 928 } 929 930 if ($metadata->description === '') { 931 // TODO: Check whether the translation module is able to 932 // extract this 933 $metadata->description = t( 934 'This module has no description' 935 ); 936 } 937 938 $this->metadata = $metadata; 939 } 940 return $this->metadata; 941 } 942 943 /** 944 * Get the module's CSS directory 945 * 946 * @return string 947 */ 948 public function getCssDir() 949 { 950 return $this->cssdir; 951 } 952 953 /** 954 * Get the module's controller directory 955 * 956 * @return string 957 */ 958 public function getControllerDir() 959 { 960 return $this->controllerdir; 961 } 962 963 /** 964 * Get the module's base directory 965 * 966 * @return string 967 */ 968 public function getBaseDir() 969 { 970 return $this->basedir; 971 } 972 973 /** 974 * Get the module's application directory 975 * 976 * @return string 977 */ 978 public function getApplicationDir() 979 { 980 return $this->appdir; 981 } 982 983 /** 984 * Get the module's library directory 985 * 986 * @return string 987 */ 988 public function getLibDir() 989 { 990 return $this->libdir; 991 } 992 993 /** 994 * Get the module's configuration directory 995 * 996 * @return string 997 */ 998 public function getConfigDir() 999 { 1000 return $this->configdir; 1001 } 1002 1003 /** 1004 * Get the module's form directory 1005 * 1006 * @return string 1007 */ 1008 public function getFormDir() 1009 { 1010 return $this->formdir; 1011 } 1012 1013 /** 1014 * Get the module config 1015 * 1016 * @param string $file 1017 * 1018 * @return Config 1019 */ 1020 public function getConfig($file = 'config') 1021 { 1022 return $this->app->getConfig()->module($this->name, $file); 1023 } 1024 1025 /** 1026 * Get provided permissions 1027 * 1028 * @return array 1029 */ 1030 public function getProvidedPermissions() 1031 { 1032 $this->launchConfigScript(); 1033 return $this->permissionList; 1034 } 1035 1036 /** 1037 * Get provided restrictions 1038 * 1039 * @return array 1040 */ 1041 public function getProvidedRestrictions() 1042 { 1043 $this->launchConfigScript(); 1044 return $this->restrictionList; 1045 } 1046 1047 /** 1048 * Whether the module provides the given restriction 1049 * 1050 * @param string $name Restriction name 1051 * 1052 * @return bool 1053 */ 1054 public function providesRestriction($name) 1055 { 1056 $this->launchConfigScript(); 1057 return array_key_exists($name, $this->restrictionList); 1058 } 1059 1060 /** 1061 * Whether the module provides the given permission 1062 * 1063 * @param string $name Permission name 1064 * 1065 * @return bool 1066 */ 1067 public function providesPermission($name) 1068 { 1069 $this->launchConfigScript(); 1070 return array_key_exists($name, $this->permissionList); 1071 } 1072 1073 /** 1074 * Get the module configuration tabs 1075 * 1076 * @return \Icinga\Web\Widget\Tabs 1077 */ 1078 public function getConfigTabs() 1079 { 1080 $this->launchConfigScript(); 1081 $tabs = Widget::create('tabs'); 1082 /** @var \Icinga\Web\Widget\Tabs $tabs */ 1083 $tabs->add('info', array( 1084 'url' => 'config/module', 1085 'urlParams' => array('name' => $this->getName()), 1086 'label' => 'Module: ' . $this->getName() 1087 )); 1088 1089 if ($this->app->getModuleManager()->hasEnabled($this->name)) { 1090 foreach ($this->configTabs as $name => $config) { 1091 $tabs->add($name, $config); 1092 } 1093 } 1094 1095 return $tabs; 1096 } 1097 1098 /** 1099 * Whether the module provides a setup wizard 1100 * 1101 * @return bool 1102 */ 1103 public function providesSetupWizard() 1104 { 1105 $this->launchConfigScript(); 1106 if (class_exists($this->setupWizard)) { 1107 $wizard = new $this->setupWizard; 1108 return $wizard instanceof SetupWizard; 1109 } 1110 1111 return false; 1112 } 1113 1114 /** 1115 * Get the module's setup wizard 1116 * 1117 * @return SetupWizard 1118 */ 1119 public function getSetupWizard() 1120 { 1121 return new $this->setupWizard; 1122 } 1123 1124 /** 1125 * Get the module's user backends 1126 * 1127 * @return array 1128 */ 1129 public function getUserBackends() 1130 { 1131 $this->launchConfigScript(); 1132 return $this->userBackends; 1133 } 1134 1135 /** 1136 * Get the module's user group backends 1137 * 1138 * @return array 1139 */ 1140 public function getUserGroupBackends() 1141 { 1142 $this->launchConfigScript(); 1143 return $this->userGroupBackends; 1144 } 1145 1146 /** 1147 * Return this module's configurable navigation items 1148 * 1149 * @return array 1150 */ 1151 public function getNavigationItems() 1152 { 1153 $this->launchConfigScript(); 1154 return $this->navigationItems; 1155 } 1156 1157 /** 1158 * Provide a named permission 1159 * 1160 * @param string $name Unique permission name 1161 * @param string $description Permission description 1162 * 1163 * @throws IcingaException If the permission is already provided 1164 */ 1165 protected function providePermission($name, $description) 1166 { 1167 if ($this->providesPermission($name)) { 1168 throw new IcingaException( 1169 'Cannot provide permission "%s" twice', 1170 $name 1171 ); 1172 } 1173 $this->permissionList[$name] = (object) array( 1174 'name' => $name, 1175 'description' => $description 1176 ); 1177 } 1178 1179 /** 1180 * Provide a named restriction 1181 * 1182 * @param string $name Unique restriction name 1183 * @param string $description Restriction description 1184 * 1185 * @throws IcingaException If the restriction is already provided 1186 */ 1187 protected function provideRestriction($name, $description) 1188 { 1189 if ($this->providesRestriction($name)) { 1190 throw new IcingaException( 1191 'Cannot provide restriction "%s" twice', 1192 $name 1193 ); 1194 } 1195 $this->restrictionList[$name] = (object) array( 1196 'name' => $name, 1197 'description' => $description 1198 ); 1199 } 1200 1201 /** 1202 * Provide a module config tab 1203 * 1204 * @param string $name Unique tab name 1205 * @param array $config Tab config 1206 * 1207 * @return $this 1208 * @throws ProgrammingError If $config lacks the key 'url' 1209 */ 1210 protected function provideConfigTab($name, $config = array()) 1211 { 1212 if (! array_key_exists('url', $config)) { 1213 throw new ProgrammingError('A module config tab MUST provide a "url"'); 1214 } 1215 $config['url'] = $this->getName() . '/' . ltrim($config['url'], '/'); 1216 $this->configTabs[$name] = $config; 1217 return $this; 1218 } 1219 1220 /** 1221 * Provide a setup wizard 1222 * 1223 * @param string $className The name of the class 1224 * 1225 * @return $this 1226 */ 1227 protected function provideSetupWizard($className) 1228 { 1229 $this->setupWizard = $className; 1230 return $this; 1231 } 1232 1233 /** 1234 * Provide a user backend capable of authenticating users 1235 * 1236 * @param string $identifier The identifier of the new backend type 1237 * @param string $className The name of the class 1238 * 1239 * @return $this 1240 */ 1241 protected function provideUserBackend($identifier, $className) 1242 { 1243 $this->userBackends[strtolower($identifier)] = $className; 1244 return $this; 1245 } 1246 1247 /** 1248 * Provide a user group backend 1249 * 1250 * @param string $identifier The identifier of the new backend type 1251 * @param string $className The name of the class 1252 * 1253 * @return $this 1254 */ 1255 protected function provideUserGroupBackend($identifier, $className) 1256 { 1257 $this->userGroupBackends[strtolower($identifier)] = $className; 1258 return $this; 1259 } 1260 1261 /** 1262 * Provide a new type of configurable navigation item with a optional label and config filename 1263 * 1264 * @param string $type 1265 * @param string $label 1266 * @param string $config 1267 * 1268 * @return $this 1269 */ 1270 protected function provideNavigationItem($type, $label = null, $config = null) 1271 { 1272 $this->navigationItems[$type] = array( 1273 'label' => $label, 1274 'config' => $config 1275 ); 1276 1277 return $this; 1278 } 1279 1280 /** 1281 * Register module namespaces on our class loader 1282 * 1283 * @return $this 1284 */ 1285 protected function registerAutoloader() 1286 { 1287 if ($this->registeredAutoloader) { 1288 return $this; 1289 } 1290 1291 $moduleName = ucfirst($this->getName()); 1292 1293 $this->app->getLoader()->registerNamespace( 1294 'Icinga\\Module\\' . $moduleName, 1295 $this->getLibDir() . '/'. $moduleName, 1296 $this->getApplicationDir() 1297 ); 1298 1299 $this->registeredAutoloader = true; 1300 1301 return $this; 1302 } 1303 1304 /** 1305 * Register this module's assets 1306 * 1307 * @return $this 1308 */ 1309 protected function registerAssets() 1310 { 1311 if (! is_dir($this->assetDir)) { 1312 return $this; 1313 } 1314 1315 $listAssets = function ($type) { 1316 $dir = join(DIRECTORY_SEPARATOR, [$this->assetDir, $type]); 1317 if (! is_dir($dir)) { 1318 return []; 1319 } 1320 1321 return new RecursiveIteratorIterator(new RecursiveDirectoryIterator( 1322 $dir, 1323 RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS 1324 )); 1325 }; 1326 1327 foreach ($listAssets('css') as $assetPath) { 1328 $this->provideCssAsset(ltrim(substr($assetPath, strlen($this->basedir)), '/\\')); 1329 } 1330 1331 foreach ($listAssets('js') as $assetPath) { 1332 $this->provideJsAsset(ltrim(substr($assetPath, strlen($this->basedir)), '/\\')); 1333 } 1334 1335 return $this; 1336 } 1337 1338 /** 1339 * Bind text domain for i18n 1340 * 1341 * @return $this 1342 */ 1343 protected function registerLocales() 1344 { 1345 if ($this->hasLocales()) { 1346 Translator::registerDomain($this->name, $this->localedir); 1347 } 1348 return $this; 1349 } 1350 1351 /** 1352 * Get whether the module has translations 1353 */ 1354 public function hasLocales() 1355 { 1356 return file_exists($this->localedir) && is_dir($this->localedir); 1357 } 1358 1359 /** 1360 * List all available locales 1361 * 1362 * @return array Locale list 1363 */ 1364 public function listLocales() 1365 { 1366 $locales = array(); 1367 if (! $this->hasLocales()) { 1368 return $locales; 1369 } 1370 1371 $dh = opendir($this->localedir); 1372 while (false !== ($file = readdir($dh))) { 1373 $filename = $this->localedir . DIRECTORY_SEPARATOR . $file; 1374 if (preg_match('/^[a-z]{2}_[A-Z]{2}$/', $file) && is_dir($filename)) { 1375 $locales[] = $file; 1376 } 1377 } 1378 closedir($dh); 1379 sort($locales); 1380 return $locales; 1381 } 1382 1383 /** 1384 * Register web integration 1385 * 1386 * Add controller directory to mvc 1387 * 1388 * @return $this 1389 */ 1390 protected function registerWebIntegration() 1391 { 1392 if (! $this->app->isWeb()) { 1393 return $this; 1394 } 1395 1396 return $this 1397 ->registerLocales() 1398 ->registerRoutes(); 1399 } 1400 1401 /** 1402 * Add routes for static content and any route added via {@link addRoute()} to the route chain 1403 * 1404 * @return $this 1405 */ 1406 protected function registerRoutes() 1407 { 1408 $router = $this->app->getFrontController()->getRouter(); 1409 1410 // TODO: We should not be required to do this. Please check dispatch() 1411 $this->app->getfrontController()->addControllerDirectory( 1412 $this->getControllerDir(), 1413 $this->getName() 1414 ); 1415 1416 /** @var \Zend_Controller_Router_Rewrite $router */ 1417 foreach ($this->routes as $name => $route) { 1418 $router->addRoute($name, $route); 1419 } 1420 $router->addRoute( 1421 $this->name . '_jsprovider', 1422 new Zend_Controller_Router_Route( 1423 'js/' . $this->name . '/:file', 1424 array( 1425 'action' => 'javascript', 1426 'controller' => 'static', 1427 'module' => 'default', 1428 'module_name' => $this->name 1429 ) 1430 ) 1431 ); 1432 $router->addRoute( 1433 $this->name . '_img', 1434 new Zend_Controller_Router_Route_Regex( 1435 'img/' . $this->name . '/(.+)', 1436 array( 1437 'action' => 'img', 1438 'controller' => 'static', 1439 'module' => 'default', 1440 'module_name' => $this->name 1441 ), 1442 array( 1443 1 => 'file' 1444 ) 1445 ) 1446 ); 1447 return $this; 1448 } 1449 1450 /** 1451 * Run module bootstrap script 1452 * 1453 * @return $this 1454 */ 1455 protected function launchRunScript() 1456 { 1457 return $this->includeScript($this->runScript); 1458 } 1459 1460 /** 1461 * Include a php script if it is readable 1462 * 1463 * @param string $file File to include 1464 * 1465 * @return $this 1466 */ 1467 protected function includeScript($file) 1468 { 1469 if (file_exists($file) && is_readable($file)) { 1470 include $file; 1471 } 1472 1473 return $this; 1474 } 1475 1476 /** 1477 * Run module config script 1478 * 1479 * @return $this 1480 */ 1481 protected function launchConfigScript() 1482 { 1483 if ($this->triedToLaunchConfigScript) { 1484 return $this; 1485 } 1486 $this->triedToLaunchConfigScript = true; 1487 $this->registerAutoloader(); 1488 return $this->includeScript($this->configScript); 1489 } 1490 1491 /** 1492 * Register a hook 1493 * 1494 * @param string $name Name of the hook 1495 * @param string $class Class of the hook w/ namespace 1496 * @param string $key 1497 * 1498 * @return $this 1499 * 1500 * @deprecated Deprecated since 2.1.1. Use {@link provideHook()} instead 1501 */ 1502 protected function registerHook($name, $class, $key = null) 1503 { 1504 return $this->provideHook($name, $class, $key); 1505 } 1506 1507 protected function slashesToNamespace($class) 1508 { 1509 $list = explode('/', $class); 1510 foreach ($list as &$part) { 1511 $part = ucfirst($part); 1512 } 1513 1514 return implode('\\', $list); 1515 } 1516 1517 /** 1518 * Provide a hook implementation 1519 * 1520 * @param string $name Name of the hook for which to provide an implementation 1521 * @param string $implementation Fully qualified name of the class providing the hook implementation. 1522 * Defaults to the module's ProvidedHook namespace plus the hook's name for the 1523 * class name 1524 * @param bool $alwaysRun To run the hook always (e.g. without permission check) 1525 * 1526 * @return $this 1527 */ 1528 protected function provideHook($name, $implementation = null, $alwaysRun = false) 1529 { 1530 if ($implementation === null) { 1531 $implementation = $name; 1532 } 1533 1534 if (strpos($implementation, '\\') === false) { 1535 $class = $this->getNamespace() 1536 . '\\ProvidedHook\\' 1537 . $this->slashesToNamespace($implementation); 1538 } else { 1539 $class = $implementation; 1540 } 1541 1542 Hook::register($name, $class, $class, $alwaysRun); 1543 return $this; 1544 } 1545 1546 /** 1547 * Add a route which will be added to the route chain 1548 * 1549 * @param string $name Name of the route 1550 * @param Zend_Controller_Router_Route_Abstract $route Instance of the route 1551 * 1552 * @return $this 1553 * @see registerRoutes() 1554 */ 1555 protected function addRoute($name, Zend_Controller_Router_Route_Abstract $route) 1556 { 1557 $this->routes[$name] = $route; 1558 return $this; 1559 } 1560 1561 /** 1562 * (non-PHPDoc) 1563 * @see Translator::translate() For the function documentation. 1564 */ 1565 protected function translate($string, $context = null) 1566 { 1567 return mt($this->name, $string, $context); 1568 } 1569 1570 /** 1571 * (non-PHPDoc) 1572 * @see Translator::translatePlural() For the function documentation. 1573 */ 1574 protected function translatePlural($textSingular, $textPlural, $number, $context = null) 1575 { 1576 return mtp($this->name, $textSingular, $textPlural, $number, $context); 1577 } 1578} 1579