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