diff --git a/.gitignore b/.gitignore index 9f11b75..49fd402 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .idea/ +/debian +/build +/coverage diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..aed4be7 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,86 @@ +stages: +- Coding Standards +- Unit-Tests +- Build Packages + +variables: + BASE_VERSION: "2.0.0" + VERSION_SUFFIX: "-b${CI_BUILD_ID}-${CI_BUILD_REF_SLUG}" + +PSR2 CS Test: + stage: Coding Standards + tags: + - xenial + script: + - phpcs --report-width=auto --report-full --report-gitblame --report-summary -p --standard=PSR2 --extensions=php --encoding=utf-8 -w -s library/Businessprocess/ application/ configuration.php run.php test + +Ubuntu Xenial: + stage: Unit-Tests + tags: + - xenial + - businessprocess + script: + - phpunit --testdox --coverage-html=coverage || phpunit --verbose + artifacts: + expire_in: 1 week + name: code-coverage + paths: + - coverage/* + +Debian Jessie: + stage: Unit-Tests + tags: + - jessie + - businessprocess + script: + - phpunit --testdox || phpunit --verbose + +CentOS 6: + stage: Unit-Tests + tags: + - centos6 + - businessprocess + script: + - phpunit --testdox || phpunit --verbose + +CentOS 7: + stage: Unit-Tests + tags: + - centos7 + - businessprocess + script: + - phpunit --testdox || phpunit --verbose + +Xenial Packages: + stage: Build Packages + tags: + - xenial + - businessprocess + script: + - cp -a packaging/debian debian + - dch --no-conf -U -M --empty -v "${BASE_VERSION}${VERSION_SUFFIX}-${CI_BUILD_REF:0:7}" "Automated build triggered by ${GITLAB_USER_ID} <${GITLAB_USER_EMAIL}>" + - cp LICENSE debian/copyright + - dpkg-buildpackage -us -uc + - mkdir build + - mv ../icingaweb2-module-businessprocess*.deb build/ + artifacts: + expire_in: 1 week + paths: + - build/* + +Jessie Packages: + stage: Build Packages + tags: + - jessie + - businessprocess + script: + - cp -a packaging/debian debian + - dch --no-conf -U -M --empty -v "${BASE_VERSION}${VERSION_SUFFIX}-${CI_BUILD_REF:0:7}" "Automated build triggered by ${GITLAB_USER_ID} <${GITLAB_USER_EMAIL}>" + - cp LICENSE debian/copyright + - dpkg-buildpackage -us -uc + - mkdir build + - mv ../icingaweb2-module-businessprocess*.deb build/ + artifacts: + expire_in: 1 week + paths: + - build/* diff --git a/COPYING b/LICENSE similarity index 99% rename from COPYING rename to LICENSE index ecbc059..d159169 100644 --- a/COPYING +++ b/LICENSE @@ -336,4 +336,4 @@ This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. \ No newline at end of file +Public License instead of this License. diff --git a/application/clicommands/CheckCommand.php b/application/clicommands/CheckCommand.php index b6ddd50..d1c561f 100644 --- a/application/clicommands/CheckCommand.php +++ b/application/clicommands/CheckCommand.php @@ -2,37 +2,15 @@ namespace Icinga\Module\Businessprocess\Clicommands; -use Icinga\Cli\Command; -use Icinga\Module\Businessprocess\Storage\LegacyStorage; -use Icinga\Module\Businessprocess\BpNode; -use Icinga\Module\Businessprocess\HostNode; - -class CheckCommand extends Command +class CheckCommand extends ProcessCommand { - protected $storage; - - protected $hostColors = array( - 0 => array('black', 'lightgreen'), - 1 => array('black', 'lightred'), - 2 => array('black', 'brown'), - 99 => array('black', 'lightgray'), - ); - - protected $serviceColors = array( - 0 => array('black', 'lightgreen'), - 1 => array('black', 'yellow'), - 2 => array('black', 'lightred'), - 3 => array('black', 'lightpurple'), - 99 => array('black', 'lightgray'), - ); - - public function init() + public function listActions() { - $this->storage = new LegacyStorage($this->Config()->getSection('global')); + return array('process'); } /** - * Check a specific process + * 'check process' is DEPRECATED, please use 'process check' instead * * USAGE * @@ -40,75 +18,6 @@ class CheckCommand extends Command */ public function processAction() { - $name = $this->params->get('config'); - if ($name === null) { - $name = $this->getFirstProcessName(); - } - - $bp = $this->storage->loadProcess($name); - - if (null !== ($stateType = $this->params->get('state-type'))) { - if ($stateType === 'soft') { - $bp->useSoftStates(); - } - if ($stateType === 'hard') { - $bp->useHardStates(); - } - } - - $node = $bp->getNode($this->params->shift()); - $bp->retrieveStatesFromBackend(); - if ($bp->hasErrors()) { - printf( - "Checking Business Process %s failed: %s\n", - $node->getAlias(), - implode("\n", $bp->getErrors()) - ); - exit(3); - } - - printf("Business Process %s: %s\n", $node->getStateName(), $node->getAlias()); - if ($this->params->shift('details')) { - echo $this->renderProblemTree($node->getProblemTree(), $this->params->shift('colors')); - } - - exit($node->getState()); - } - - protected function renderProblemTree($tree, $useColors = false, $depth = 0) - { - $output = ''; - - foreach ($tree as $name => $subtree) { - $node = $subtree['node']; - - if ($node instanceof HostNode) { - $colors = $this->hostColors[$node->getState()]; - } else { - $colors = $this->serviceColors[$node->getState()]; - } - - $state = sprintf('[%s]', $node->getStateName()); - if ($useColors) { - $state = $this->screen->colorize($state, $colors[0], $colors[1]); - } - - $output .= sprintf( - "%s%s %s %s\n", - str_repeat(' ', $depth), - $node instanceof BpNode ? $node->getOperator() : '-', - $state, - $node->getAlias() - ); - $output .= $this->renderProblemTree($subtree['children'], $useColors, $depth + 1); - } - - return $output; - } - - protected function getFirstProcessName() - { - $list = $this->storage->listProcesses(); - return key($list); + $this->checkAction(); } } diff --git a/application/clicommands/ProcessCommand.php b/application/clicommands/ProcessCommand.php new file mode 100644 index 0000000..67c63b1 --- /dev/null +++ b/application/clicommands/ProcessCommand.php @@ -0,0 +1,153 @@ + array('black', 'lightgreen'), + 1 => array('lightgray', 'lightred'), + 2 => array('black', 'brown'), + 99 => array('black', 'lightgray'), + ); + + protected $serviceColors = array( + 0 => array('black', 'lightgreen'), + 1 => array('black', 'yellow'), + 2 => array('lightgray', 'lightred'), + 3 => array('black', 'lightpurple'), + 99 => array('black', 'lightgray'), + ); + + public function init() + { + $this->storage = new LegacyStorage($this->Config()->getSection('global')); + } + + /** + * List all available process + * + * USAGE + * + * icingacli businessprocess list processes [options] + * + * OPTIONS + * + * --no-title Show only the process names and no related title + */ + public function listAction() + { + $noTitle = $this->params->shift('no-title'); + foreach ($this->storage->listProcessNames() as $key => $title) { + if ($noTitle) { + echo $key . "\n"; + } else { + echo $title . "\n"; + } + } + } + + /** + * Check a specific process + * + * USAGE + * + * icingacli businessprocess process check [options] + * + * OPTIONS + * + * --config Name of the config that contains + * --details Show problem details as a tree + * --colors Show colored output + * --state-type Define which state type to look at. Could be + * either soft or hard, overrides an eventually + * configured default + */ + public function checkAction() + { + $name = $this->params->get('config'); + if ($name === null) { + $name = $this->getFirstProcessName(); + } + + $bp = $this->storage->loadProcess($name); + + if (null !== ($stateType = $this->params->get('state-type'))) { + if ($stateType === 'soft') { + $bp->useSoftStates(); + } + if ($stateType === 'hard') { + $bp->useHardStates(); + } + } + + /** @var BpNode $node */ + $node = $bp->getNode($this->params->shift()); + MonitoringState::apply($bp); + if ($bp->hasErrors()) { + printf( + "Checking Business Process %s failed: %s\n", + $node->getAlias(), + implode("\n", $bp->getErrors()) + ); + exit(3); + } + + printf("Business Process %s: %s\n", $node->getStateName(), $node->getAlias()); + if ($this->params->shift('details')) { + echo $this->renderProblemTree($node->getProblemTree(), $this->params->shift('colors')); + } + + exit($node->getState()); + } + + protected function renderProblemTree($tree, $useColors = false, $depth = 0) + { + $output = ''; + + foreach ($tree as $name => $subtree) { + /** @var Node $node */ + $node = $subtree['node']; + + if ($node instanceof HostNode) { + $colors = $this->hostColors[$node->getState()]; + } else { + $colors = $this->serviceColors[$node->getState()]; + } + + $state = sprintf('[%s]', $node->getStateName()); + if ($useColors) { + $state = $this->screen->colorize($state, $colors[0], $colors[1]); + } + + $output .= sprintf( + "%s%s %s %s\n", + str_repeat(' ', $depth), + $node instanceof BpNode ? $node->getOperator() : '-', + $state, + $node->getAlias() + ); + $output .= $this->renderProblemTree($subtree['children'], $useColors, $depth + 1); + } + + return $output; + } + + protected function getFirstProcessName() + { + $list = $this->storage->listProcessNames(); + return key($list); + } +} diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php index 9db9841..60ddc70 100644 --- a/application/controllers/IndexController.php +++ b/application/controllers/IndexController.php @@ -1,30 +1,20 @@ tabs()->add('welcome', array( - 'label' => $this->translate('Business Processes'), - 'url' => $this->getRequest()->getUrl() - ))->activate('welcome'); - - $configs = $this->storage()->listProcesses(); - - if (! empty($configs)) { - // Redirect to show the first process if there is any - $this->redirectNow( - 'businessprocess/process/show', - array('config' => key($configs)) - ); - } - - // Check back from time to time, maybe someone created a process - $this->setAutorefreshInterval(30); + $this->setTitle($this->translate('Business Process Overview')); + $this->controls()->add($this->overviewTab()); + $this->content()->add(Dashboard::create($this->Auth(), $this->storage())); + $this->setAutorefreshInterval(15); } } diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php index 31596f8..2c7c444 100644 --- a/application/controllers/NodeController.php +++ b/application/controllers/NodeController.php @@ -1,79 +1,60 @@ -process = - -*/ -class Businessprocess_NodeController extends Controller +class NodeController extends Controller { - // rename to config - public function editAction() + public function impactAction() { - $bp = $this->loadModifiedBpConfig(); - $node = $bp->getNode($this->getParam('node')); - $detail = Url::fromPath( - 'businessprocess/node/edit', - array( - 'config' => $this->view->configName, - 'node' => $node - ) + $this->setAutorefreshInterval(10); + $content = $this->content(); + $this->controls()->add( + $this->singleTab($this->translate('Node Impact')) ); + $name = $this->params->get('name'); + $this->addTitle($this->translate('Business Impact (%s)'), $name); - $this->view->form = ProcessForm::construct() - ->setProcess($bp) - ->setSession($this->session()) - ->setNode($node) - ->setRedirectUrl( - sprintf( - 'businessprocess/process/show?config=%s&unlocked#!%s', - $bp->getName(), - $detail->getAbsoluteUrl() - ) - ) - ->handleRequest(); + foreach ($this->storage()->listProcessNames() as $configName) { + $config = $this->storage()->loadProcess($configName); - $this->view->node = $node; - } + // TODO: Fix issues with children, they do not exist unless resolved :-/ + // This is a workaround: + foreach ($config->getRootNodes() as $node) { + $node->getState(); + } + foreach ($config->getRootNodes() as $node) { + $node->clearState(); + } - public function simulateAction() - { - $bp = $this->loadBpConfig(); - $nodename = $this->getParam('node'); - $node = $this->view->node = $bp->getNode($nodename); + if (! $config->hasNode($name)) { + continue; + } - $this->view->form = $this->loadForm('simulation') - ->setNode($node) - ->setSimulation(new Simulation($bp, $this->session())) - ->handleRequest(); + MonitoringState::apply($config); + $simulation = new Simulation($config, $this->session()); + $config->applySimulation($simulation); - if ($this->view->form->succeeded()) { - $this->render('empty'); + foreach ($config->getNode($name)->getPaths() as $path) { + $node = array_pop($path); + $renderer = new TileRenderer($config, $config->getNode($node)); + $renderer->setUrl( + Url::fromPath( + 'businessprocess/process/show', + array('config' => $configName) + ) + )->setPath($path); + + $bc = Breadcrumb::create($renderer); + $bc->attributes()->set('data-base-target', '_next'); + $content->add($bc); + } } } - - public function addAction() - { - $bp = $this->loadBpConfig(); - - $redirectUrl = Url::fromPath( - 'businessprocess/process/show', - array('config' => $bp->getName()) - ); - - $this->view->form = ProcessForm::construct() - ->setProcess($bp) - ->setSession($this->session()) - ->setRedirectUrl($redirectUrl) - ->handleRequest(); - } - - public function deleteAction() - { - } } diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index d1da782..421b298 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -1,30 +1,54 @@ setTitle($this->translate('Create a new business process')); - $this->tabsForCreate()->activate('create'); + $this->assertPermission('businessprocess/create'); - $this->view->form = BpConfigForm::construct() + $title = $this->translate('Create a new business process'); + $this->setTitle($title); + $this->controls() + ->add($this->tabsForCreate()->activate('create')) + ->add(HtmlTag::h1($title)); + + $this->content()->add( + $this->loadForm('bpConfig') ->setStorage($this->storage()) - ->setRedirectUrl('businessprocess/process/show') - ->handleRequest(); + ->setSuccessUrl('businessprocess/process/show') + ->handleRequest() + ); } /** @@ -32,84 +56,247 @@ class Businessprocess_ProcessController extends Controller */ public function uploadAction() { - $this->setTitle($this->translate('Upload a business process config file')); - $this->tabsForCreate()->activate('upload'); + $title = $this->translate('Upload a business process config file'); + $this->setTitle($title); + $this->controls() + ->add($this->tabsForCreate()->activate('upload')) + ->add(HtmlTag::h1($title)); + + $this->content()->add( + $this->loadForm('BpUpload') + ->setStorage($this->storage()) + ->setSuccessUrl('businessprocess/process/show') + ->handleRequest() + ); } /** - * Show a business process tree + * Show a business process */ public function showAction() { - $this->redirectIfConfigChosen(); + $bp = $this->loadModifiedBpConfig(); + $node = $this->getNode($bp); + $this->redirectOnConfigSwitch(); + MonitoringState::apply($bp); + $this->handleSimulations($bp); + + $this->setTitle($this->translate('Business Process "%s"'), $bp->getTitle()); + + $renderer = $this->prepareRenderer($bp, $node); if ($this->params->get('unlocked')) { - $bp = $this->loadModifiedBpConfig(); - $bp->unlock(); - } else { - $bp = $this->loadBpConfig(); + $renderer->unlock(); } - $this->setTitle('Business Process "%s"', $bp->getTitle()); - $this->tabsForShow()->activate('show'); - - // Do not lock empty configs - if ($bp->isEmpty() && ! $this->view->compact && $bp->isLocked()) { + if ($bp->isEmpty() && $renderer->isLocked()) { $this->redirectNow($this->url()->with('unlocked', true)); } - if ($node = $this->params->get('node')) { - // Render a specific node - $this->view->nodeName = $node; - $this->view->bp = $bp->getNode($node); - } else { - // Render a single process - $this->view->bp = $bp; - if ($bp->hasWarnings()) { - $this->view->warnings = $bp->getWarnings(); - } + $this->prepareControls($bp, $renderer); + $this->content()->addContent($this->showHints($bp)); + $this->content()->addContent($this->showWarnings($bp)); + $this->content()->add($renderer); + $this->loadActionForm($bp, $node); + $this->setDynamicAutorefresh(); + } + + protected function prepareControls($bp, $renderer) + { + $controls = $this->controls(); + + if ($this->showFullscreen) { + $controls->attributes()->add('class', 'want-fullscreen'); + $controls->add( + Link::create( + Icon::create('resize-small'), + $this->url()->without('showFullscreen')->without('view'), + null, + array('style' => 'float: right') + ) + ); } - $bp->retrieveStatesFromBackend(); + if (! ($this->showFullscreen || $this->view->compact)) { + $controls->add($this->getProcessTabs($bp, $renderer)); + } + if (! $this->view->compact) { + $controls->add(Element::create('h1')->setContent($this->view->title)); + } + $controls->add(Breadcrumb::create($renderer)); + if (! $this->showFullscreen && ! $this->view->compact) { + $controls->add( + new RenderedProcessActionBar($bp, $renderer, $this->Auth(), $this->url()) + ); + } + } - if ($bp->isLocked()) { - $this->tabs()->extend(new DashboardAction()); + protected function getNode(BpConfig $bp) + { + if ($nodeName = $this->params->get('node')) { + return $bp->getNode($nodeName); } else { - $simulation = new Simulation($bp, $this->session()); - if ($this->params->get('dismissSimulations')) { - Notification::success( - sprintf( - $this->translate('%d applied simulation(s) have been dropped'), - $simulation->count() - ) - ); - $simulation->clear(); - $this->redirectNow($this->url()->without('dismissSimulations')->without('unlocked')); - } + return null; + } + } - $bp->applySimulation($simulation); + protected function prepareRenderer($bp, $node) + { + if ($this->renderer === null) { + + if ($this->params->get('mode') === 'tree') { + $renderer = new TreeRenderer($bp, $node); + } else { + $renderer = new TileRenderer($bp, $node); + } + $renderer->setUrl($this->url()) + ->setPath($this->params->getValues('path')); + + $this->renderer = $renderer; } - if ($this->isXhr()) { - $this->setAutorefreshInterval(10); - } else { + return $this->renderer; + } + + protected function getProcessTabs(BpConfig $bp, Renderer $renderer) + { + + $tabs = $this->singleTab($bp->getTitle()); + if ($renderer->isLocked()) { + $tabs->extend(new DashboardAction()); + } + + return $tabs; + } + + protected function handleSimulations(BpConfig $bp) + { + $simulation = new Simulation($bp, $this->session()); + + if ($this->params->get('dismissSimulations')) { + Notification::success( + sprintf( + $this->translate('%d applied simulation(s) have been dropped'), + $simulation->count() + ) + ); + $simulation->clear(); + $this->redirectNow($this->url()->without('dismissSimulations')->without('unlocked')); + } + + $bp->applySimulation($simulation); + } + + protected function loadActionForm(BpConfig $bp, Node $node = null) + { + $action = $this->params->get('action'); + $form = null; + if ($this->showFullscreen) { + return; + } + + if ($action === 'add') { + $form = $this->loadForm('AddNode') + ->setProcess($bp) + ->setParentNode($node) + ->setSession($this->session()) + ->handleRequest(); + } elseif ($action === 'delete') { + $form =$this->loadForm('DeleteNode') + ->setProcess($bp) + ->setNode($bp->getNode($this->params->get('deletenode'))) + ->setParentNode($node) + ->setSession($this->session()) + ->handleRequest(); + } elseif ($action === 'edit') { + $form =$this->loadForm('Process') + ->setProcess($bp) + ->setNode($bp->getNode($this->params->get('editnode'))) + ->setSession($this->session()) + ->handleRequest(); + } elseif ($action === 'simulation') { + $form = $this->loadForm('simulation') + ->setNode($bp->getNode($this->params->get('simulationnode'))) + ->setSimulation(new Simulation($bp, $this->session())) + ->handleRequest(); + } + + if ($form) { + $this->content()->prependContent(HtmlString::create((string) $form)); + } + } + + protected function setDynamicAutorefresh() + { + if (! $this->isXhr()) { // This will trigger the very first XHR refresh immediately on page // load. Please not that this may hammer the server in case we would // decide to use autorefreshInterval for HTML meta-refreshes also. $this->setAutorefreshInterval(1); + return; } - if ($this->params->get('mode') === 'toplevel') { - $this->render('toplevel'); + if ($this->params->get('action')) { + $this->setAutorefreshInterval(45); + } else { + $this->setAutorefreshInterval(10); } } - /** - * Show a business process from a toplevel perspective - */ - public function toplevelAction() + protected function showWarnings(BpConfig $bp) { - $this->redirectIfConfigChosen(); + if ($bp->hasWarnings()) { + $ul = Element::create('ul', array('class' => 'warning')); + foreach ($bp->getWarnings() as $warning) { + $ul->createElement('li')->addContent($warning); + } + + return $ul; + } else { + return null; + } + } + + protected function showHints(BpConfig $bp) + { + $ul = Element::create('ul', array('class' => 'error')); + foreach ($bp->getErrors() as $error) { + $ul->createElement('li')->addContent($error); + } + if ($bp->hasChanges()) { + $ul->createElement('li')->setSeparator(' ')->addContent(sprintf( + $this->translate('This process has %d pending change(s).'), + $bp->countChanges() + ))->addContent( + Link::create( + $this->translate('Store'), + 'businessprocess/process/config', + array('config' => $bp->getName()) + ) + )->addContent( + Link::create( + $this->translate('Dismiss'), + $this->url()->with('dismissChanges', true), + null + ) + ); + } + + if ($bp->hasSimulations()) { + $ul->createElement('li')->setSeparator(' ')->addContent(sprintf( + $this->translate('This process shows %d simulated state(s).'), + $bp->countSimulations() + ))->addContent(Link::create( + $this->translate('Dismiss'), + $this->url()->with('dismissSimulations', true) + )); + } + + if ($ul->hasContent()) { + return $ul; + } else { + return null; + } } /** @@ -117,27 +304,73 @@ class Businessprocess_ProcessController extends Controller */ public function sourceAction() { - $this->tabsForConfig()->activate('source'); $bp = $this->loadModifiedBpConfig(); + $this->view->showDiff = $showDiff = (bool) $this->params->get('showDiff', false); - $this->view->source = $bp->toLegacyConfigString(); - $this->view->showDiff = (bool) $this->params->get('showDiff', false); - + $this->view->source = LegacyConfigRenderer::renderConfig($bp); if ($this->view->showDiff) { $this->view->diff = ConfigDiff::create( $this->storage()->getSource($this->view->configName), $this->view->source ); - $this->view->title = sprintf( + $title = sprintf( $this->translate('%s: Source Code Differences'), $bp->getTitle() ); } else { - $this->view->title = sprintf( + $title = sprintf( $this->translate('%s: Source Code'), $bp->getTitle() ); } + + $actionBar = new ActionBar(); + $this->setTitle($title); + $this->controls() + ->add($this->tabsForConfig()->activate('source')) + ->add(HtmlTag::h1($title)) + ->add($actionBar); + + if ($showDiff) { + $actionBar->add( + Link::create( + $this->translate('Source'), + $this->url()->without('showDiff'), + null, + array( + 'class' => 'icon-doc-text', + 'title' => $this->translate('Show source code'), + ) + ) + ); + } else { + $actionBar->add( + Link::create( + $this->translate('Diff'), + $this->url()->with('showDiff', true), + null, + array( + 'class' => 'icon-flapping', + 'title' => $this->translate('Highlight changes'), + ) + ) + ); + } + + $actionBar->add( + Link::create( + $this->translate('Download'), + 'businessprocess/process/download', + array('config' => $bp->getName()), + array( + 'target' => '_blank', + 'class' => 'icon-download', + 'title' => $this->translate('Download process configuration') + ) + ) + ); + + $this->setViewScript('process/source'); } /** @@ -146,18 +379,17 @@ class Businessprocess_ProcessController extends Controller public function downloadAction() { $bp = $this->loadModifiedBpConfig(); - - header( + $response = $this->getResponse(); + $response->setHeader( + 'Content-Disposition', sprintf( - 'Content-Disposition: attachment; filename="%s.conf";', + 'attachment; filename="%s.conf";', $bp->getName() ) ); - header('Content-Type: text/plain'); + $response->setHeader('Content-Type', 'text/plain'); - echo $bp->toLegacyConfigString(); - // Didn't have time to lookup how to correctly disable our renderers - // TODO: no exit :) + echo $this->storage()->render($bp); $this->doNotRender(); } @@ -166,40 +398,38 @@ class Businessprocess_ProcessController extends Controller */ public function configAction() { - $this->tabsForConfig()->activate('config'); $bp = $this->loadModifiedBpConfig(); - $this->setTitle( + $title = sprintf( $this->translate('%s: Configuration'), $bp->getTitle() ); + $this->setTitle($title); + $this->controls() + ->add($this->tabsForConfig()->activate('config')) + ->add(HtmlTag::h1($title)); - $url = sprintf( - 'businessprocess/process/show?config=%s&unlocked#!%s', - $bp->getName(), - $this->getRequest()->getUrl() + $url = Url::fromPath( + 'businessprocess/process/show?unlocked', + array('config' => $bp->getName()) + ); + $this->content()->add( + $this->loadForm('bpConfig') + ->setProcessConfig($bp) + ->setStorage($this->storage()) + ->setSuccessUrl($url) + ->handleRequest() ); - $this->view->form = BpConfigForm::construct() - ->setProcessConfig($bp) - ->setStorage($this->storage()) - ->setRedirectUrl($url) - ->handleRequest(); - - $this->view->deleteForm = DeleteConfigForm::construct() - ->setStorage($this->storage()) - ->setController($this) - ->setBpConfig($bp) - ->handleRequest(); } /** * Redirect to our URL plus the chosen config if someone switched the * config in the appropriate dropdown list */ - protected function redirectIfConfigChosen() + protected function redirectOnConfigSwitch() { $request = $this->getRequest(); - if ($request->isPost()) { + if ($request->isPost() && $request->getPost('action') === 'switchConfig') { // We switched the process in the config dropdown list $params = array( 'config' => $request->getPost('config') @@ -216,6 +446,9 @@ class Businessprocess_ProcessController extends Controller )); } + /** + * @return Tabs + */ protected function tabsForCreate() { return $this->tabs()->add('create', array( diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php new file mode 100644 index 0000000..e79bf36 --- /dev/null +++ b/application/forms/AddNodeForm.php @@ -0,0 +1,397 @@ +getView(); + if ($this->hasParentNode()) { + $this->addHtml( + '

' . $view->escape( + sprintf($this->translate('Add a node to %s'), $this->parent->getAlias()) + ) . '

' + ); + } else { + $this->addHtml( + '

' . $this->translate('Add a new root node') . '

' + ); + } + + $type = $this->selectNodeType(); + switch ($type) { + case 'host': + $this->selectHost(); + break; + case 'service': + $this->selectService(); + break; + case 'process': + $this->selectProcess(); + break; + case 'new-process': + $this->addNewProcess(); + break; + case null: + $this->setSubmitLabel($this->translate('Next')); + return; + } + } + + protected function addNewProcess() + { + $this->addElement('text', 'name', array( + 'label' => $this->translate('Name'), + 'required' => true, + 'description' => $this->translate( + 'This is the unique identifier of this process' + ), + )); + + $this->addElement('text', 'alias', array( + 'label' => $this->translate('Title'), + 'description' => $this->translate( + 'Usually this title will be shown for this node. Equals name' + . ' if not given' + ), + )); + + $this->addElement('select', 'operator', array( + 'label' => $this->translate('Operator'), + 'required' => true, + 'multiOptions' => array( + '&' => $this->translate('AND'), + '|' => $this->translate('OR'), + '!' => $this->translate('NOT'), + '<' => $this->translate('DEG'), + '1' => $this->translate('MIN 1'), + '2' => $this->translate('MIN 2'), + '3' => $this->translate('MIN 3'), + '4' => $this->translate('MIN 4'), + '5' => $this->translate('MIN 5'), + '6' => $this->translate('MIN 6'), + '7' => $this->translate('MIN 7'), + '8' => $this->translate('MIN 8'), + '9' => $this->translate('MIN 9'), + ) + )); + + $this->addElement('select', 'display', array( + 'label' => $this->translate('Visualization'), + 'required' => true, + 'description' => $this->translate( + 'Where to show this process' + ), + 'value' => $this->hasParentNode() ? '0' : '1', + 'multiOptions' => array( + '1' => $this->translate('Toplevel Process'), + '0' => $this->translate('Subprocess only'), + ) + )); + + $this->addElement('text', 'infoUrl', array( + 'label' => $this->translate('Info URL'), + 'description' => $this->translate( + 'URL pointing to more information about this node' + ) + )); + } + + /** + * @return string|null + */ + protected function selectNodeType() + { + $types = array(); + if ($this->hasParentNode()) { + $types['host'] = $this->translate('Host'); + $types['service'] = $this->translate('Service'); + } elseif (! $this->hasProcesses()) { + $this->addElement('hidden', 'node_type', array( + 'ignore' => true, + 'decorators' => array('ViewHelper'), + 'value' => 'new-process' + )); + + return 'new-process'; + } + + if ($this->hasProcesses()) { + $types['process'] = $this->translate('Existing Process'); + } + + $types['new-process'] = $this->translate('New Process Node'); + + $this->addElement('select', 'node_type', array( + 'label' => $this->translate('Node type'), + 'required' => true, + 'description' => $this->translate( + 'The node type you want to add' + ), + 'ignore' => true, + 'class' => 'autosubmit', + 'multiOptions' => $this->optionalEnum($types) + )); + + return $this->getSentValue('node_type'); + } + + protected function selectHost() + { + $this->addElement('multiselect', 'children', array( + 'label' => $this->translate('Hosts'), + 'required' => true, + 'size' => 8, + 'style' => 'width: 25em', + 'multiOptions' => $this->enumHostList(), + 'description' => $this->translate( + 'Hosts that should be part of this business process node' + ) + )); + } + + protected function selectService() + { + $this->addHostElement(); + if ($host = $this->getSentValue('host')) { + $this->addServicesElement($host); + } else { + $this->setSubmitLabel($this->translate('Next')); + } + } + + protected function addHostElement() + { + $this->addElement('select', 'host', array( + 'label' => $this->translate('Host'), + 'required' => true, + 'ignore' => true, + 'class' => 'autosubmit', + 'multiOptions' => $this->optionalEnum($this->enumHostForServiceList()), + )); + } + + protected function addServicesElement($host) + { + $this->addElement('multiselect', 'children', array( + 'label' => $this->translate('Services'), + 'required' => true, + 'size' => 8, + 'style' => 'width: 25em', + 'multiOptions' => $this->enumServiceList($host), + 'description' => $this->translate( + 'Services that should be part of this business process node' + ) + )); + } + + protected function selectProcess() + { + $this->addElement('multiselect', 'children', array( + 'label' => $this->translate('Process nodes'), + 'required' => true, + 'size' => 8, + 'style' => 'width: 25em', + 'multiOptions' => $this->enumProcesses(), + 'description' => $this->translate( + 'Other processes that should be part of this business process node' + ) + )); + } + + /** + * @param MonitoringBackend $backend + * @return $this + */ + public function setBackend(MonitoringBackend $backend) + { + $this->backend = $backend; + return $this; + } + + /** + * @param BpConfig $process + * @return $this + */ + public function setProcess(BpConfig $process) + { + $this->bp = $process; + $this->setBackend($process->getBackend()); + return $this; + } + + /** + * @param BpNode|null $node + * @return $this + */ + public function setParentNode(BpNode $node = null) + { + $this->parent = $node; + return $this; + } + + /** + * @return bool + */ + public function hasParentNode() + { + return $this->parent !== null; + } + + /** + * @param SessionNamespace $session + * @return $this + */ + public function setSession(SessionNamespace $session) + { + $this->session = $session; + return $this; + } + + protected function enumHostForServiceList() + { + $names = $this->backend->select()->from('hostStatus', array( + 'hostname' => 'host_name', + ))->order('host_name')->getQuery()->fetchColumn(); + + // fetchPairs doesn't seem to work when using the same column with + // different aliases twice + + return array_combine((array) $names, (array) $names); + } + + protected function enumHostList() + { + $names = $this->backend->select()->from('hostStatus', array( + 'hostname' => 'host_name', + ))->order('host_name')->getQuery()->fetchColumn(); + + // fetchPairs doesn't seem to work when using the same column with + // different aliases twice + $res = array(); + $suffix = ';Hoststatus'; + foreach ($names as $name) { + $res[$name . $suffix] = $name; + } + + return $res; + } + + protected function enumServiceList($host) + { + $query = $this->backend->select()->from( + 'serviceStatus', + array('service' => 'service_description') + )->where('host_name', $host); + $query->order('service_description'); + $names = $query->getQuery()->fetchColumn(); + + $services = array(); + foreach ($names as $name) { + $services[$host . ';' . $name] = $name; + } + + return $services; + } + + protected function hasProcesses() + { + return count($this->enumProcesses()) > 0; + } + + protected function enumProcesses() + { + $list = array(); + + foreach ($this->bp->getNodes() as $node) { + if ($node instanceof BpNode) { + // TODO: Blacklist parents + $list[(string) $node] = (string) $node; // display name? + } + } + + natsort($list); + return $list; + } + + protected function fetchObjectList() + { + $this->objectList = array(); + $hosts = $this->backend->select()->from('hostStatus', array( + 'hostname' => 'host_name', + 'in_downtime' => 'host_in_downtime', + 'ack' => 'host_acknowledged', + 'state' => 'host_state' + ))->order('host_name')->getQuery()->fetchAll(); + + $services = $this->backend->select()->from('serviceStatus', array( + 'hostname' => 'host_name', + 'service' => 'service_description', + 'in_downtime' => 'service_in_downtime', + 'ack' => 'service_acknowledged', + 'state' => 'service_state' + ))->order('host_name')->order('service_description')->getQuery()->fetchAll(); + + foreach ($hosts as $host) { + $this->objectList[$host->hostname] = array( + $host->hostname . ';Hoststatus' => 'Host Status' + ); + } + + foreach ($services as $service) { + $this->objectList[$service->hostname][ + $service->hostname . ';' . $service->service + ] = $service->service; + } + + return $this; + } + + public function onSuccess() + { + $changes = ProcessChanges::construct($this->bp, $this->session); + switch ($this->getValue('node_type')) { + case 'host': + case 'service': + case 'process': + $changes->addChildrenToNode($this->getValue('children'), $this->parent); + break; + case 'new-process': + $properties = $this->getValues(); + unset($properties['name']); + $properties['parentName'] = $this->parent->getName(); + $changes->createNode($this->getValue('name'), $properties); + break; + } + + // Trigger session destruction to make sure it get's stored. + // TODO: figure out why this is necessary, might be an unclean shutdown on redirect + unset($changes); + + parent::onSuccess(); + } +} diff --git a/application/forms/BpConfigForm.php b/application/forms/BpConfigForm.php index 2e06262..c485a57 100644 --- a/application/forms/BpConfigForm.php +++ b/application/forms/BpConfigForm.php @@ -3,18 +3,19 @@ namespace Icinga\Module\Businessprocess\Forms; use Icinga\Application\Config; -use Icinga\Module\Businessprocess\BusinessProcess; -use Icinga\Module\Businessprocess\Form; -use Icinga\Web\Notification; -use Icinga\Web\Request; -use Icinga\Web\Url; +use Icinga\Authentication\Auth; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Storage\Storage; +use Icinga\Module\Businessprocess\Web\Form\QuickForm; -class BpConfigForm extends Form +class BpConfigForm extends QuickForm { + /** @var Storage */ protected $storage; protected $backend; + /** @var BpConfig */ protected $config; protected $node; @@ -23,47 +24,112 @@ class BpConfigForm extends Form protected $processList = array(); + protected $deleteButtonName; + public function setup() { $this->addElement('text', 'name', array( - 'label' => $this->translate('Name'), + 'label' => $this->translate('Name'), 'required' => true, + 'validators' => array( + array( + 'validator' => 'StringLength', + 'options' => array( + 'min' => 2, + 'max' => 40 + ) + ), + array( + 'validator' => 'Regex', + 'options' => array( + 'pattern' => '/^[a-zA-Z0-9](?:[a-zA-Z0-9 ._-]*)?[a-zA-Z0-9_]$/' + ) + ) + ), 'description' => $this->translate( 'This is the unique identifier of this process' ), )); - $this->addElement('text', 'title', array( + $this->addElement('text', 'Title', array( 'label' => $this->translate('Title'), 'description' => $this->translate( 'Usually this title will be shown for this process. Equals name' - . ' if not given' + . ' if not given' ), )); - $this->addElement('select', 'backend_name', array( + $this->addElement('textarea', 'Description', array( + 'label' => $this->translate('Description'), + 'description' => $this->translate( + 'A slightly more detailed description for this process, about 100-150 characters long' + ), + 'rows' => 4, + )); + + $this->addElement('select', 'Backend', array( 'label' => $this->translate('Backend'), 'description' => $this->translate( 'Icinga Web Monitoring Backend where current object states for' - . ' this process should be retrieved from' + . ' this process should be retrieved from' ), 'multiOptions' => array( null => $this->translate('Use the configured default backend'), ) + $this->listAvailableBackends() )); - $this->addElement('select', 'state_type', array( + $this->addElement('select', 'Statetype', array( 'label' => $this->translate('State Type'), 'required' => true, 'description' => $this->translate( 'Whether this process should be based on Icinga hard or soft states' ), 'multiOptions' => array( - 'hard' => $this->translate('Use HARD states'), 'soft' => $this->translate('Use SOFT states'), + 'hard' => $this->translate('Use HARD states'), ) )); - $this->addElement('submit', $this->translate('Store')); + + $this->addElement('select', 'AddToMenu', array( + 'label' => $this->translate('Add to menu'), + 'required' => true, + 'description' => $this->translate( + 'Whether this process should be linked in the main Icinga Web 2 menu' + ), + 'multiOptions' => array( + 'yes' => $this->translate('Yes'), + 'no' => $this->translate('No'), + ) + )); + + if ($this->config === null) { + $this->setSubmitLabel( + $this->translate('Add') + ); + } else { + $config = $this->config; + + $meta = $config->getMetadata(); + foreach ($meta->getProperties() as $k => $v) { + if ($el = $this->getElement($k)) { + $el->setValue($v); + } + } + $this->getElement('name') + ->setValue($config->getName()) + ->setAttrib('readonly', true); + + $this->setSubmitLabel( + $this->translate('Store') + ); + + $label = $this->translate('Delete'); + $el = $this->createElement('submit', $label, array( + 'data-base-target' => '_main' + ))->setLabel($label)->setDecorators(array('ViewHelper')); + $this->deleteButtonName = $el->getName(); + $this->addElement($el); + } } protected function listAvailableBackends() @@ -81,78 +147,69 @@ class BpConfigForm extends Form public function setProcessConfig($config) { $this->config = $config; - $this->getElement('name')->setValue($config->getName()); - $this->getElement('name')->setAttrib('readonly', true); - - if ($config->hasTitle()) { - $this->getElement('title')->setValue($config->getTitle()); - } - - if ($config->hasBackend()) { - $this->getElement('backend_name')->setValue( - $config->getBackend()->getName() - ); - } - - if ($config->usesSoftStates()) { - $this->getElement('state_type')->setValue('soft'); - } else { - $this->getElement('state_type')->setValue('hard'); - } - return $this; } + protected function onRequest() + { + $name = $this->getValue('name'); + + if ($this->shouldBeDeleted()) { + $this->config->clearAppliedChanges(); + $this->storage->deleteProcess($name); + $this->setSuccessUrl('businessprocess'); + $this->redirectOnSuccess(sprintf('Process %s has been deleted', $name)); + } + } + public function onSuccess() { - $name = $this->getValue('name'); - $title = $this->getValue('title'); - $backend = $this->getValue('backend'); + $name = $this->getValue('name'); if ($this->config === null) { // New config - $config = new BusinessProcess(); + $config = new BpConfig(); $config->setName($name); - if ($title) { - $config->setTitle($title); - } - if ($backend) { - $config->setBackendName($backend); - } - if ($this->getValue('state_type') === 'soft') { - $config->useSoftStates(); - } else { - $config->useHardStates(); - } - $this->storage->storeProcess($config); - $config->clearAppliedChanges(); - $this->setRedirectUrl( - $this->getRedirectUrl()->setParams( + $config->getMetadata()->set('Owner', Auth::getInstance()->getUser()->getUsername()); + $this->setSuccessUrl( + $this->getSuccessUrl()->setParams( array('config' => $name, 'unlocked' => true) ) ); + $this->setSuccessMessage(sprintf('Process %s has been created', $name)); - Notification::success(sprintf('Process %s has been created', $name)); } else { - // Existing config $config = $this->config; - - if ($title) { - $config->setTitle($title); - } - if ($backend) { - $config->setBackendName($backend); - } - if ($this->getValue('state_type') === 'soft') { - $config->useSoftStates(); - } else { - $config->useHardStates(); - } - - $this->storage->storeProcess($config); - $config->clearAppliedChanges(); - $this->getRedirectUrl()->setParam('config', $name); - Notification::success(sprintf('Process %s has been stored', $name)); + $this->setSuccessMessage(sprintf('Process %s has been stored', $name)); } + $meta = $config->getMetadata(); + foreach ($this->getValues() as $key => $value) { + if ($value === null || $value === '') { + continue; + } + if ($meta->hasKey($key)) { + $meta->set($key, $value); + } + } + + $this->storage->storeProcess($config); + $config->clearAppliedChanges(); + $this->setSuccessUrl('businessprocess/process/show', array('config' => $name)); + parent::onSuccess(); + } + + public function hasDeleteButton() + { + return $this->deleteButtonName !== null; + } + + public function shouldBeDeleted() + { + if (! $this->hasDeleteButton()) { + return false; + } + + $name = $this->deleteButtonName; + return $this->getSentValue($name) === $this->getElement($name)->getLabel(); } } diff --git a/application/forms/BpUploadForm.php b/application/forms/BpUploadForm.php new file mode 100644 index 0000000..54c39c4 --- /dev/null +++ b/application/forms/BpUploadForm.php @@ -0,0 +1,232 @@ +addElement('text', 'name', array( + 'label' => $this->translate('Name'), + // 'required' => true, + 'description' => $this->translate( + 'This is the unique identifier of this process' + ), + )); + + $this->addElement('text', 'title', array( + 'label' => $this->translate('Title'), + 'description' => $this->translate( + 'Usually this title will be shown for this process. Equals name' + . ' if not given' + ), + )); + + $this->addElement('select', 'backend_name', array( + 'label' => $this->translate('Backend'), + 'description' => $this->translate( + 'Icinga Web Monitoring Backend where current object states for' + . ' this process should be retrieved from' + ), + 'multiOptions' => array( + null => $this->translate('Use the configured default backend'), + ) + $this->listAvailableBackends() + )); + + $this->addElement('select', 'state_type', array( + 'label' => $this->translate('State Type'), + 'required' => true, + 'description' => $this->translate( + 'Whether this process should be based on Icinga hard or soft states' + ), + 'multiOptions' => array( + 'hard' => $this->translate('Use HARD states'), + 'soft' => $this->translate('Use SOFT states'), + ) + )); + + $this->setAttrib('enctype', 'multipart/form-data'); + + $tmpdir = sys_get_temp_dir(); + + $this->addElement('file', 'uploaded_file', array( + 'label' => $this->translate('File'), + 'destination' => $tmpdir, + 'required' => true, + )); + + /** @var \Zend_Form_Element_File $el */ + $el = $this->getElement('uploaded_file'); + $el->setValueDisabled(true); + + if ($this->config === null) { + $this->setSubmitLabel( + $this->translate('Add') + ); + } else { + $config = $this->config; + + $this->getElement('name') + ->setValue($config->getName()) + ->setAttrib('readonly', true); + + if ($config->hasTitle()) { + $this->getElement('title')->setValue($config->getTitle()); + } + + if ($config->hasBackend()) { + $this->getElement('backend_name')->setValue( + $config->getBackend()->getName() + ); + } + if ($config->usesSoftStates()) { + $this->getElement('state_type')->setValue('soft'); + } else { + $this->getElement('state_type')->setValue('hard'); + } + + $this->setSubmitLabel( + $this->translate('Upload') + ); +/* + $label = $this->translate('Delete'); + $el = $this->createElement('submit', $label) + ->setLabel($label) + ->setDecorators(array('ViewHelper')); + $this->deleteButtonName = $el->getName(); + $this->addElement($el); +*/ + } + } + + protected function listAvailableBackends() + { + $keys = array_keys(Config::module('monitoring', 'backends')->toArray()); + return array_combine($keys, $keys); + } + + public function setStorage(LegacyStorage $storage) + { + $this->storage = $storage; + return $this; + } + + public function setProcessConfig(BpConfig $config) + { + $this->config = $config; + return $this; + } + + public function onSuccess() + { + + $tmpdir = sys_get_temp_dir(); + $tmpfile = tempnam($tmpdir, 'bpupload_'); + unlink($tmpfile); + $values = $this->getValues(); + /** @var \Zend_Form_Element_File $el */ + $el = $this->getElement('uploaded_file'); + var_dump($el->getFileName()); + var_dump($tmpfile); + $el->addFilter('Rename', $tmpfile); + if (!$el->receive()) { + print_r($el->file->getMessages()); + } + echo file_get_contents($tmpfile); + unlink($tmpfile); + echo "DONE\n"; + exit; + $name = $this->getValue('name'); + $title = $this->getValue('title'); + $backend = $this->getValue('backend'); + /* + onSuccess: + $uploadedData = $form->getValues(); + $fullFilePath = $form->file->getFileName(); + */ + var_dump($this->getValues()); + + exit; + + if ($this->config === null) { + // New config + $config = new BpConfig(); + $config->setName($name); + if ($title) { + $config->setTitle($title); + } + if ($backend) { + $config->setBackendName($backend); + } + if ($this->getValue('state_type') === 'soft') { + $config->useSoftStates(); + } else { + $config->useHardStates(); + } + $this->storage->storeProcess($config); + $config->clearAppliedChanges(); + $this->setSuccessUrl( + $this->getSuccessUrl()->setParams( + array('config' => $name, 'unlocked' => true) + ) + ); + + $this->redirectOnSuccess(sprintf('Process %s has been created', $name)); + } else { + $config = $this->config; + if ($title) { + $config->setTitle($title); + } + if ($backend) { + $config->setBackendName($backend); + } + if ($this->getValue('state_type') === 'soft') { + $config->useSoftStates(); + } else { + $config->useHardStates(); + } + + $this->storage->storeProcess($config); + $config->clearAppliedChanges(); + $this->getSuccessUrl()->setParam('config', $name); + Notification::success(sprintf('Process %s has been stored', $name)); + } + } + + public function hasDeleteButton() + { + return $this->deleteButtonName !== null; + } + + public function shouldBeDeleted() + { + if (! $this->hasDeleteButton()) { + return false; + } + + $name = $this->deleteButtonName; + return $this->getSentValue($name) === $this->getElement($name)->getLabel(); + } +} diff --git a/application/forms/DeleteConfigForm.php b/application/forms/DeleteConfigForm.php deleted file mode 100644 index 02f5dc4..0000000 --- a/application/forms/DeleteConfigForm.php +++ /dev/null @@ -1,46 +0,0 @@ -addHidden('name'); - $this->addElement('submit', $this->translate('Delete this process')); - } - - public function setStorage($storage) - { - $this->storage = $storage; - return $this; - } - - public function setController($controller) - { - $this->controller = $controller; - return $this; - } - - public function setBpConfig($bp) - { - $this->getElement('name')->setValue($bp->getName()); - return $this; - } - - public function onSuccess() - { - $name = $this->getValue('name'); - $this->storage->deleteProcess($name); - $this->setRedirectUrl('businessprocess'); - Notification::success(sprintf('Process %s has been deleted', $name)); - } -} diff --git a/application/forms/DeleteNodeForm.php b/application/forms/DeleteNodeForm.php new file mode 100644 index 0000000..b09cd68 --- /dev/null +++ b/application/forms/DeleteNodeForm.php @@ -0,0 +1,152 @@ +node; + $view = $this->getView(); + $this->addHtml( + '

' . $view->escape( + sprintf($this->translate('Delete "%s"'), $node->getAlias()) + ) . '

' + ); + + $biLink = $view->qlink( + $node->getAlias(), + 'businessprocess/node/impact', + array('name' => $node->getName()), + array('data-base-target' => '_next') + ); + $this->addHtml( + '

' . sprintf( + $view->escape( + $this->translate('Unsure? Show business impact of "%s"') + ), + $biLink + ) . '

' + ); + + if ($this->parentNode) { + $yesMsg = sprintf( + $this->translate('Delete from %s'), + $this->parentNode->getAlias() + ); + } else { + $yesMsg = sprintf( + $this->translate('Delete root node "%s"'), + $this->node->getAlias() + ); + } + + $this->addElement('select', 'confirm', array( + 'label' => $this->translate('Are you sure?'), + 'required' => true, + 'description' => $this->translate( + 'Do you really want to delete this node?' + ), + 'multiOptions' => $this->optionalEnum(array( + 'no' => $this->translate('No'), + 'yes' => $yesMsg, + 'all' => sprintf($this->translate('Delete all occurrences of %s'), $node->getAlias()), + )) + )); + } + + /** + * @param MonitoringBackend $backend + * @return $this + */ + public function setBackend(MonitoringBackend $backend) + { + $this->backend = $backend; + return $this; + } + + /** + * @param BpConfig $process + * @return $this + */ + public function setProcess(BpConfig $process) + { + $this->bp = $process; + $this->setBackend($process->getBackend()); + return $this; + } + + /** + * @param Node $node + * @return $this + */ + public function setNode(Node $node) + { + $this->node = $node; + return $this; + } + + /** + * @param BpNode|null $node + * @return $this + */ + public function setParentNode(BpNode $node = null) + { + $this->parentNode = $node; + return $this; + } + + /** + * @param SessionNamespace $session + * @return $this + */ + public function setSession(SessionNamespace $session) + { + $this->session = $session; + return $this; + } + + public function onSuccess() + { + $changes = ProcessChanges::construct($this->bp, $this->session); + + switch ($this->getValue('confirm')) { + case 'yes': + $changes->deleteNode($this->node, $this->path); + break; + case 'all': + $changes->deleteNode($this->node); + break; + case 'no': + $this->setSuccessMessage($this->translate('Well, maybe next time')); + } + // Trigger session desctruction to make sure it get's stored. + // TODO: figure out why this is necessary, might be an unclean shutdown on redirect + unset($changes); + + parent::onSuccess(); + } +} diff --git a/application/forms/ProcessForm.php b/application/forms/ProcessForm.php index 2fda9fd..b3a5e7b 100644 --- a/application/forms/ProcessForm.php +++ b/application/forms/ProcessForm.php @@ -2,42 +2,55 @@ namespace Icinga\Module\Businessprocess\Forms; -use Icinga\Web\Notification; -use Icinga\Web\Request; -use Icinga\Module\Businessprocess\Node; use Icinga\Module\Businessprocess\BpNode; -use Icinga\Module\Businessprocess\Form; -use Icinga\Module\Businessprocess\ProcessChanges; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Modification\ProcessChanges; +use Icinga\Module\Businessprocess\Web\Form\QuickForm; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Web\Notification; +use Icinga\Web\Session\SessionNamespace; -class ProcessForm extends Form +class ProcessForm extends QuickForm { + /** @var MonitoringBackend */ protected $backend; - protected $process; + /** @var BpConfig */ + protected $bp; + /** @var BpNode */ protected $node; protected $objectList = array(); protected $processList = array(); + /** @var SessionNamespace */ protected $session; public function setup() { - $this->addElement('text', 'name', array( - 'label' => $this->translate('Name'), - 'required' => true, - 'description' => $this->translate( - 'This is the unique identifier of this process' - ), - )); + if ($this->node === null) { + $this->addElement('text', 'name', array( + 'label' => $this->translate('Name'), + 'required' => true, + 'description' => $this->translate( + 'This is the unique identifier of this process' + ), + )); + } else { + $this->addHtml( + '

' . $this->getView()->escape( + sprintf($this->translate('Modify "%s"'), $this->node->getAlias()) + ) . '

' + ); + } $this->addElement('text', 'alias', array( 'label' => $this->translate('Title'), 'description' => $this->translate( 'Usually this title will be shown for this node. Equals name' - . ' if not given' + . ' if not given' ), )); @@ -67,19 +80,8 @@ class ProcessForm extends Form 'Where to show this process' ), 'multiOptions' => array( - '0' => $this->translate('Subprocess only'), '1' => $this->translate('Toplevel Process'), - ) - )); - - $this->addElement('multiselect', 'children', array( - 'label' => $this->translate('Process components'), - 'required' => true, - 'size' => 14, - 'style' => 'width: 25em;', - 'description' => $this->translate( - 'Hosts, services or other processes that should be part of this' - . ' business process' + '0' => $this->translate('Subprocess only'), ) )); @@ -90,96 +92,54 @@ class ProcessForm extends Form ) )); - $this->addElement('submit', $this->translate('Store')); - } - - public function setBackend($backend) - { - $this->backend = $backend; - $this->fetchObjectList(); - $this->fillAvailableChildren(); - return $this; - } - - protected function fillAvailableChildren() - { - if (empty($this->processList)) { - $children = $this->objectList; - } else { - $children = array( - $this->translate('Other Business Processes') => $this->processList - ) + $this->objectList; - } - - $this->getElement('children')->setMultiOptions($children); - } - - public function setProcess($process) - { - $this->process = $process; - $this->setBackend($process->getBackend()); - $this->processList = array(); - foreach ($process->getNodes() as $node) { - if ($node instanceof BpNode) { - // TODO: Blacklist parents - $this->processList[(string) $node] = (string) $node; // display name? + if ($node = $this->node) { + if ($node->hasAlias()) { + $this->getElement('alias')->setValue($node->getAlias()); + } + $this->getElement('operator')->setValue($node->getOperator()); + $this->getElement('display')->setValue($node->getDisplay()); + if ($node->hasInfoUrl()) { + $this->getElement('url')->setValue($node->getInfoUrl()); } } - natsort($this->processList); - $this->fillAvailableChildren(); + } + + /** + * @param MonitoringBackend $backend + * @return $this + */ + public function setBackend(MonitoringBackend $backend) + { + $this->backend = $backend; return $this; } - public function setNode(Node $node) + /** + * @param BpConfig $process + * @return $this + */ + public function setProcess(BpConfig $process) + { + $this->bp = $process; + $this->setBackend($process->getBackend()); + return $this; + } + + /** + * @param BpNode $node + * @return $this + */ + public function setNode(BpNode $node) { $this->node = $node; - - $this->setDefaults(array( - 'name' => (string) $node, - 'alias' => $node->hasAlias() ? $node->getAlias() : '', - 'display' => $node->getDisplay(), - 'operator' => $node->getOperator(), - 'url' => $node->getInfoUrl(), - 'children' => array_keys($node->getChildren()) - )); - $this->getElement('name')->setAttrib('readonly', true); return $this; } - protected function fetchObjectList() - { - $this->objectList = array(); - $hosts = $this->backend->select()->from('hostStatus', array( - 'hostname' => 'host_name', - 'in_downtime' => 'host_in_downtime', - 'ack' => 'host_acknowledged', - 'state' => 'host_state' - ))->order('host_name')->getQuery()->fetchAll(); - - $services = $this->backend->select()->from('serviceStatus', array( - 'hostname' => 'host_name', - 'service' => 'service_description', - 'in_downtime' => 'service_in_downtime', - 'ack' => 'service_acknowledged', - 'state' => 'service_state' - ))->order('host_name')->order('service_description')->getQuery()->fetchAll(); - - foreach ($hosts as $host) { - $this->objectList[$host->hostname] = array( - $host->hostname . ';Hoststatus' => 'Host Status' - ); - } - - foreach ($services as $service) { - $this->objectList[$service->hostname][ - $service->hostname . ';' . $service->service - ] = $service->service; - } - - return $this; - } - - public function setSession($session) + /** + * @param SessionNamespace $session + * @return $this + */ + public function setSession(SessionNamespace $session) { $this->session = $session; return $this; @@ -187,10 +147,9 @@ class ProcessForm extends Form public function onSuccess() { - $changes = ProcessChanges::construct($this->process, $this->session); + $changes = ProcessChanges::construct($this->bp, $this->session); $modifications = array(); - $children = $this->getValue('children'); $alias = $this->getValue('alias'); $operator = $this->getValue('operator'); $display = $this->getValue('display'); @@ -201,7 +160,6 @@ class ProcessForm extends Form if (empty($alias)) { $alias = null; } - ksort($children); // TODO: rename if ($node = $this->node) { @@ -212,9 +170,6 @@ class ProcessForm extends Form if ($operator !== $node->getOperator()) { $modifications['operator'] = $operator; } - if ($children !== $node->getChildNames()) { - $modifications['childNames'] = $children; - } if ($url !== $node->getInfoUrl()) { $modifications['infoUrl'] = $url; } @@ -225,7 +180,6 @@ class ProcessForm extends Form $modifications = array( 'display' => $display, 'operator' => $operator, - 'childNames' => $children, 'infoUrl' => $url, 'alias' => $alias, ); @@ -241,7 +195,7 @@ class ProcessForm extends Form Notification::success( sprintf( 'Process %s has been modified', - $this->process->getName() + $this->bp->getName() ) ); } diff --git a/application/forms/SimulationForm.php b/application/forms/SimulationForm.php index 44ad3bf..23a4313 100644 --- a/application/forms/SimulationForm.php +++ b/application/forms/SimulationForm.php @@ -2,56 +2,72 @@ namespace Icinga\Module\Businessprocess\Forms; -use Icinga\Module\Businessprocess\BpNode; -use Icinga\Module\Businessprocess\Web\Form\QuickForm; +use Icinga\Module\Businessprocess\MonitoredNode; use Icinga\Module\Businessprocess\Simulation; -use Icinga\Web\Request; +use Icinga\Module\Businessprocess\Web\Form\QuickForm; class SimulationForm extends QuickForm { + /** @var MonitoredNode */ protected $node; + /** @var MonitoredNode */ protected $simulatedNode; + /** @var Simulation */ protected $simulation; public function setup() { - $states = array( - null => sprintf( - $this->translate('Use current state (%s)'), - $this->translate($this->node->getStateName()) - ), - '0' => $this->translate('OK'), - '1' => $this->translate('WARNING'), - '2' => $this->translate('CRITICAL'), - '3' => $this->translate('UNKNOWN'), - '99' => $this->translate('PENDING'), - ); + $states = $this->enumStateNames(); // TODO: Fetch state from object if ($this->simulatedNode) { - $states[$this->simulatedNode->getState()] . sprintf(' (%s)', $this->translate('Current simulation')); + $simulatedState = $this->simulatedNode->getState(); + $states[$simulatedState] = sprintf( + '%s (%s)', + $this->node->getStateName($simulatedState), + $this->translate('Current simulation') + ); $node = $this->simulatedNode; + $hasSimulation = true; } else { + $hasSimulation = false; $node = $this->node; } + $view = $this->getView(); + if ($hasSimulation) { + $title = $this->translate('Modify simulation for %s'); + } else { + $title = $this->translate('Add simulation for %s'); + } + $this->addHtml( + '

' + . $view->escape(sprintf($title, $node->getAlias())) + . '

' + ); + $this->addElement('select', 'state', array( 'label' => $this->translate('State'), 'multiOptions' => $states, + 'class' => 'autosubmit', 'value' => $this->simulatedNode ? $node->getState() : null, )); + if (in_array($this->getSentValue('state'), array('0', '99'))) { + return; + } + if ($hasSimulation || ctype_digit($this->getSentValue('state'))) { + $this->addElement('checkbox', 'acknowledged', array( + 'label' => $this->translate('Acknowledged'), + 'value' => $node->isAcknowledged(), + )); - $this->addElement('checkbox', 'acknowledged', array( - 'label' => $this->translate('Acknowledged'), - 'value' => $node->isAcknowledged(), - )); - - $this->addElement('checkbox', 'in_downtime', array( - 'label' => $this->translate('In downtime'), - 'value' => $node->isInDowntime(), - )); + $this->addElement('checkbox', 'in_downtime', array( + 'label' => $this->translate('In downtime'), + 'value' => $node->isInDowntime(), + )); + } $this->setSubmitLabel($this->translate('Apply')); } @@ -62,14 +78,14 @@ class SimulationForm extends QuickForm return $this; } - public function setSimulation($simulation) + public function setSimulation(Simulation $simulation) { $this->simulation = $simulation; - $nodeName = (string) $this->node; - if ($simulation->hasNode($nodeName)) { + $name = $this->node->getName(); + if ($simulation->hasNode($name)) { $this->simulatedNode = clone($this->node); - $s = $simulation->getNode($nodeName); + $s = $simulation->getNode($name); $this->simulatedNode->setState($s->state) ->setAck($s->acknowledged) ->setDowntime($s->in_downtime) @@ -81,7 +97,7 @@ class SimulationForm extends QuickForm public function onSuccess() { - $nodeName = (string) $this->node; + $nodeName = $this->node->getName(); if (ctype_digit($this->getValue('state'))) { $this->notifySuccess($this->translate('Simulation has been set')); @@ -95,5 +111,21 @@ class SimulationForm extends QuickForm $this->notifySuccess($this->translate('Simulation has been removed')); } } + $this->redirectOnSuccess(); + } + + /** + * @return array + */ + protected function enumStateNames() + { + $states = array( + null => sprintf( + $this->translate('Use current state (%s)'), + $this->translate($this->node->getStateName()) + ) + ) + $this->node->enumStateNames(); + + return $states; } } diff --git a/application/forms/_CreateConfigForm.php b/application/forms/_CreateConfigForm.php deleted file mode 100644 index 4182833..0000000 --- a/application/forms/_CreateConfigForm.php +++ /dev/null @@ -1,101 +0,0 @@ -addElement('text', 'name', array( - 'label' => $this->translate('Name'), - 'required' => true, - 'description' => $this->translate('This is the unique identifier of this process'), - )); - - $this->addElement('text', 'title', array( - 'label' => $this->translate('Title'), - 'description' => $this->translate('Usually this title will be shown for this process. Equals name if not given'), - )); - - $this->addElement('select', 'backend_name', array( - 'label' => $this->translate('Backend'), - 'required' => true, - 'description' => $this->translate('Icinga Web Monitoring Backend where current object states for this process should be retrieved from'), - 'multiOptions' => array( - null => $this->translate('Use current default backend'), - ) + $this->listAvailableBackends() - )); - - $this->addElement('select', 'state_type', array( - 'label' => $this->translate('State Type'), - 'required' => true, - 'description' => $this->translate('Whether this process should be based on Icinga hard or soft states'), - 'multiOptions' => array( - 'hard' => $this->translate('Use HARD states'), - 'soft' => $this->translate('Use SOFT states'), - ) - )); - $this->addElement('submit', $this->translate('Store')); - } - - protected function listAvailableBackends() - { - $keys = array_keys(Config::module('monitoring', 'backends')->toArray()); - return array_combine($keys, $keys); - } - - public function setBackend($backend) - { - $this->backend = $backend; - return $this; - } - - public function setProcessConfig($config) - { - $this->process = $config; - $this->getElement('name')->setValue($config->getName()); - - if ($config->hasTitle()) { - $this->getElement('title')->setValue($config->getTitle()); - } - - if ($config->hasBackend()) { - $this->getElement('backend_name')->setValue($config->getBackend()->getName()); - } - - if ($config->usesSoftStates()) { - $this->getElement('state_type')->setValue('soft'); - } else { - $this->getElement('state_type')->setValue('hard'); - } - - return $this; - } - - public function setSession($session) - { - $this->session = $session; - return $this; - } - - public function onSuccess() - { -/* - $storage->storeProcess($bp); - $modifications = $this->session->get('modifications', array()); - $node = $this->process->getNode($this->getValue('name')); - $node->setChildNames($this->getValue('children')); - $node->setOperator($this->getValue('operator')); - $modifications[$this->process->getName()] = $this->process->toLegacyConfigString(); - $this->session->set('modifications', $modifications); - $message = 'Process %s has been modified'; - Notification::success(sprintf($message, $this->process->getName())); -*/ - } -} diff --git a/application/locale/de_DE/LC_MESSAGES/businessprocess.mo b/application/locale/de_DE/LC_MESSAGES/businessprocess.mo index f8b0484..dcdc468 100644 Binary files a/application/locale/de_DE/LC_MESSAGES/businessprocess.mo and b/application/locale/de_DE/LC_MESSAGES/businessprocess.mo differ diff --git a/application/locale/de_DE/LC_MESSAGES/businessprocess.po b/application/locale/de_DE/LC_MESSAGES/businessprocess.po index 2f2c880..3e6bddc 100644 --- a/application/locale/de_DE/LC_MESSAGES/businessprocess.po +++ b/application/locale/de_DE/LC_MESSAGES/businessprocess.po @@ -1,135 +1,258 @@ # Icinga Web 2 - Head for multiple monitoring backends. -# Copyright (C) 2015 Icinga Development Team +# Copyright (C) 2016 Icinga Development Team # This file is distributed under the same license as Businessprocess Module. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" -"Project-Id-Version: Businessprocess Module (2.0.0-beta1)\n" +"Project-Id-Version: Businessprocess Module (2.0.0)\n" "Report-Msgid-Bugs-To: dev@icinga.org\n" -"POT-Creation-Date: 2015-03-16 06:19+0100\n" -"PO-Revision-Date: 2015-03-16 09:04+0100\n" +"POT-Creation-Date: 2016-12-12 13:32+0100\n" +"PO-Revision-Date: 2016-12-12 13:48+0100\n" "Last-Translator: Thomas Gelf \n" "Language: de_DE\n" -"Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 1.5.4\n" +"Language-Team: \n" +"X-Generator: Poedit 1.8.7.1\n" -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:83 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:174 #, php-format msgid "%d applied simulation(s) have been dropped" msgstr "%d angewendete Simulation(en) wurde(n) entfernt" -#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Controller.php:73 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Controller.php:187 #, php-format msgid "%d pending change(s) have been dropped" msgstr "%d vorgenommene Änderung(en) wurde(n) verworfen" -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:165 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:349 #, php-format msgid "%s: Configuration" msgstr "%s: Konfiguration" -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:129 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:313 #, php-format msgid "%s: Source Code" msgstr "%s: Quellcode" -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:124 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:308 #, php-format msgid "%s: Source Code Differences" msgstr "%s: Quellcode Unterschiede" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:48 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/ProvidedHook/Director/DataType/BusinessProcess.php:17 +msgid "(no process config chosen)" +msgstr "(keine Prozess-Konfiguration gewählt)" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Form/QuickBaseForm.php:113 +msgid "- please choose -" +msgstr "- bitte wählen -" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:50 +msgid "" +"A slightly more detailed description for this process, about 100-150 " +"characters long" +msgstr "" +"Eine etwas detailliertere Beschreibung dieses Prozesses, etwa 100-150 " +"Zeichen lang" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:53 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:76 msgid "AND" msgstr "UND" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:42 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:62 msgid "Acknowledged" msgstr "Bestätigt" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/node/add.phtml:8 -msgid "Add new process node" -msgstr "Neuen Knoten hinzufügen" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:92 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:86 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TileRenderer.php:91 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TreeRenderer.php:241 +msgid "Add" +msgstr "Hinzufügen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:49 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TileRenderer.php:89 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TreeRenderer.php:246 +msgid "Add a new business process node" +msgstr "Neuen Business-Prozess hinzufügen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:88 +msgid "Add children" +msgstr "Nachfolger hinzufügen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:43 +#, php-format +msgid "Add simulation for %s" +msgstr "Simulation für %s hinzufügen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:79 +msgid "Add to menu" +msgstr "Zum Menü hinzufügen" + +#: /usr/local/icingaweb-modules/businessprocess/configuration.php:50 +msgid "Allow to create whole new process configuration (files)" +msgstr "Erlaube das erstellen einer neuen Prozess-Konfiguration (Datei)" + +#: /usr/local/icingaweb-modules/businessprocess/configuration.php:54 +msgid "Allow to modify process definitions, to add and remove nodes" +msgstr "" +"Erlaubt es Prozessdefinitionen zu modifizieren, sowie Knoten hinzuzufügen " +"und zu entfernen" + +#: /usr/local/icingaweb-modules/businessprocess/configuration.php:46 +msgid "" +"Allow to see all available processes, regardless of configured restrictions" +msgstr "" +"Erlaubt es, alle verfügbaren Prozesse unabhängig von konfigurierten " +"Restriktionen zu sehen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:96 +msgid "Another process" +msgstr "Ein anderer Prozess" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:72 msgid "Apply" msgstr "Anwenden" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/index/index.phtml:8 -#, php-format -msgid "" -"As no business process has been defined yet you might want to create a %s or " -"upload an %s." -msgstr "" -"Nachdem noch kein Business-Prozess definiert wurde, möchtest du vermutlich " -"einen %s erstellen oder einen %s hochladen." +#: /usr/local/icingaweb-modules/businessprocess/application/forms/DeleteNodeForm.php:32 +msgid "Are you sure?" +msgstr "Bist du sicher?" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:47 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:27 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:56 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:48 msgid "Backend" msgstr "Backend" -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:206 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:384 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Controller.php:128 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Controller.php:139 msgid "Business Process" msgstr "Business-Prozess" -#: /usr/local/icingaweb-modules/businessprocess/configuration.php:4 -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/IndexController.php:13 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:81 +#, php-format +msgid "Business Process \"%s\"" +msgstr "Businessprozesse \"%s\"" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/IndexController.php:15 +msgid "Business Process Overview" +msgstr "Businessprozessübersicht" + +#: /usr/local/icingaweb-modules/businessprocess/configuration.php:5 msgid "Business Processes" msgstr "Business-Prozesse" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:31 -msgid "CRITICAL" -msgstr "KRITISCH" +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/ActionBar.php:75 +msgid "Config" +msgstr "Konfiguration" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/index/index.phtml:6 -msgid "Configure your first business process" -msgstr "Konfiguriere deinen ersten Business-Prozess" - -#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:297 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:316 #, php-format msgid "Could not retrieve process state: %s" msgstr "Konnte den Prozess-Status nicht ermitteln: %s" -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:214 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:395 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/Dashboard.php:55 msgid "Create" msgstr "Erstelle" -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:21 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/Dashboard.php:56 +msgid "Create a new Business Process configuration" +msgstr "Erstelle eine neue Businessprozesskonfiguration" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:37 msgid "Create a new business process" msgstr "Neuen Business-Prozess erstellen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/DeleteConfigForm.php:18 -msgid "Delete this process" -msgstr "Lösche diesen Prozess" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:30 +msgid "Current simulation" +msgstr "Aktuelle Simulation" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:40 -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:51 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:79 +msgid "DEG" +msgstr "DEG" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:111 +msgid "Delete" +msgstr "Löschen" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TileRenderer/NodeTile.php:159 +msgid "Delete this node" +msgstr "Lösche diesen Knoten" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:48 +msgid "Description" +msgstr "Beschreibung" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:267 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:279 msgid "Dismiss" msgstr "Verwerfen" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/DeleteNodeForm.php:35 +msgid "Do you really want to delete this node" +msgstr "Möchtest du diesen Knoten wirklich löschen" + #: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/source.phtml:6 msgid "Download process configuration" msgstr "Prozess-Konfiguration herunterladen" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:125 +msgid "Existing Process" +msgstr "Existierender Prozess" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:97 +msgid "External process" +msgstr "Externer Prozess" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:75 +msgid "File" +msgstr "Datei" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Form/QuickForm.php:390 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Form/QuickForm.php:415 +msgid "Form has successfully been sent" +msgstr "Das Formular wurde erfolgreich versandt" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/Dashboard.php:47 +msgid "" +"From here you can reach all your defined Business Process configurations, " +"create new or modify existing ones" +msgstr "" +"Von hier kannst du alle definierten Businessprozesskonfigurationen " +"erreichen, neue erstellen oder bestehende bearbeiten" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/ActionBar.php:89 +msgid "Fullscreen" +msgstr "Vollbild" + #: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/source.phtml:10 msgid "Highlight changes" msgstr "Änderungen hervorheben" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:80 -msgid "" -"Hosts, services or other processes that should be part of this business " -"process" -msgstr "" -"Hosts, Services oder andere Prozesse welche Teil dieses Business-Prozesses " -"sein sollen" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:94 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:135 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:120 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:171 +msgid "Host" +msgstr "Host" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:49 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:29 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:121 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:147 +msgid "Hosts" +msgstr "Hosts" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:127 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:153 +msgid "Hosts that should be part of this business process node" +msgstr "Hosts welche Teil dieses Business-Prozesses sein sollen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:58 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:50 msgid "" "Icinga Web Monitoring Backend where current object states for this process " "should be retrieved from" @@ -137,51 +260,65 @@ msgstr "" "Das Icinga Web Monitoring Backend von welchem die Status-Informationen für " "diesen Prozess bezogen werden sollen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:46 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:67 msgid "In downtime" msgstr "In Downtime" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:86 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:81 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:106 msgid "Info URL" msgstr "Info-URL" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:13 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/ActionBar.php:61 +msgid "Lock" +msgstr "Sperren" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/ActionBar.php:66 msgid "Lock this process" msgstr "Sperre diesen Prozess" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:50 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:56 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:80 msgid "MIN 1" msgstr "MIN 1" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:51 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:57 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:81 msgid "MIN 2" msgstr "MIN 2" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:52 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:58 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:82 msgid "MIN 3" msgstr "MIN 3" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:53 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:59 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:83 msgid "MIN 4" msgstr "MIN 4" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:54 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:60 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:84 msgid "MIN 5" msgstr "MIN 5" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:55 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:61 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:85 msgid "MIN 6" msgstr "MIN 6" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:56 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:62 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:86 msgid "MIN 7" msgstr "MIN 7" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:57 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:63 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:87 msgid "MIN 8" msgstr "MIN 8" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:58 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:64 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:88 msgid "MIN 9" msgstr "MIN 9" @@ -190,225 +327,359 @@ msgstr "MIN 9" msgid "Modify process node: %s" msgstr "Bearbeite Knoten: %s" -#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BpNode.php:268 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:41 +#, php-format +msgid "Modify simulation for %s" +msgstr "Bearbeite die Simulation für %s" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TileRenderer/NodeTile.php:136 +msgid "Modify this business process node" +msgstr "Bearbeite diesen Prozessknoten" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TreeRenderer.php:199 msgid "Modify this node" msgstr "Bearbeite diesen Knoten" -#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Node.php:350 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/ActionBar.php:80 +msgid "Modify this process" +msgstr "Bearbeite diesen Prozess" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TreeRenderer.php:222 msgid "More information" msgstr "Weitere Informationen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:31 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:16 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:29 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:55 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:78 +msgid "NOT" +msgstr "NICHT" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:32 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:34 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:57 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:32 msgid "Name" msgstr "Name" -#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:519 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BpNode.php:276 +#, php-format +msgid "Nesting error detected: %s" +msgstr "Verschachtelungsfehler erkannt: %s" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:128 +msgid "New Process Node" +msgstr "Neuen Prozess-Knoten hinzufügen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:47 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:164 +msgid "Next" +msgstr "Weiter" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:86 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/DeleteNodeForm.php:39 +msgid "No" +msgstr "Nein" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/Dashboard.php:77 +msgid "No Business Process has been defined for you" +msgstr "Es wurde noch kein Business-Prozess für dich definiert" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:693 #, php-format msgid "No business process nodes for \"%s\" have been defined yet" msgstr "Es wurden noch keine Business-Prozess Knoten für \"%s\" definiert" -#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:467 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Controller.php:205 +#, php-format +msgid "No such process config: \"%s\"" +msgstr "Keine entsprechende Prozesskonfiguration gefunden: \"%s\"" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:575 #, php-format msgid "Node \"%s\" has been defined twice" msgstr "Der Knoden \"%s\" wurde doppelt definiert" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:29 -msgid "OK" -msgstr "OK" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:131 +msgid "Node type" +msgstr "Knotentyp" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:49 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/Dashboard.php:76 +msgid "Not available" +msgstr "NIcht verfügbar" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:54 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:77 msgid "OR" msgstr "ODER" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:45 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:50 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:73 msgid "Operator" msgstr "Operator" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:109 -msgid "Other Business Processes" -msgstr "Andere Business-Prozesse" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:166 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:202 +msgid "Other processes that should be part of this business process node" +msgstr "Andere Prozesse welche Teil dieses Business-Prozesses sein sollen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:33 -msgid "PENDING" -msgstr "PENDING" - -#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:538 -#, php-format -msgid "Parser waring on %s:%s: %s" -msgstr "Parse-Warnung an %s:%s: %s" - -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:38 -msgid "Plugin output" -msgstr "Plugin-Ausgabe" - -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:225 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:406 msgid "Process Configuration" msgstr "Prozess-Konfiguration" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:75 -msgid "Process components" -msgstr "Prozess-Komponenten" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:160 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:196 +msgid "Process nodes" +msgstr "Prozess-Knoten" -#: /usr/local/icingaweb-modules/businessprocess/configuration.php:3 -msgid "Reporting" -msgstr "Reporting" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:95 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:121 +msgid "Service" +msgstr "Service" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:146 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:182 +msgid "Services" +msgstr "Services" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:152 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:188 +msgid "Services that should be part of this business process node" +msgstr "Services welche Teil dieses Business-Prozesses sein sollen" + +#: /usr/local/icingaweb-modules/businessprocess/configuration.php:26 +msgid "Show all" +msgstr "Alle anzeigen" #: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/source.phtml:8 msgid "Show source code" msgstr "Quellcode anzeigen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:86 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TileRenderer/NodeTile.php:151 +msgid "Show the business impact of this node by simulating a specific state" +msgstr "" +"Zeige den Business-Impact dieses Knoten durch Simulation eines gewünschten " +"Zustandes" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TileRenderer/NodeTile.php:142 +msgid "Show this subtree as a tree" +msgstr "Zeige diesen Teilbaum als eigenen Baum" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Renderer/TreeRenderer.php:212 +msgid "Simulate a specific state" +msgstr "Einen bestimmten Zustand simulieren" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:111 msgid "Simulation has been removed" msgstr "Simulation wurde entfernt" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:89 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:103 msgid "Simulation has been set" msgstr "Simulation wurde gesended" -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:228 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:409 msgid "Source" msgstr "Quelle" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:26 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:52 msgid "State" msgstr "Status" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:58 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:36 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:67 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:59 msgid "State Type" msgstr "Statustyp" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:36 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:68 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:44 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:92 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:261 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:108 msgid "Store" msgstr "Speichern" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:69 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Form/QuickForm.php:178 +msgid "Submit" +msgstr "Absenden" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:76 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:101 msgid "Subprocess only" msgstr "Nur Sub-Prozess" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/upload.phtml:8 -msgid "This has not been implemented yet" -msgstr "Das wurde noch nicht implementiert" +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/ActionBar.php:94 +msgid "Switch to fullscreen mode" +msgstr "Zum Vollbildmodus wechseln" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:34 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:18 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:32 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:134 +msgid "The node type you want to add" +msgstr "Der gewünschte Knotentyp" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:35 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:37 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:60 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:35 msgid "This is the unique identifier of this process" msgstr "Das ist der eindeutige Identifier dieses Prozesses" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:33 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:257 #, php-format msgid "This process has %d pending change(s)." msgstr "Dieser Prozess hat %d ungespeicherte Änderung(en)" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:48 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:276 #, php-format msgid "This process shows %d simulated state(s)." msgstr "Dieser Prozess zeigt %d simulierte Zustände" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:39 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:22 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:37 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/ActionBar.php:28 +msgid "Tiles" +msgstr "Kacheln" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:40 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:42 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:65 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:40 msgid "Title" msgstr "Titel" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:70 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:75 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:100 msgid "Toplevel Process" msgstr "Toplevel-Prozess" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:32 -msgid "UNKNOWN" -msgstr "UNBEKANNT" +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/ActionBar.php:37 +msgid "Tree" +msgstr "Baum" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:88 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:83 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:108 msgid "URL pointing to more information about this node" msgstr "URL zu mehr Informationen über diesen Knoten" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:10 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/ActionBar.php:49 +msgid "Unlock" +msgstr "Entsperren" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/ActionBar.php:54 msgid "Unlock this process" msgstr "Entsperre diesen Prozess" -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:217 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:398 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:111 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/Dashboard.php:64 msgid "Upload" msgstr "Upload" -#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:35 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:56 msgid "Upload a business process config file" msgstr "Lade eine Business-Prozess Konfigurationsdatei hoch" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:64 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:40 +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/Dashboard.php:65 +msgid "Upload an existing Business Process configuration" +msgstr "Lade eine Business-Prozess Konfigurationsdatei hoch" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:74 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:65 msgid "Use HARD states" msgstr "HARD-States benutzen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:65 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:41 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:73 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:66 msgid "Use SOFT states" msgstr "SOFT-States benutzen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:31 -msgid "Use current default backend" -msgstr "Aktuelles Standard-Backend benutzen" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:124 +#, php-format +msgid "Use current state (%s)" +msgstr "Aktuellen Status benutzen (%s)" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:28 -msgid "Use current state" -msgstr "Aktuellen Status benutzen" - -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:53 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:62 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:54 msgid "Use the configured default backend" msgstr "Benutze das konfigurierte Standard-Backend" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:39 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:44 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:67 msgid "" "Usually this title will be shown for this node. Equals name if not given" msgstr "" "Für gewöhnlich wird dieser Titel für diesen Knoten angezeigt. Entspricht dem " "Namen, wenn nicht angegeben" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:41 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:23 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:42 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:42 msgid "" "Usually this title will be shown for this process. Equals name if not given" msgstr "" "Für gewöhnlich wird dieser Titel für diesen Prozess angezeigt. Entspricht " "dem Namen, wenn nicht angegeben" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:63 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:69 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:93 msgid "Visualization" msgstr "Darstellung" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:30 -msgid "WARNING" -msgstr "WARNUNG" +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Web/Component/Dashboard.php:42 +msgid "Welcome to your Business Process Overview" +msgstr "Willkommen zur Übersicht deiner Business-Prozesse" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/warnings.phtml:2 -msgid "Warnings" -msgstr "Warnungen" - -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:66 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:72 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/AddNodeForm.php:96 msgid "Where to show this process" msgstr "Wo soll dieser Prozess angezeigt werden" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:61 -#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:38 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:70 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpUploadForm.php:62 msgid "Whether this process should be based on Icinga hard or soft states" -msgstr "Ob dieser Prozess auf Icinga's hard- oder soft-states basieren soll" +msgstr "Ob dieser Prozess auf Icinga's Hard- oder Softstates basieren soll" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/index/index.phtml:10 -msgid "existing one" -msgstr "einen existierenden" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:82 +msgid "Whether this process should be linked in the main Icinga Web 2 menu" +msgstr "Ob dieser Prozess ins Icinga Web 2 Hauptmenü verlinkt werden soll" -#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/index/index.phtml:9 -msgid "new business process" -msgstr "neuen Business-Prozess" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:85 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/DeleteNodeForm.php:40 +msgid "Yes" +msgstr "Ja" -#~ msgid "Show" -#~ msgstr "Zeige" +#~ msgid "" +#~ "As no business process has been defined yet you might want to create a %s " +#~ "or upload an %s." +#~ msgstr "" +#~ "Nachdem noch kein Business-Prozess definiert wurde, möchtest du " +#~ "vermutlich einen %s erstellen oder einen %s hochladen." + +#~ msgid "CRITICAL" +#~ msgstr "KRITISCH" + +#~ msgid "Configure your first business process" +#~ msgstr "Konfiguriere deinen ersten Business-Prozess" + +#~ msgid "OK" +#~ msgstr "OK" + +#~ msgid "PENDING" +#~ msgstr "PENDING" + +#~ msgid "Parser waring on %s:%s: %s" +#~ msgstr "Parse-Warnung an %s:%s: %s" + +#~ msgid "Plugin output" +#~ msgstr "Plugin-Ausgabe" + +#~ msgid "Reporting" +#~ msgstr "Reporting" + +#~ msgid "This has not been implemented yet" +#~ msgstr "Das wurde noch nicht implementiert" + +#~ msgid "UNKNOWN" +#~ msgstr "UNBEKANNT" + +#~ msgid "Use current default backend" +#~ msgstr "Aktuelles Standard-Backend benutzen" + +#~ msgid "WARNING" +#~ msgstr "WARNUNG" + +#~ msgid "Warnings" +#~ msgstr "Warnungen" #~ msgid "min" #~ msgstr "min" diff --git a/application/views/helpers/FormSimpleNote.php b/application/views/helpers/FormSimpleNote.php new file mode 100644 index 0000000..d8315f4 --- /dev/null +++ b/application/views/helpers/FormSimpleNote.php @@ -0,0 +1,15 @@ +_getInfo($name, $value); + extract($info); // name, value, attribs, options, listsep, disable + return $value; + } +} diff --git a/application/views/helpers/RenderStateBadges.php b/application/views/helpers/RenderStateBadges.php index 14db762..70633aa 100644 --- a/application/views/helpers/RenderStateBadges.php +++ b/application/views/helpers/RenderStateBadges.php @@ -1,15 +1,24 @@ $cnt) { - if ($cnt === 0) continue; - if ($state === 'OK') continue; - if ($state === 'UP') continue; + if ($cnt === 0 + || $state === 'OK' + || $state === 'UP' + ) { + continue; + } + $html .= '' . $cnt . ''; diff --git a/application/views/scripts/default.phtml b/application/views/scripts/default.phtml new file mode 100644 index 0000000..3e2cc59 --- /dev/null +++ b/application/views/scripts/default.phtml @@ -0,0 +1,2 @@ +controls->render() ?> +content->render() ?> diff --git a/application/views/scripts/editlink.phtml b/application/views/scripts/editlink.phtml deleted file mode 100644 index d00d340..0000000 --- a/application/views/scripts/editlink.phtml +++ /dev/null @@ -1,9 +0,0 @@ - bp->isEditMode()): ?> - bp->hasBeenChanged()): ?> - icon('ok') ?> - - icon('cancel') ?> - - icon('wrench') ?> - - diff --git a/application/views/scripts/index/index.phtml b/application/views/scripts/index/index.phtml deleted file mode 100644 index 4f090f3..0000000 --- a/application/views/scripts/index/index.phtml +++ /dev/null @@ -1,12 +0,0 @@ -
-tabs ?> -
- -
-

translate('Configure your first business process') ?>

-

translate('As no business process has been defined yet you might want to create a %s or upload an %s.'), - $this->qlink($this->translate('new business process'), 'businessprocess/process/create'), - $this->qlink($this->translate('existing one'), 'businessprocess/process/upload') -) ?>

-
diff --git a/application/views/scripts/node/add.phtml b/application/views/scripts/node/add.phtml deleted file mode 100644 index a765433..0000000 --- a/application/views/scripts/node/add.phtml +++ /dev/null @@ -1,10 +0,0 @@ -
-

translate('Add new process node') ?>

-form ?> -
diff --git a/application/views/scripts/node/edit.phtml b/application/views/scripts/node/edit.phtml deleted file mode 100644 index ad2ea47..0000000 --- a/application/views/scripts/node/edit.phtml +++ /dev/null @@ -1,15 +0,0 @@ -
-

escape( - sprintf( - $this->translate('Modify process node: %s'), - $node - ) -) ?>

-form ?> -
diff --git a/application/views/scripts/node/empty.phtml b/application/views/scripts/node/empty.phtml deleted file mode 100644 index 263fd9f..0000000 --- a/application/views/scripts/node/empty.phtml +++ /dev/null @@ -1 +0,0 @@ -__CLOSEME__ diff --git a/application/views/scripts/node/simulate.phtml b/application/views/scripts/node/simulate.phtml deleted file mode 100644 index 3ee903b..0000000 --- a/application/views/scripts/node/simulate.phtml +++ /dev/null @@ -1,8 +0,0 @@ - - -
-

BI Simulation: escape(preg_replace('/;/', ' - ', $this->node)) ?>

-form ?> -
diff --git a/application/views/scripts/process/config.phtml b/application/views/scripts/process/config.phtml deleted file mode 100644 index c788759..0000000 --- a/application/views/scripts/process/config.phtml +++ /dev/null @@ -1,16 +0,0 @@ -
-tabs ?> -

escape($this->title) ?> - icon('doc-text') ?> -

-
- -
-form ?> -
-deleteForm ?> -
-
- diff --git a/application/views/scripts/process/create.phtml b/application/views/scripts/process/create.phtml deleted file mode 100644 index 6cd98f7..0000000 --- a/application/views/scripts/process/create.phtml +++ /dev/null @@ -1,9 +0,0 @@ -
-tabs ?> -

escape($this->title) ?>

-
- -
-form ?> -
- diff --git a/application/views/scripts/process/history.phtml b/application/views/scripts/process/history.phtml deleted file mode 100644 index 34a20b3..0000000 --- a/application/views/scripts/process/history.phtml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - -
 
- -
- - - - - - -
 
-
- -
-history as $entry) { -if ($entry->hostname !== $last_host || $entry->service !== $last_service) { - -echo '' . "\n"; -} -$cnt++; -if ($cnt > 10000) break; - $duration = $entry->timestamp - $current_offset; - if ($next_color === null) { - $color = stateColor($entry->last_state); - } else { - $color = $next_color; - } - $next_color = stateColor($entry->state); - - if ($entry->state == 0) { - $offset = ceil($duration / 3600 / 6); - } else { - $offset = floor($duration / 3600 / 6); - } - echo '
 
'; - $current_offset += $duration; - - $last_host = $entry->hostname; -$last_service = $entry->service; - -} - - -?>
diff --git a/application/views/scripts/process/show.phtml b/application/views/scripts/process/show.phtml deleted file mode 100644 index c3bf5eb..0000000 --- a/application/views/scripts/process/show.phtml +++ /dev/null @@ -1,71 +0,0 @@ - -compact): ?> -
-tabs ?> -

- formSelect('config', $this->configName, array('class' => 'autosubmit'), $this->processList) ?> -
- -icon('dashboard') ?> -bpconfig->isLocked()): ?> -icon('lock') ?> - -bpconfig->isEmpty()): ?> -icon('lock-open') ?> - -icon('wrench') ?> - -icon('plus') ?> -

-
- - -
-bpconfig->isLocked()): ?> -qlink('Add new node', 'businessprocess/node/add', array('config' => $this->configName)) ?> - -bpconfig->hasErrors() || $this->bpconfig->hasChanges() || $this->bpconfig->hasSimulations()): ?> -
    -bpconfig->getErrors() as $error): ?> -
  • escape($error) ?>
  • - -bpconfig->hasChanges()): ?> -
  • translate('This process has %d pending change(s).'), - $this->bpconfig->countChanges() -) ?> qlink( - $this->translate('Store'), - 'businessprocess/process/config', - array('config' => $this->configName) -) ?> qlink( - $this->translate('Dismiss'), - $this->url()->with('dismissChanges', true), - null -) ?>
  • - -bpconfig->hasSimulations()): ?> -
  • translate('This process shows %d simulated state(s).'), - $this->bpconfig->countSimulations() -) ?> qlink( - $this->translate('Dismiss'), - $this->url()->with('dismissSimulations', true), - null -) ?>
  • - -
- -
-bp->renderHtml($this) ?> -bpconfig->isLocked()): ?> -bp instanceof BusinessProcess): /* do not render when showing subtree */ ?> -bp->renderUnbound($this) ?> - - -
-render('warnings.phtml') ?> -
diff --git a/application/views/scripts/process/source.phtml b/application/views/scripts/process/source.phtml index d4f7323..d5ba6bb 100644 --- a/application/views/scripts/process/source.phtml +++ b/application/views/scripts/process/source.phtml @@ -1,21 +1,9 @@ -
-tabs ?> -

escape($this->title) ?> - icon('download') ?> -showDiff): ?> - icon('doc-text') ?> - - icon('flapping') ?> - -

-
+controls->render() ?>
showDiff): ?>
-diff->renderHtml() ?> +diff->render() ?>
diff --git a/application/views/scripts/process/toplevel.phtml b/application/views/scripts/process/toplevel.phtml deleted file mode 100644 index b943dad..0000000 --- a/application/views/scripts/process/toplevel.phtml +++ /dev/null @@ -1,47 +0,0 @@ -bp->countChildren(); -$howMany = 'normal'; - -if ($count < 20) { - $howMany = 'few'; -} elseif ($count > 50) { - $howMany = 'many'; -} - -?> -compact): ?> -
-tabs ?> -

- formSelect('config', $this->configName, array('class' => 'autosubmit'), $this->processList) ?> - - icon('sitemap') ?> -

-
- - -
- - render('warnings.phtml') ?> -
diff --git a/application/views/scripts/process/upload.phtml b/application/views/scripts/process/upload.phtml deleted file mode 100644 index 36262ad..0000000 --- a/application/views/scripts/process/upload.phtml +++ /dev/null @@ -1,11 +0,0 @@ -
-tabs ?> -

escape($this->title) ?>

-
- -
-
    -
  • translate('This has not been implemented yet') ?>
  • -
-
- diff --git a/application/views/scripts/simulationlink.phtml b/application/views/scripts/simulationlink.phtml deleted file mode 100644 index 813d24d..0000000 --- a/application/views/scripts/simulationlink.phtml +++ /dev/null @@ -1,6 +0,0 @@ - bp->isSimulationMode()): ?> - icon('globe') ?> - - icon('magic') ?> - - diff --git a/application/views/scripts/warnings.phtml b/application/views/scripts/warnings.phtml deleted file mode 100644 index 9d1b16c..0000000 --- a/application/views/scripts/warnings.phtml +++ /dev/null @@ -1,6 +0,0 @@ -warnings): ?> -

translate('Warnings') ?>

-warnings as $warning): ?> -escape($warning) ?>
- - diff --git a/configuration.php b/configuration.php index a94fedc..3cd5f94 100644 --- a/configuration.php +++ b/configuration.php @@ -1,7 +1,56 @@ menuSection(N_('Overview')) - ->add($this->translate('Business Processes')) - ->setPriority(45) - ->setUrl('businessprocess'); +use Icinga\Module\Businessprocess\Storage\LegacyStorage; +/** @var \Icinga\Application\Modules\Module $this */ +$section = $this->menuSection(N_('Business Processes'), array( + 'url' => 'businessprocess', + 'icon' => 'sitemap', + 'priority' => 46 +)); + +try { + $storage = new LegacyStorage( + $this->getConfig()->getSection('global') + ); + + $prio = 0; + foreach ($storage->listProcessNames() as $name) { + $prio++; + + $meta = $storage->loadMetadata($name); + if ($meta->get('AddToMenu') === 'no') { + continue; + } + + if ($prio > 5) { + $section->add(N_('Show all'), array( + 'url' => 'businessprocess', + 'priority' => $prio + )); + + break; + } + + $section->add($meta->getTitle(), array( + 'url' => 'businessprocess/process/show', + 'urlParameters' => array('config' => $name), + 'priority' => $prio + )); + } +} catch (Exception $e) { + // Well... there is not much we could do here +} + +$this->providePermission( + 'businessprocess/showall', + $this->translate('Allow to see all available processes, regardless of configured restrictions') +); +$this->providePermission( + 'businessprocess/create', + $this->translate('Allow to create whole new process configuration (files)') +); +$this->providePermission( + 'businessprocess/modify', + $this->translate('Allow to modify process definitions, to add and remove nodes') +); diff --git a/library/Businessprocess/BusinessProcess.php b/library/Businessprocess/BpConfig.php similarity index 54% rename from library/Businessprocess/BusinessProcess.php rename to library/Businessprocess/BpConfig.php index 1789415..767e820 100644 --- a/library/Businessprocess/BusinessProcess.php +++ b/library/Businessprocess/BpConfig.php @@ -2,12 +2,14 @@ namespace Icinga\Module\Businessprocess; -use Icinga\Application\Benchmark; +use Icinga\Exception\IcingaException; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Businessprocess\Exception\NestingError; +use Icinga\Module\Businessprocess\Modification\ProcessChanges; use Icinga\Module\Monitoring\Backend\MonitoringBackend; -use Icinga\Data\Filter\Filter; use Exception; -class BusinessProcess +class BpConfig { const SOFT_STATE = 0; @@ -27,6 +29,9 @@ class BusinessProcess */ protected $backend; + /** @var Metadata */ + protected $metadata; + /** * Business process name * @@ -76,13 +81,6 @@ class BusinessProcess */ protected $root_nodes = array(); - /** - * All check names { 'hostA;ping' => true, ... } - * - * @var array - */ - protected $all_checks = array(); - /** * All host names { 'hostA' => true, ... } * @@ -90,6 +88,11 @@ class BusinessProcess */ protected $hosts = array(); + /** @var bool Whether catchable errors should be thrown nonetheless */ + protected $throwErrors = false; + + protected $loopDetection = array(); + /** * Applied state simulation * @@ -97,25 +100,51 @@ class BusinessProcess */ protected $simulation; - /** - * Whether we are in edit mode - * - * @var boolean - */ - protected $editMode = false; - - protected $locked = true; - protected $changeCount = 0; protected $simulationCount = 0; + /** @var ProcessChanges */ protected $appliedChanges; public function __construct() { } + /** + * Retrieve metadata for this configuration + * + * @return Metadata + */ + public function getMetadata() + { + if ($this->metadata === null) { + $this->metadata = new Metadata($this->name); + } + + return $this->metadata; + } + + /** + * Set metadata + * + * @param Metadata $metadata + * + * @return $this + */ + public function setMetadata(Metadata $metadata) + { + $this->metadata = $metadata; + return $this; + } + + /** + * Apply pending process changes + * + * @param ProcessChanges $changes + * + * @return $this + */ public function applyChanges(ProcessChanges $changes) { $cnt = 0; @@ -130,6 +159,13 @@ class BusinessProcess return $this; } + /** + * Apply a state simulation + * + * @param Simulation $simulation + * + * @return $this + */ public function applySimulation(Simulation $simulation) { $cnt = 0; @@ -147,28 +183,57 @@ class BusinessProcess } $this->simulationCount = $cnt; + + return $this; } + + /** + * Number of applied changes + * + * @return int + */ public function countChanges() { return $this->changeCount; } + /** + * Whether changes have been applied to this configuration + * + * @return int + */ public function hasChanges() { return $this->countChanges() > 0; } + /** + * @param $name + * + * @return $this + */ public function setName($name) { $this->name = $name; return $this; } + /** + * @return string + */ public function getName() { return $this->name; } + /** + * @return string + */ + public function getHtmlId() + { + return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', $this->getName()); + } + public function setTitle($title) { $this->title = $title; @@ -177,28 +242,22 @@ class BusinessProcess public function getTitle() { - return $this->title ?: $this->getName(); + return $this->getMetadata()->getTitle(); } public function hasTitle() { - return $this->title !== null; - } - - public function setBackendName($name) - { - $this->backendName = $name; - return $this; + return $this->getMetadata()->has('Title'); } public function getBackendName() { - return $this->backendName; + return $this->getMetadata()->get('Backend'); } public function hasBackendName() { - return $this->backendName !== null; + return $this->getMetadata()->has('Backend'); } public function setBackend(MonitoringBackend $backend) @@ -210,7 +269,7 @@ class BusinessProcess public function getBackend() { if ($this->backend === null) { - $this->backend = MonitoringBackend::createBackend( + $this->backend = MonitoringBackend::instance( $this->getBackendName() ); } @@ -228,22 +287,6 @@ class BusinessProcess return false; } - public function isLocked() - { - return $this->locked; - } - - public function lock($lock = true) - { - $this->locked = (bool) $lock; - return $this; - } - - public function unlock() - { - return $this->lock(false); - } - public function hasSimulations() { return $this->countSimulations() > 0; @@ -254,17 +297,6 @@ class BusinessProcess return $this->simulationCount; } - public function setEditMode($mode = true) - { - $this->editMode = (bool) $mode; - return $this; - } - - public function isEditMode() - { - return $this->editMode; - } - public function clearAppliedChanges() { if ($this->appliedChanges !== null) { @@ -292,7 +324,7 @@ class BusinessProcess public function usesHardStates() { - $this->state_type === self::HARD_STATE; + return $this->state_type === self::HARD_STATE; } public function addRootNode($name) @@ -315,122 +347,38 @@ class BusinessProcess return array_key_exists($name, $this->root_nodes); } - public function retrieveStatesFromBackend() - { - try { - $this->reallyRetrieveStatesFromBackend(); - } catch (Exception $e) { - $this->error( - $this->translate('Could not retrieve process state: %s'), - $e->getMessage() - ); - } - } - - public function reallyRetrieveStatesFromBackend() - { - Benchmark::measure('Retrieving states for business process ' . $this->getName()); - $backend = $this->getBackend(); - // TODO: Split apart, create a dedicated function. - // Separate "parse-logic" from "retrieve-state-logic" - // Allow DB-based backend - // Use IcingaWeb2 Multi-Backend-Support - $check_results = array(); - $hostFilter = array_keys($this->hosts); - - if ($this->state_type === self::HARD_STATE) { - $hostStateColumn = 'host_hard_state'; - $hostStateChangeColumn = 'host_last_hard_state_change'; - $serviceStateColumn = 'service_hard_state'; - $serviceStateChangeColumn = 'service_last_hard_state_change'; - } else { - $hostStateColumn = 'host_state'; - $hostStateChangeColumn = 'host_last_state_change'; - $serviceStateColumn = 'service_state'; - $serviceStateChangeColumn = 'service_last_state_change'; - } - $filter = Filter::matchAny(); - foreach ($hostFilter as $host) { - $filter->addFilter(Filter::where('host_name', $host)); - } - - if ($filter->isEmpty()) { - return $this; - } - - $hostStatus = $backend->select()->from('hostStatus', array( - 'hostname' => 'host_name', - 'last_state_change' => $hostStateChangeColumn, - 'in_downtime' => 'host_in_downtime', - 'ack' => 'host_acknowledged', - 'state' => $hostStateColumn - ))->applyFilter($filter)->getQuery()->fetchAll(); - - $serviceStatus = $backend->select()->from('serviceStatus', array( - 'hostname' => 'host_name', - 'service' => 'service_description', - 'last_state_change' => $serviceStateChangeColumn, - 'in_downtime' => 'service_in_downtime', - 'ack' => 'service_acknowledged', - 'state' => $serviceStateColumn - ))->applyFilter($filter)->getQuery()->fetchAll(); - - foreach ($serviceStatus as $row) { - $this->handleDbRow($row); - } - - foreach ($hostStatus as $row) { - $this->handleDbRow($row); - } - - ksort($this->root_nodes); - Benchmark::measure('Got states for business process ' . $this->getName()); - - return $this; - } - - protected function handleDbRow($row) - { - $key = $row->hostname; - if (property_exists($row, 'service')) { - $key .= ';' . $row->service; - } else { - $key .= ';Hoststatus'; - } - // We fetch more states than we need, so skip unknown ones - if (! $this->hasNode($key)) return; - $node = $this->getNode($key); - - if ($row->state !== null) { - $node->setState($row->state)->setMissing(false); - } - if ($row->last_state_change !== null) { - $node->setLastStateChange($row->last_state_change); - } - if ((int) $row->in_downtime === 1) { - $node->setDowntime(true); - } - if ((int) $row->ack === 1) { - $node->setAck(true); - } - } - + /** + * @return BpNode[] + */ public function getChildren() { return $this->getRootNodes(); } + /** + * @return int + */ public function countChildren() { return count($this->root_nodes); } + /** + * @return BpNode[] + */ public function getRootNodes() { ksort($this->root_nodes); return $this->root_nodes; } + public function listRootNodes() + { + $names = array_keys($this->root_nodes); + sort($names); + return $names; + } + public function getNodes() { return $this->nodes; @@ -441,6 +389,11 @@ class BusinessProcess return array_key_exists($name, $this->nodes); } + public function hasRootNode($name) + { + return array_key_exists($name, $this->root_nodes); + } + public function createService($host, $service) { $node = new ServiceNode( @@ -463,13 +416,53 @@ class BusinessProcess return $node; } - public function createImportedNode($config, $name) + public function listInvolvedHostNames() { - $node = new ImportedNode($this, (object) array('name' => $name, 'configName' => $config)); - $this->nodes[$name] = $node; + return array_keys($this->hosts); + } + + /** + * Create and attach a new process (BpNode) + * + * @param string $name Process name + * @param string $operator Operator (defaults to &) + * + * @return BpNode + */ + public function createBp($name, $operator = '&') + { + $node = new BpNode($this, (object) array( + 'name' => $name, + 'operator' => $operator, + 'child_names' => array(), + )); + + $this->addNode($name, $node); return $node; } + public function createMissingBp($name) + { + return $this->createBp($name)->setMissing(); + } + + public function createImportedNode($config, $name = null) + { + $params = (object) array('configName' => $config); + if ($name !== null) { + $params->node = $name; + } + + $node = new ImportedNode($this, $params); + $this->nodes[$node->getName()] = $node; + return $node; + } + + /** + * @param $name + * @return Node + * @throws Exception + */ public function getNode($name) { if (array_key_exists($name, $this->nodes)) { @@ -482,7 +475,12 @@ class BusinessProcess if ($pos !== false) { $host = substr($name, 0, $pos); $service = substr($name, $pos + 1); - return $this->createService($host, $service); + // TODO: deactivated, this scares me, test it + if ($service === 'Hoststatus') { + return $this->createHost($host); + } else { + return $this->createService($host, $service); + } } throw new Exception( @@ -490,13 +488,55 @@ class BusinessProcess ); } - public function addObjectName($name) + /** + * @param $name + * @return BpNode + * + * @throws NotFoundError + */ + public function getBpNode($name) { - $this->all_checks[$name] = 1; + if ($this->hasBpNode($name)) { + return $this->nodes[$name]; + } else { + throw new NotFoundError('Trying to access a missing business process node "%s"', $name); + } + } + + /** + * @param $name + * + * @return bool + */ + public function hasBpNode($name) + { + return array_key_exists($name, $this->nodes) + && $this->nodes[$name] instanceof BpNode; + } + + /** + * Set the state for a specific node + * + * @param string $name Node name + * @param int $state Desired state + * + * @return $this + */ + public function setNodeState($name, $state) + { + $this->getNode($name)->setState($state); return $this; } - public function addNode($name, Node $node) + /** + * Add the given node to the given BpNode + * + * @param $name + * @param BpNode $node + * + * @return $this + */ + public function addNode($name, BpNode $node) { if (array_key_exists($name, $this->nodes)) { $this->warn( @@ -523,16 +563,53 @@ class BusinessProcess return $this; } - public function listBpNodes() + /** + * Remove all occurrences of a specific node by name + * + * @param $name + */ + public function removeNode($name) + { + unset($this->nodes[$name]); + if (array_key_exists($name, $this->root_nodes)) { + unset($this->root_nodes[$name]); + } + + foreach ($this->getBpNodes() as $node) { + if ($node->hasChild($name)) { + $node->removeChild($name); + } + } + } + + /** + * Get all business process nodes + * + * @return BpNode[] + */ + public function getBpNodes() { $nodes = array(); foreach ($this->nodes as $node) { - if (! $node instanceof BpNode) { - continue; + if ($node instanceof BpNode) { + $nodes[$node->getName()] = $node; } + } - $name = (string) $node; + return $nodes; + } + + /** + * List all business process node names + * + * @return array + */ + public function listBpNodes() + { + $nodes = array(); + + foreach ($this->getBpNodes() as $name => $node) { $alias = $node->getAlias(); $nodes[$name] = $name === $alias ? $name : sprintf('%s (%s)', $alias, $node); } @@ -541,42 +618,57 @@ class BusinessProcess return $nodes; } + /** + * All business process nodes defined in this config but not + * assigned to any parent + * + * @return BpNode[] + */ public function getUnboundNodes() { $nodes = array(); - foreach ($this->nodes as $node) { - if (! $node instanceof BpNode) { - continue; - } + foreach ($this->getBpNodes() as $name => $node) { if ($node->hasParents()) { continue; } if ($node->getDisplay() === 0) { - $nodes[(string) $node] = $node; + $nodes[$name] = $node; } } return $nodes; } + /** + * @return bool + */ public function hasWarnings() { return ! empty($this->warnings); } + /** + * @return array + */ public function getWarnings() { return $this->warnings; } + /** + * @return bool + */ public function hasErrors() { return ! empty($this->errors) || $this->isEmpty(); } + /** + * @return array + */ public function getErrors() { $errors = $this->errors; @@ -591,104 +683,103 @@ class BusinessProcess return $errors; } - protected function translate($msg) + /** + * Translation helper + * + * @param $msg + * + * @return mixed|string + */ + public function translate($msg) { return mt('businessprocess', $msg); } + /** + * Add a message to our warning stack + * + * @param $msg + */ protected function warn($msg) { $args = func_get_args(); array_shift($args); - if (isset($this->parsing_line_number)) { - $this->warnings[] = sprintf( - $this->translate('Parser waring on %s:%s: %s'), - $this->filename, - $this->parsing_line_number, - vsprintf($msg, $args) - ); - } else { - $this->warnings[] = vsprintf($msg, $args); - } + $this->warnings[] = vsprintf($msg, $args); } - protected function error($msg) + /** + * @param string $msg,... + * + * @return $this + * + * @throws IcingaException + */ + public function addError($msg) { $args = func_get_args(); array_shift($args); - $this->errors[] = vsprintf($msg, $args); + $msg = vsprintf($msg, $args); + if ($this->throwErrors) { + throw new IcingaException($msg); + } + + $this->errors[] = $msg; + return $this; } - public function toLegacyConfigString() + /** + * Decide whether errors should be thrown or collected + * + * @param bool $throw + * + * @return $this + */ + public function throwErrors($throw = true) { - $settings = array(); - if ($this->hasTitle()) { - $settings['Title'] = $this->getTitle(); - } - // TODO: backendName? - if ($this->backend) { - $settings['Backend'] = $this->backend->getName(); - } - $settings['Statetype'] = $this->usesSoftStates() ? 'soft' : 'hard'; - - if (false) { - $settings['SLA Hosts'] = implode(', ', array()); - } - - $conf = "### Business Process Config File ###\n#\n"; - foreach ($settings as $key => $value) { - $conf .= sprintf("# %-9s : %s\n", $key, $value); - } - - $conf .= "#\n###################################\n\n"; - - $rendered = array(); - foreach ($this->getChildren() as $child) { - $conf .= $child->toLegacyConfigString($rendered); - $rendered[(string) $child] = true; - } - foreach ($this->getUnboundNodes() as $node) { - $conf .= $node->toLegacyConfigString($rendered); - $rendered[(string) $node] = true; - } - return $conf . "\n"; + $this->throwErrors = $throw; + return $this; } + /** + * Begin loop detection for the given name + * + * Will throw a NestingError in case this node will be met again below itself + * + * @param $name + * + * @throws NestingError + */ + public function beginLoopDetection($name) + { + // echo "Begin loop $name\n"; + if (array_key_exists($name, $this->loopDetection)) { + $loop = array_keys($this->loopDetection); + $loop[] = $name; + $this->loopDetection = array(); + throw new NestingError('Loop detected: %s', implode(' -> ', $loop)); + } + + $this->loopDetection[$name] = true; + } + + /** + * Remove the given name from the loop detection stack + * + * @param $name + */ + public function endLoopDetection($name) + { + // echo "End loop $this->name\n"; + unset($this->loopDetection[$name]); + } + + /** + * Whether this configuration has any Nodes + * + * @return bool + */ public function isEmpty() { return $this->countChildren() === 0; } - - public function renderHtml($view) - { - $html = ''; - foreach ($this->getRootNodes() as $name => $node) { - $html .= $node->renderHtml($view); - } - return $html; - } - - public function renderUnbound($view) - { - $html = ''; - - $unbound = $this->getUnboundNodes(); - if (empty($unbound)) { - return $html; - } - - $parent = new BpNode($this, (object) array( - 'name' => '__unbound__', - 'operator' => '|', - 'child_names' => array_keys($unbound) - )); - $parent->getState(); - $parent->setMissing() - ->setDowntime(false) - ->setAck(false) - ->setAlias('Unbound nodes'); - - $html .= $parent->renderHtml($view); - return $html; - } } diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index d9e91c4..cef6298 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -3,7 +3,7 @@ namespace Icinga\Module\Businessprocess; use Icinga\Exception\ConfigurationError; -use Icinga\Exception\ProgrammingError; +use Icinga\Module\Businessprocess\Exception\NestingError; class BpNode extends Node { @@ -14,11 +14,16 @@ class BpNode extends Node protected $url; protected $info_command; protected $display = 0; + + /** @var Node[] */ protected $children; - protected $child_names = array(); + + /** @var array */ + protected $childNames = array(); protected $alias; protected $counters; protected $missing = null; + protected $missingChildren; protected static $emptyStateSummary = array( 'OK' => 0, @@ -42,7 +47,7 @@ class BpNode extends Node protected $className = 'process'; - public function __construct(BusinessProcess $bp, $object) + public function __construct(BpConfig $bp, $object) { $this->bp = $bp; $this->name = $object->name; @@ -56,7 +61,7 @@ class BpNode extends Node $this->getState(); $this->counters = self::$emptyStateSummary; - foreach ($this->children as $child) { + foreach ($this->getChildren() as $child) { if ($child instanceof BpNode) { $counters = $child->getStateSummary(); foreach ($counters as $k => $v) { @@ -88,6 +93,30 @@ class BpNode extends Node return false; } + /** + * @param Node $node + * @return $this + * @throws ConfigurationError + */ + public function addChild(Node $node) + { + if ($this->children === null) { + $this->getChildren(); + } + + $name = $node->getName(); + if (array_key_exists($name, $this->children)) { + throw new ConfigurationError( + 'Node "%s" has been defined more than once', + $name + ); + } + $this->children[$name] = $node; + $this->childNames[] = $name; + $node->addParent($this); + return $this; + } + public function getProblematicChildren() { $problems = array(); @@ -103,6 +132,24 @@ class BpNode extends Node return $problems; } + public function hasChild($name) + { + return in_array($name, $this->childNames); + } + + public function removeChild($name) + { + if (($key = array_search($name, $this->childNames)) !== false) { + unset($this->childNames[$key]); + + if (! empty($this->children)) { + unset($this->children[$name]); + } + } + + return $this; + } + public function getProblemTree() { $tree = array(); @@ -135,6 +182,27 @@ class BpNode extends Node return $this->missing; } + public function getMissingChildren() + { + if ($this->missingChildren === null) { + $missing = array(); + + foreach ($this->getChildren() as $child) { + if ($child->isMissing()) { + $missing[(string) $child] = $child; + } + + foreach ($child->getMissingChildren() as $m) { + $missing[(string) $m] = $m; + } + } + + $this->missingChildren = $missing; + } + + return $this->missingChildren; + } + public function getOperator() { return $this->operator; @@ -174,7 +242,7 @@ class BpNode extends Node public function hasInfoUrl() { - return $this->url !== null; + return ! empty($this->url); } public function getInfoUrl() @@ -213,26 +281,60 @@ class BpNode extends Node return $this; } + /** + * @return int + */ public function getState() { if ($this->state === null) { - $this->calculateState(); + try { + $this->reCalculateState(); + } catch (NestingError $e) { + $this->bp->addError( + $this->bp->translate('Nesting error detected: %s'), + $e->getMessage() + ); + + // Failing nodes are unknown + $this->state = 3; + } } + return $this->state; } + public function getHtmlId() + { + return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', (string) $this); + } + protected function invertSortingState($state) { return self::$sortStateInversionMap[$state >> self::SHIFT_FLAGS] << self::SHIFT_FLAGS; } - protected function calculateState() + /** + * @return $this + */ + public function reCalculateState() { + $bp = $this->bp; + $sort_states = array(); $lastStateChange = 0; + + if (!$this->hasChildren()) { + // TODO: delegate this to operators, should mostly fail + $this->state = 3; + $this->setMissing(); + return $this; + } + foreach ($this->getChildren() as $child) { + $bp->beginLoopDetection($this->name); $sort_states[] = $child->getSortingState(); $lastStateChange = max($lastStateChange, $child->getLastStateChange()); + $bp->endLoopDetection($this->name); } $this->setLastStateChange($lastStateChange); @@ -266,6 +368,21 @@ class BpNode extends Node } $this->state = $this->sortStateTostate($sort_state); + return $this; + } + + public function checkForLoops() + { + $bp = $this->bp; + foreach ($this->getChildren() as $child) { + $bp->beginLoopDetection($this->name); + if ($child instanceof BpNode) { + $child->checkForLoops(); + } + $bp->endLoopDetection($this->name); + } + + return $this; } public function setDisplay($display) @@ -282,29 +399,51 @@ class BpNode extends Node public function setChildNames($names) { sort($names); - $this->child_names = $names; + $this->childNames = $names; $this->children = null; return $this; } + public function hasChildren($filter = null) + { + return !empty($this->childNames); + } + public function getChildNames() { - return $this->child_names; + return $this->childNames; } public function getChildren($filter = null) { if ($this->children === null) { $this->children = array(); - natsort($this->child_names); - foreach ($this->child_names as $name) { + natsort($this->childNames); + foreach ($this->childNames as $name) { $this->children[$name] = $this->bp->getNode($name); $this->children[$name]->addParent($this); } } + return $this->children; } + /** + * return BpNode[] + */ + public function getChildBpNodes() + { + $children = array(); + + foreach ($this->getChildren() as $name => $child) { + if ($child instanceof BpNode) { + $children[$name] = $child; + } + } + + return $children; + } + protected function assertNumericOperator() { if (! is_numeric($this->operator)) { @@ -312,76 +451,6 @@ class BpNode extends Node } } - protected function getActionIcons($view) - { - $icons = array(); - if (! $this->bp->isLocked() && $this->name !== '__unbound__') { - $icons[] = $this->actionIcon( - $view, - 'wrench', - $view->url('businessprocess/node/edit', array( - 'config' => $this->bp->getName(), - 'node' => $this->name - )), - mt('businessprocess', 'Modify this node') - ); - } - return $icons; - } - - public function toLegacyConfigString(& $rendered = array()) - { - $cfg = ''; - if (array_key_exists($this->name, $rendered)) { - return $cfg; - } - $rendered[$this->name] = true; - $children = array(); - - foreach ($this->getChildren() as $name => $child) { - $children[] = (string) $child; - if (array_key_exists($name, $rendered)) { continue; } - if ($child instanceof BpNode) { - $cfg .= $child->toLegacyConfigString($rendered) . "\n"; - } - } - $eq = '='; - $op = $this->operator; - if (is_numeric($op)) { - $eq = '= ' . $op . ' of:'; - $op = '+'; - } - - $strChildren = implode(' ' . $op . ' ', $children); - if ((count($children) < 2) && $op !== '&') { - $strChildren = $op . ' ' . $strChildren; - } - $cfg .= sprintf( - "%s %s %s\n", - $this->name, - $eq, - $strChildren - ); - if ($this->hasAlias() || $this->getDisplay() > 0) { - $prio = $this->getDisplay(); - $cfg .= sprintf( - "display %s;%s;%s\n", - $prio, - $this->name, - $this->getAlias() - ); - } - if ($this->hasInfoUrl()) { - $cfg .= sprintf( - "info_url;%s;%s\n", - $this->name, - $this->getInfoUrl() - ); - } - - return $cfg; - } - public function operatorHtml() { switch ($this->operator) { diff --git a/library/Businessprocess/ConfigDiff.php b/library/Businessprocess/ConfigDiff.php deleted file mode 100644 index 355e9b1..0000000 --- a/library/Businessprocess/ConfigDiff.php +++ /dev/null @@ -1,40 +0,0 @@ -a = $a; - $this->b = $b; - require_once dirname(__DIR__) . '/vendor/PHP-FineDiff/finediff.php'; - $granularity = FineDiff::$paragraphGranularity; // character, word, sentence, paragraph - $this->diff = new FineDiff($a, $b, $granularity); - } - - public function renderHtml() - { - return $this->diff->renderDiffToHTML(); - } - - public function __toString() - { - return $this->renderHtml(); - } - - public static function create($a, $b) - { - $diff = new static($a, $b); - return $diff; - } -} diff --git a/library/Businessprocess/Controller.php b/library/Businessprocess/Controller.php deleted file mode 100644 index d961dd9..0000000 --- a/library/Businessprocess/Controller.php +++ /dev/null @@ -1,164 +0,0 @@ -getModuleManager(); - if (! $m->hasLoaded('monitoring') && $m->hasInstalled('monitoring')) { - $m->loadModule('monitoring'); - } - $this->view->errors = array(); - - $this->view->compact = $this->params->get('view') === 'compact'; - } - - protected function url() - { - if ($this->url === null) { - $this->url = clone $this->getRequest()->getUrl(); - } - return $this->url; - } - - protected function tabs() - { - if ($this->view->tabs === null) { - $this->view->tabs = Widget::create('tabs'); - } - return $this->view->tabs; - } - - protected function session() - { - return $this->Window()->getSessionNamespace('businessprocess'); - } - - protected function setTitle($title) - { - $args = func_get_args(); - array_shift($args); - $this->view->title = vsprintf($title, $args); - } - - protected function loadModifiedBpConfig() - { - $bp = $this->loadBpConfig(); - $changes = ProcessChanges::construct($bp, $this->session()); - if ($this->params->get('dismissChanges')) { - Notification::success( - sprintf( - $this->translate('%d pending change(s) have been dropped'), - $changes->count() - ) - ); - $changes->clear(); - $this->redirectNow($this->url()->without('dismissChanges')->without('unlocked')); - } - $bp->applyChanges($changes); - return $bp; - } - - protected function loadBpConfig() - { - $storage = $this->storage(); - $this->view->processList = $storage->listProcesses(); - - // No process found? Go to welcome page - if (empty($this->view->processList)) { - $this->redirectNow('businessprocess'); - } - - $name = $this->params->get( - 'config', - key($this->view->processList) - ); - - $modifications = $this->session()->get('modifications', array()); - if (array_key_exists($name, $modifications)) { - $bp = $storage->loadFromString($name, $modifications[$name]); - } else { - $bp = $storage->loadProcess($name); - } - - // allow URL parameter to override configured state type - if (null !== ($stateType = $this->params->get('state_type'))) { - if ($stateType === 'soft') { - $bp->useSoftStates(); - } - if ($stateType === 'hard') { - $bp->useHardStates(); - } - } - - $this->view->bpconfig = $bp; - $this->view->configName = $bp->getName(); - - return $bp; - } - - public function loadForm($name) - { - return FormLoader::load($name, $this->Module()); - } - - protected function storage() - { - if ($this->storage === null) { - $this->storage = new LegacyStorage( - $this->Config()->getSection('global') - ); - } - - return $this->storage; - } - - protected function loadSlas() - { - $bpconf = $this->bpconf; - - if (! isset($bpconf->slahosts)) { - return array(); - } - - // TODO: This doesn't work right now - $sla_hosts = preg_split('~\s*,s*~', $bpconf->slahosts, -1, PREG_SPLIT_NO_EMPTY); - - if (isset($bpconf->sla_year)) { - $start = mktime(0, 0, 0, 1, 1, $bpconf->sla_year); - $end = mktime(23, 59, 59, 1, 0, $bpconf->sla_year + 1); - } else { - $start = mktime(0, 0, 0, 1, 1, (int) date('Y')); - $end = null; - // Bis zum Jahresende hochrechnen: - // $end = mktime(23, 59, 59, 1, 0, (int) date('Y') + 1); - } - - return $this->backend - ->module('BpAddon') - ->getBpSlaValues($sla_hosts, $start, $end); - } -} diff --git a/library/Businessprocess/Director/ShipConfigFiles.php b/library/Businessprocess/Director/ShipConfigFiles.php index 8ebf194..35019d9 100644 --- a/library/Businessprocess/Director/ShipConfigFiles.php +++ b/library/Businessprocess/Director/ShipConfigFiles.php @@ -3,7 +3,7 @@ namespace Icinga\Module\Businessprocess\Director; use Icinga\Application\Config; -use Icinga\Module\Director\Web\Hook\ShipConfigFilesHook; +use Icinga\Module\Director\Hook\ShipConfigFilesHook; use Icinga\Module\Businessprocess\Storage\LegacyStorage; class ShipConfigFiles extends ShipConfigFilesHook diff --git a/library/Businessprocess/Exception/NestingError.php b/library/Businessprocess/Exception/NestingError.php new file mode 100644 index 0000000..89cbf81 --- /dev/null +++ b/library/Businessprocess/Exception/NestingError.php @@ -0,0 +1,9 @@ + self::ICINGA_DOWN, 3 => self::ICINGA_UNREACHABLE, 1 => self::ICINGA_PENDING, 0 => self::ICINGA_UP ); - protected static $stateToSortStateMap = array( + protected $stateToSortStateMap = array( self::ICINGA_PENDING => 1, self::ICINGA_UNREACHABLE => 3, self::ICINGA_DOWN => 4, self::ICINGA_UP => 0, ); - protected static $state_names = array( + protected $stateNames = array( 'UP', 'DOWN', 'UNREACHABLE', @@ -31,7 +32,7 @@ class HostNode extends Node protected $className = 'host'; - public function __construct(BusinessProcess $bp, $object) + public function __construct(BpConfig $bp, $object) { $this->name = $object->hostname . ';Hoststatus'; $this->hostname = $object->hostname; @@ -43,46 +44,31 @@ class HostNode extends Node } } - public function renderLink($view) + public function getAlias() { - if ($this->isMissing()) { - return '' . $view->escape($this->hostname) . ''; - } - - $params = array( - 'host' => $this->getHostname(), - ); - - if ($this->bp->hasBackendName()) { - $params['backend'] = $this->bp->getBackendName(); - } - return $view->qlink($this->hostname, 'monitoring/host/show', $params); - } - - protected function getActionIcons($view) - { - $icons = array(); - - if (! $this->bp->isLocked()) { - - $url = Url::fromPath( 'businessprocess/node/simulate', array( - 'config' => $this->bp->getName(), - 'node' => $this->name - )); - - $icons[] = $this->actionIcon( - $view, - 'magic', - $url, - 'Simulation' - ); - } - - return $icons; + return $this->getHostname(); } public function getHostname() { return $this->hostname; } + + public function getUrl() + { + $params = array( + 'host' => $this->getHostname(), + ); + + if ($this->bp->hasBackendName()) { + $params['backend'] = $this->bp->getBackendName(); + } + + return Url::fromPath('monitoring/host/show', $params); + } + + public function getLink() + { + return Link::create($this->hostname, $this->getUrl()); + } } diff --git a/library/Businessprocess/Html/Attribute.php b/library/Businessprocess/Html/Attribute.php new file mode 100644 index 0000000..9012c3c --- /dev/null +++ b/library/Businessprocess/Html/Attribute.php @@ -0,0 +1,130 @@ +name = $name; + $this->value = $value; + } + + /** + * @param $name + * @param $value + * @return static + */ + public static function create($name, $value) + { + return new static($name, $value); + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * @param string|array $value + * @return $this + */ + public function setValue($value) + { + $this->value = $value; + return $this; + } + + /** + * @param string $value + * @return $this + */ + public function addValue($value) + { + if (! is_array($this->value)) { + $this->value = array($this->value); + } + + if (is_array($value)) { + $this->value = array_merge($this->value, $value); + } else { + $this->value[] = $value; + } + return $this; + } + + /** + * @return string + */ + public function render() + { + return sprintf( + '%s="%s"', + $this->renderName(), + $this->renderValue() + ); + } + + /** + * @return string + */ + public function renderName() + { + return static::escapeName($this->name); + } + + /** + * @return string + */ + public function renderValue() + { + return static::escapeValue($this->value); + } + + /** + * @param $name + * @return string + */ + public static function escapeName($name) + { + // TODO: escape + return (string) $name; + } + + /** + * @param $value + * @return string + */ + public static function escapeValue($value) + { + // TODO: escape differently + if (is_array($value)) { + return Util::escapeForHtml(implode(' ', $value)); + } else { + return Util::escapeForHtml((string) $value); + } + } +} diff --git a/library/Businessprocess/Html/Attributes.php b/library/Businessprocess/Html/Attributes.php new file mode 100644 index 0000000..77099cc --- /dev/null +++ b/library/Businessprocess/Html/Attributes.php @@ -0,0 +1,195 @@ + $value) { + if ($value instanceof Attribute) { + $this->addAttribute($value); + } elseif (is_string($key)) { + $this->add($key, $value); + } elseif (is_array($value) && count($value) === 2) { + $this->add(array_shift($value), array_shift($value)); + } + } + } + + /** + * @param Attribute[] $attributes + * @return static + */ + public static function create(array $attributes = null) + { + return new static($attributes); + } + + /** + * @param Attributes|array|null $attributes + * @return Attributes + * @throws IcingaException + */ + public static function wantAttributes($attributes) + { + if ($attributes instanceof Attributes) { + return $attributes; + } else { + $self = new static(); + if (is_array($attributes)) { + foreach ($attributes as $k => $v) { + $self->add($k, $v); + } + + return $self; + + } elseif ($attributes !== null) { + throw new IcingaException( + 'Attributes, Array or Null expected, got %s', + Util::getPhpTypeName($attributes) + ); + } + return $self; + } + } + + /** + * @return Attribute[] + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * @param Attribute|string $attribute + * @param string|array $value + * @return $this + */ + public function add($attribute, $value = null) + { + if ($attribute instanceof static) { + foreach ($attribute->getAttributes() as $a) { + $this->add($a); + } + } elseif ($attribute instanceof Attribute) { + $this->addAttribute($attribute); + } elseif (is_array($attribute)) { + foreach ($attribute as $name => $value) { + $this->add($name, $value); + } + } else { + $this->addAttribute(Attribute::create($attribute, $value)); + } + + return $this; + } + + /** + * @param Attribute|string $attribute + * @param string|array $value + * @return $this + */ + public function set($attribute, $value = null) + { + if ($attribute instanceof static) { + foreach ($attribute as $a) { + $this->setAttribute($a); + } + + return $this; + } elseif ($attribute instanceof Attribute) { + return $this->setAttribute($attribute); + } else { + return $this->setAttribute(new Attribute($attribute, $value)); + } + } + + /** + * @param Attribute $attribute + * @return $this + */ + public function addAttribute(Attribute $attribute) + { + $name = $attribute->getName(); + if (array_key_exists($name, $this->attributes)) { + $this->attributes[$name]->addValue($attribute->getValue()); + } else { + $this->attributes[$name] = $attribute; + } + + return $this; + } + + /** + * @param Attribute $attribute + * @return $this + */ + public function setAttribute(Attribute $attribute) + { + $name = $attribute->getName(); + $this->attributes[$name] = $attribute; + return $this; + } + + /** + * Callback must return an instance of Attribute + * + * @param string $name + * @param callable $callback + * @return $this + */ + public function registerCallbackFor($name, $callback) + { + if (! is_callable($callback)) { + throw new ProgrammingError('registerCallBack expects a callable callback'); + } + $this->callbacks[$name] = $callback; + return $this; + } + + /** + * @inheritdoc + */ + public function render() + { + if (empty($this->attributes) && empty($this->callbacks)) { + return ''; + } + + $parts = array(); + foreach ($this->callbacks as $callback) { + $attribute = call_user_func($callback); + if (! $attribute instanceof Attribute) { + throw new ProgrammingError( + 'A registered attribute callback must return an Attribute' + ); + } + + $parts[] = $attribute->render(); + } + + foreach ($this->attributes as $attribute) { + $parts[] = $attribute->render(); + } + return ' ' . implode(' ', $parts); + } +} diff --git a/library/Businessprocess/Html/BaseElement.php b/library/Businessprocess/Html/BaseElement.php new file mode 100644 index 0000000..564fc22 --- /dev/null +++ b/library/Businessprocess/Html/BaseElement.php @@ -0,0 +1,117 @@ +attributes === null) { + $default = $this->getDefaultAttributes(); + if (empty($default)) { + $this->attributes = new Attributes(); + } else { + $this->attributes = Attributes::wantAttributes($default); + } + } + + return $this->attributes; + } + + /** + * @param Attributes|array|null $attributes + * @return $this + */ + public function setAttributes($attributes) + { + $this->attributes = Attributes::wantAttributes($attributes); + return $this; + } + + /** + * @param Attributes|array|null $attributes + * @return $this + */ + public function addAttributes($attributes) + { + $this->attributes = Attributes::wantAttributes($attributes); + return $this; + } + + public function getDefaultAttributes() + { + return $this->defaultAttributes; + } + + public function setTag($tag) + { + $this->tag = $tag; + return $this; + } + + public function getTag() + { + return $this->tag; + } + + /** + * Container constructor. + * + * @param string $tag + * @param Attributes|array $attributes + * + * @return Element + */ + public function createElement($tag, $attributes = null) + { + $element = Element::create($tag, $attributes); + $this->add($element); + return $element; + } + + public function renderContent() + { + return parent::render(); + } + + /** + * @return string + */ + public function render() + { + $tag = $this->getTag(); + + return sprintf( + '<%s%s>%s', + $tag, + $this->attributes()->render(), + $this->renderContent(), + $tag + ); + } + + /** + * Whether the given something can be rendered + * + * @param mixed $any + * @return bool + */ + protected function canBeRendered($any) + { + return is_string($any) || is_int($any) || is_null($any); + } +} diff --git a/library/Businessprocess/Html/Container.php b/library/Businessprocess/Html/Container.php new file mode 100644 index 0000000..f838721 --- /dev/null +++ b/library/Businessprocess/Html/Container.php @@ -0,0 +1,40 @@ +setContent($content); + } + + if ($attributes !== null) { + $container->setAttributes($attributes); + } + if ($tag !== null) { + $container->setTag($tag); + } + + return $container; + } +} diff --git a/library/Businessprocess/Html/Element.php b/library/Businessprocess/Html/Element.php new file mode 100644 index 0000000..2f19f7e --- /dev/null +++ b/library/Businessprocess/Html/Element.php @@ -0,0 +1,34 @@ +tag = $tag; + + if ($attributes !== null) { + $this->attributes = $this->attributes()->add($attributes); + } + } + + /** + * Container constructor. + * + * @param string $tag + * @param Attributes|array $attributes + * + * @return static + */ + public static function create($tag, $attributes = null) + { + return new static($tag, $attributes); + } +} diff --git a/library/Businessprocess/Html/Html.php b/library/Businessprocess/Html/Html.php new file mode 100644 index 0000000..d4e46f6 --- /dev/null +++ b/library/Businessprocess/Html/Html.php @@ -0,0 +1,157 @@ +content[] = $element; + return $this; + } + + /** + * @param Renderable|array|string $content + * @return $this + */ + public function setContent($content) + { + $this->content = array( + static::escape($content) + ); + + return $this; + } + + /** + * @param Renderable|array|string $content + * @return $this + */ + public function addContent($content) + { + $this->content[] = static::escape($content); + return $this; + } + + /** + * @param Renderable|array|string $content + * @return $this + */ + public function prependContent($content) + { + array_unshift($this->content, static::escape($content)); + return $this; + } + + /** + * return Html + */ + public function getContent() + { + if ($this->content === null) { + $this->content = array(new Html()); + } + + return $this->content; + } + + public function hasContent() + { + if ($this->content === null) { + return false; + } + + // TODO: unfinished + // return $this->content->isEmpty(); + return true; + } + + /** + * @param $separator + * @return $this + */ + public function setSeparator($separator) + { + $this->contentSeparator = $separator; + return $this; + } + + /** + * @inheritdoc + */ + public function render() + { + $html = array(); + + foreach ($this->content as $element) { + if (is_string($element)) { + var_dump($this->content); + } + $html[] = $element->render(); + } + + return implode($this->contentSeparator, $html); + } + + protected function translate($msg) + { + // TODO: Not so nice + return mt('businessprocess', $msg); + } + + public static function element($name, $attributes = null) + { + // TODO: This might be anything here, add a better check + if (! ctype_alnum($name)) { + throw new ProgrammingError('Invalid element requested'); + } + + $class = __NAMESPACE__ . '\\' . $name; + /** @var Element $element */ + $element = new $class(); + if ($attributes !== null) { + $element->setAttributes($attributes); + } + + return $element; + } + + /** + * @param Exception|string $error + * @return string + */ + protected function renderError($error) + { + return Util::renderError($error); + } + + /** + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return $this->renderError($e); + } + } +} diff --git a/library/Businessprocess/Html/HtmlString.php b/library/Businessprocess/Html/HtmlString.php new file mode 100644 index 0000000..5232805 --- /dev/null +++ b/library/Businessprocess/Html/HtmlString.php @@ -0,0 +1,8 @@ +setContent($content); + } + + /** + * @param $content + * @param Attributes|array $attributes + * + * @return Element + */ + public static function p($content, $attributes = null) + { + return Element::create('p', $attributes)->setContent($content); + } +} diff --git a/library/Businessprocess/Html/Icon.php b/library/Businessprocess/Html/Icon.php new file mode 100644 index 0000000..afe4c22 --- /dev/null +++ b/library/Businessprocess/Html/Icon.php @@ -0,0 +1,26 @@ +setAttributes($attributes); + $icon->attributes()->add('class', array('icon', 'icon-' . $name)); + return $icon; + } +} diff --git a/library/Businessprocess/Html/Img.php b/library/Businessprocess/Html/Img.php new file mode 100644 index 0000000..eccd4a8 --- /dev/null +++ b/library/Businessprocess/Html/Img.php @@ -0,0 +1,72 @@ + ''); + + protected function __construct() + { + } + + /** + * @param Url|string $url + * @param array $urlParams + * @param array $attributes + * + * @return static + */ + public static function create($url, $urlParams = null, array $attributes = null) + { + $img = new static(); + $img->setAttributes($attributes); + $img->attributes()->registerCallbackFor('src', array($img, 'getSrcAttribute')); + $img->setUrl($url, $urlParams); + return $img; + } + + public function setUrl($url, $urlParams) + { + if ($url instanceof WebUrl) { // Hint: Url is also a WebUrl + if ($urlParams !== null) { + $url->addParams($urlParams); + } + + $this->url = $url; + } else { + if ($urlParams === null) { + $this->url = Url::fromPath($url); + } else { + $this->url = Url::fromPath($url, $urlParams); + } + } + + $this->url->getParams(); + } + + /** + * @return Attribute + */ + public function getSrcAttribute() + { + return new Attribute('src', $this->getUrl()->getAbsoluteUrl('&')); + } + + /** + * @return Url + */ + public function getUrl() + { + // TODO: What if null? #? + return $this->url; + } +} diff --git a/library/Businessprocess/Html/Link.php b/library/Businessprocess/Html/Link.php new file mode 100644 index 0000000..052698f --- /dev/null +++ b/library/Businessprocess/Html/Link.php @@ -0,0 +1,72 @@ +setContent($content); + $link->setAttributes($attributes); + $link->attributes()->registerCallbackFor('href', array($link, 'getHrefAttribute')); + $link->setUrl($url, $urlParams); + return $link; + } + + public function setUrl($url, $urlParams) + { + if ($url instanceof WebUrl) { // Hint: Url is also a WebUrl + if ($urlParams !== null) { + $url->addParams($urlParams); + } + + $this->url = $url; + } else { + if ($urlParams === null) { + $this->url = Url::fromPath($url); + } else { + $this->url = Url::fromPath($url, $urlParams); + } + } + + $this->url->getParams(); + } + + /** + * @return Attribute + */ + public function getHrefAttribute() + { + return new Attribute('href', $this->getUrl()->getAbsoluteUrl('&')); + } + + /** + * @return Url + */ + public function getUrl() + { + // TODO: What if null? #? + return $this->url; + } +} diff --git a/library/Businessprocess/Html/Renderable.php b/library/Businessprocess/Html/Renderable.php new file mode 100644 index 0000000..372baa4 --- /dev/null +++ b/library/Businessprocess/Html/Renderable.php @@ -0,0 +1,8 @@ +string = (string) $string; + } + + /** + * @return string + */ + public function getText() + { + return $this->string; + } + + /** + * @param bool $escaped + * @return $this + */ + public function setEscaped($escaped = true) + { + $this->escaped = $escaped; + return $this; + } + + /** + * @param $text + * + * @return static + */ + public static function create($text) + { + return new static($text); + } + + /** + * @return string + */ + public function render() + { + if ($this->escaped) { + return $this->string; + } else { + return Util::escapeForHtml($this->string); + } + } + + /** + * @param Exception|string $error + * @return string + */ + protected function renderError($error) + { + return Util::renderError($error); + } + + /** + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return $this->renderError($e); + } + } +} diff --git a/library/Businessprocess/Html/Util.php b/library/Businessprocess/Html/Util.php new file mode 100644 index 0000000..ee82eb0 --- /dev/null +++ b/library/Businessprocess/Html/Util.php @@ -0,0 +1,130 @@ +getFile(), -1, PREG_SPLIT_NO_EMPTY); + $file = array_pop($file); + $msg = sprintf( + '%s (%s:%d)', + $error->getMessage(), + $file, + $error->getLine() + ); + } elseif (is_string($error)) { + $msg = $error; + } else { + $msg = 'Got an invalid error'; // TODO: translate? + } + + return sprintf( + 'ERROR: %s', // TODO: translate? + static::escapeForHtml($msg) + ); + } + + /** + * @param $any + * @return Renderable + * @throws IcingaException + */ + public static function wantHtml($any) + { + if ($any instanceof Renderable) { + return $any; + } elseif (static::canBeRenderedAsString($any)) { + return new Text($any); + } elseif (is_array($any)) { + $html = new Html(); + foreach ($any as $el) { + $html->add(static::wantHtml($el)); + } + + return $html; + } else { + // TODO: Should we add a dedicated Exception class? + throw new IcingaException( + 'String, Html Element or Array of such expected, got "%s"', + Util::getPhpTypeName($any) + ); + } + } + + public static function canBeRenderedAsString($any) + { + return is_string($any) || is_int($any) || is_null($any); + } + + /** + * @param $any + * @return string + */ + public static function getPhpTypeName($any) + { + if (is_object($any)) { + return get_class($any); + } else { + return gettype($any); + } + } + + /** + * This defines the flags used when escaping for HTML + * + * - Single quotes are not escaped (ENT_COMPAT) + * - With PHP >= 5.4, invalid characters are replaced with � (ENT_SUBSTITUTE) + * - With PHP 5.3 they are ignored (ENT_IGNORE, less secure) + * - Uses HTML5 entities for PHP >= 5.4, disallowing + * + * @return int + */ + protected static function htmlEscapeFlags() + { + if (self::$htmlEscapeFlags === null) { + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { + self::$htmlEscapeFlags = ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5; + } else { + self::$htmlEscapeFlags = ENT_COMPAT | ENT_IGNORE; + } + } + + return self::$htmlEscapeFlags; + } +} diff --git a/library/Businessprocess/ImportedNode.php b/library/Businessprocess/ImportedNode.php index f829b92..e21a169 100644 --- a/library/Businessprocess/ImportedNode.php +++ b/library/Businessprocess/ImportedNode.php @@ -3,25 +3,42 @@ namespace Icinga\Module\Businessprocess; use Icinga\Application\Config; -use Icinga\Web\Url; +use Icinga\Module\Businessprocess\Html\Link; use Icinga\Module\Businessprocess\Storage\LegacyStorage; +use Icinga\Module\Businessprocess\Web\Url; use Exception; class ImportedNode extends Node { + /** @var string */ protected $configName; - protected $importedBp; + /** @var string */ + protected $nodeName; - protected $importedNode; + /** @var BpNode */ + private $node; protected $className = 'subtree'; - public function __construct(BusinessProcess $bp, $object) + /** @var BpConfig */ + protected $config; + + /** + * @inheritdoc + */ + public function __construct(BpConfig $bp, $object) { - $this->name = $object->name; + $this->bp = $bp; $this->configName = $object->configName; - $this->bp = $bp; + $this->name = '@' . $object->configName; + if (property_exists($object, 'node')) { + $this->nodeName = $object->node; + $this->name .= ':' . $object->node; + } else { + $this->useAllRootNodes(); + } + if (isset($object->state)) { $this->setState($object->state); } else { @@ -29,108 +46,189 @@ class ImportedNode extends Node } } + public function hasNode() + { + return $this->nodeName !== null; + } + + /** + * @return string + */ public function getConfigName() { return $this->configName; } + /** + * @inheritdoc + */ public function getState() { if ($this->state === null) { + try { + $this->importedConfig()->retrieveStatesFromBackend(); + } catch (Exception $e) { + } + $this->state = $this->importedNode()->getState(); } return $this->state; } + /** + * @inheritdoc + */ public function getAlias() { return $this->importedNode()->getAlias(); } - public function isMissing() + public function getUrl() { - return $this->importedNode()->isMissing(); - // TODO: WHY? return $this->getState() === null; + $params = array( + 'config' => $this->getConfigName(), + 'node' => $this->importedNode()->getName() + ); + + return Url::fromPath('businessprocess/process/show', $params); } + /** + * @inheritdoc + */ + public function isMissing() + { + // TODO: WHY? return $this->getState() === null; + return $this->importedNode()->isMissing(); + } + + /** + * @inheritdoc + */ public function isInDowntime() { if ($this->downtime === null) { + $this->getState(); $this->downtime = $this->importedNode()->isInDowntime(); } + return $this->downtime; } + /** + * @inheritdoc + */ public function isAcknowledged() { if ($this->ack === null) { + $this->getState(); $this->downtime = $this->importedNode()->isAcknowledged(); } + return $this->ack; } + /** + * @return BpNode + */ protected function importedNode() { - if ($this->importedNode === null) { - $storage = new LegacyStorage( - Config::module('businessprocess')->getSection('global') - ); - try { - $this->importedBp = $storage->loadProcess($this->configName); - if ($this->bp->usesSoftStates()) { - $this->importedBp->useSoftStates(); - } else { - $this->importedBp->useHardStates(); - } - $this->importedBp->retrieveStatesFromBackend(); - $this->importedNode = $this->importedBp->getNode($this->name); - } catch (Exception $e) { - - - $node = new BpNode($this->bp, (object) array( - 'name' => $this->name, - 'operator' => '&', - 'child_names' => array() - )); - $node->setState(2); - $node->setMissing(false) - ->setDowntime(false) - ->setAck(false) - ->setAlias($e->getMessage()); - - $this->importedNode = $node; - } + if ($this->node === null) { + $this->node = $this->loadImportedNode(); } - return $this->importedNode; + + return $this->node; } - - protected function getActionIcons($view) + + /** + * @return BpNode + */ + protected function loadImportedNode() { - $icons = array(); + try { + $import = $this->importedConfig(); - if (! $this->bp->isLocked()) { + return $import->getNode($this->nodeName); + } catch (Exception $e) { + return $this->createFailedNode($e); + } + } - $url = Url::fromPath( 'businessprocess/node/simulate', array( - 'config' => $this->bp->getName(), - 'node' => $this->name + protected function useAllRootNodes() + { + try { + $bp = $this->importedConfig(); + $this->node = new BpNode($bp, (object) array( + 'name' => $this->getName(), + 'operator' => '&', + 'child_names' => $bp->listRootNodes(), )); + } catch (Exception $e) { - $icons[] = $this->actionIcon( - $view, - 'magic', - $url, - 'Simulation' - ); + $this->createFailedNode($e); } - - return $icons; } - public function renderLink($view) + protected function importedConfig() { - return $view->qlink($this->getAlias(), 'businessprocess/process/show', array( - 'config' => $this->configName, - 'process' => $this->name + if ($this->config === null) { + $import = $this->storage()->loadProcess($this->configName); + if ($this->bp->usesSoftStates()) { + $import->useSoftStates(); + } else { + $import->useHardStates(); + } + + $this->config = $import; + } + + return $this->config; + } + + /** + * @return LegacyStorage + */ + protected function storage() + { + return new LegacyStorage( + Config::module('businessprocess')->getSection('global') + ); + } + + /** + * @param Exception $e + * + * @return BpNode + */ + protected function createFailedNode(Exception $e) + { + $this->bp->addError($e->getMessage()); + $node = new BpNode($this->importedConfig(), (object) array( + 'name' => $this->getName(), + 'operator' => '&', + 'child_names' => array() )); + $node->setState(2); + $node->setMissing(false) + ->setDowntime(false) + ->setAck(false) + ->setAlias($e->getMessage()); + + return $node; + } + + /** + * @inheritdoc + */ + public function getLink() + { + return Link::create( + $this->getAlias(), + 'businessprocess/process/show', + array( + 'config' => $this->configName, + 'process' => $this->name + ) + ); } } diff --git a/library/Businessprocess/Metadata.php b/library/Businessprocess/Metadata.php new file mode 100644 index 0000000..925c29d --- /dev/null +++ b/library/Businessprocess/Metadata.php @@ -0,0 +1,244 @@ + null, + 'Description' => null, + 'Owner' => null, + 'AllowedUsers' => null, + 'AllowedGroups' => null, + 'AllowedRoles' => null, + 'AddToMenu' => null, + 'Backend' => null, + 'Statetype' => null, + // 'SLAHosts' => null + ); + + public function __construct($name) + { + $this->name = $name; + } + + public function getTitle() + { + if ($this->has('Title')) { + return $this->get('Title'); + } else { + return $this->name; + } + } + + public function getExtendedTitle() + { + $title = $this->getTitle(); + + if ($title === $this->name) { + return $title; + } else { + return sprintf('%s (%s)', $title, $this->name); + } + } + + public function getProperties() + { + return $this->properties; + } + + public function hasKey($key) + { + return array_key_exists($key, $this->properties); + } + + public function get($key, $default = null) + { + $this->assertKeyExists($key); + if ($this->properties[$key] === null) { + return $default; + } + + return $this->properties[$key]; + } + + public function set($key, $value) + { + $this->assertKeyExists($key); + $this->properties[$key] = $value; + + return $this; + } + + public function isNull($key) + { + return null === $this->get($key); + } + + public function has($key) + { + return null !== $this->get($key); + } + + protected function assertKeyExists($key) + { + if (! $this->hasKey($key)) { + throw new ProgrammingError('Trying to access invalid header key: %s', $key); + } + + return $this; + } + + public function hasRestrictions() + { + return ! ( + $this->isNull('AllowedUsers') + && $this->isNull('AllowedGroups') + && $this->isNull('AllowedRoles') + ); + } + + protected function getAuth() + { + return Auth::getInstance(); + } + + public function canModify(Auth $auth = null) + { + if ($auth === null) { + if (Icinga::app()->isCli()) { + return true; + } else { + $auth = $this->getAuth(); + } + } + + return $this->canRead($auth) && ( + $auth->hasPermission('businessprocess/modify') + || $this->ownerIs($auth->getUser()->getUsername()) + ); + } + + public function canRead(Auth $auth = null) + { + if ($auth === null) { + if (Icinga::app()->isCli()) { + return true; + } else { + $auth = $this->getAuth(); + } + } + + if ($auth->hasPermission('businessprocess/showall')) { + return true; + } + + if (! $this->hasRestrictions()) { + return true; + } + + if (! $auth->isAuthenticated()) { + return false; + } + + return $this->userCanRead($auth->getUser()); + } + + protected function userCanRead(User $user) + { + $username = $user->getUsername(); + + return $this->ownerIs($username) + || $this->isInAllowedUserList($username) + || $this->isMemberOfAllowedGroups($user) + || $this->hasOneOfTheAllowedRoles($user); + } + + public function ownerIs($username) + { + return $this->get('Owner') === $username; + } + + public function listAllowedUsers() + { + // TODO: $this->get('AllowedUsers', array()); + $list = $this->get('AllowedUsers'); + if ($list === null) { + return array(); + } else { + return $this->splitCommaSeparated($list); + } + } + + public function listAllowedGroups() + { + $list = $this->get('AllowedGroups'); + if ($list === null) { + return array(); + } else { + return $this->splitCommaSeparated($list); + } + } + + public function listAllowedRoles() + { + $list = $this->get('AllowedRoles'); + if ($list === null) { + return array(); + } else { + return $this->splitCommaSeparated($list); + } + } + + public function isInAllowedUserList($username) + { + foreach ($this->listAllowedUsers() as $allowedUser) { + if ($username === $allowedUser) { + return true; + } + } + + return false; + } + + public function isMemberOfAllowedGroups(User $user) + { + foreach ($this->listAllowedGroups() as $groups) { + foreach ($groups as $group) { + if ($user->isMemberOf($group)) { + return true; + } + } + } + + return false; + } + + public function hasOneOfTheAllowedRoles(User $user) + { + foreach ($this->listAllowedRoles() as $roles) { + foreach ($roles as $roleName) { + foreach ($user->getRoles() as $role) { + if ($role->getName() === $roleName) { + return true; + } + } + } + } + + return false; + } + + protected function splitCommaSeparated($string) + { + return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY); + } +} diff --git a/library/Businessprocess/Modification/NodeAction.php b/library/Businessprocess/Modification/NodeAction.php new file mode 100644 index 0000000..c93f13a --- /dev/null +++ b/library/Businessprocess/Modification/NodeAction.php @@ -0,0 +1,160 @@ +nodeName = (string) $node; + } + } + + /** + * Every NodeAction must be able to apply itself to a BusinessProcess + * + * @param BpConfig $config + * @return mixed + */ + abstract public function applyTo(BpConfig $config); + + /** + * Every NodeAction must be able to tell whether it could be applied to a BusinessProcess + * + * @param BpConfig $config + * @return bool + */ + abstract public function appliesTo(BpConfig $config); + + /** + * The name of the node this modification applies to + * + * @return string + */ + public function getNodeName() + { + return $this->nodeName; + } + + public function hasNode() + { + return $this->nodeName !== null; + } + + /** + * Whether this is an instance of a given action name + * + * @param string $actionName + * @return bool + */ + public function is($actionName) + { + return $this->getActionName() === $actionName; + } + + /** + * Create an instance of a given actionName for a specific Node + * + * @param string $actionName + * @param string $nodeName + * + * @return static + */ + public static function create($actionName, $nodeName) + { + $className = __NAMESPACE__ . '\\Node' . ucfirst($actionName) . 'Action'; + $object = new $className($nodeName); + return $object; + } + + /** + * Returns a JSON-encoded serialized NodeAction + * + * @return string + */ + public function serialize() + { + $object = (object) array( + 'actionName' => $this->getActionName(), + 'nodeName' => $this->getNodeName(), + 'properties' => array() + ); + + foreach ($this->preserveProperties as $key) { + $func = 'get' . ucfirst($key); + $object->properties[$key] = $this->$func(); + } + + return json_encode($object); + } + + /** + * Decodes a JSON-serialized NodeAction and returns an object instance + * + * @param $string + * @return NodeAction + */ + public static function unSerialize($string) + { + $object = json_decode($string); + $action = self::create($object->actionName, $object->nodeName); + + foreach ($object->properties as $key => $val) { + $func = 'set' . ucfirst($key); + $action->$func($val); + } + + return $action; + } + + /** + * Returns the defined action name or determines such from the class name + * + * @return string The action name + * + * @throws ProgrammingError when no such class exists + */ + public function getActionName() + { + if ($this->actionName === null) { + if (! preg_match('/\\\Node(\w+)Action$/', get_class($this), $m)) { + throw new ProgrammingError( + '"%s" is not a NodeAction class', + get_class($this) + ); + } + $this->actionName = lcfirst($m[1]); + } + + return $this->actionName; + } +} diff --git a/library/Businessprocess/Modification/NodeAddChildrenAction.php b/library/Businessprocess/Modification/NodeAddChildrenAction.php new file mode 100644 index 0000000..dfd2ac2 --- /dev/null +++ b/library/Businessprocess/Modification/NodeAddChildrenAction.php @@ -0,0 +1,80 @@ +getNodeName(); + + if (! $config->hasNode($name)) { + return false; + } + + return $config->getNode($name) instanceof BpNode; + } + + /** + * @inheritdoc + */ + public function applyTo(BpConfig $config) + { + /** @var BpNode $node */ + if (! $this->hasNode()) { + // TODO: We end up here when defining "top nodes", but that would probably + // be a different action + return $this; + } + + $node = $config->getBpNode($this->getNodeName()); + + foreach ($this->children as $name) { + if (! $config->hasNode($name)) { + if (strpos($name, ';') !== false) { + list($host, $service) = preg_split('/;/', $name, 2); + + if ($service === 'Hoststatus') { + $config->createHost($host); + } else { + $config->createService($host, $service); + } + } + } + $node->addChild($config->getNode($name)); + } + + return $this; + } + + /** + * @param array|string $children + * @return $this + */ + public function setChildren($children) + { + if (is_string($children)) { + $children = array($children); + } + $this->children = $children; + return $this; + } + + /** + * @return array + */ + public function getChildren() + { + return $this->children; + } +} diff --git a/library/Businessprocess/Modification/NodeCreateAction.php b/library/Businessprocess/Modification/NodeCreateAction.php new file mode 100644 index 0000000..69dc77c --- /dev/null +++ b/library/Businessprocess/Modification/NodeCreateAction.php @@ -0,0 +1,109 @@ +parentName = (string) $name; + } + + /** + * @return bool + */ + public function hasParent() + { + return $this->parentName !== null; + } + + /** + * @return string + */ + public function getParentName() + { + return $this->parentName; + } + + /** + * @param string $name + */ + public function setParentName($name) + { + $this->parentName = $name; + } + + /** + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * @param array $properties + * @return $this + */ + public function setProperties($properties) + { + $this->properties = (array) $properties; + return $this; + } + + /** + * @inheritdoc + */ + public function appliesTo(BpConfig $config) + { + return ! $config->hasNode($this->getNodeName()); + } + + /** + * @inheritdoc + */ + public function applyTo(BpConfig $config) + { + $name = $this->getNodeName(); + + $properties = array( + 'name' => $name, + 'operator' => $this->properties['operator'], + ); + if (array_key_exists('childNames', $this->properties)) { + $properties['child_names'] = $this->properties['childNames']; + } else { + $properties['child_names'] = array(); + } + $node = new BpNode($config, (object) $properties); + + foreach ($this->getProperties() as $key => $val) { + if ($key === 'parentName') { + $config->getBpNode($val)->addChild($node); + continue; + } + $func = 'set' . ucfirst($key); + $node->$func($val); + } + + $config->addNode($name, $node); + + return $node; + } +} diff --git a/library/Businessprocess/NodeModifyAction.php b/library/Businessprocess/Modification/NodeModifyAction.php similarity index 62% rename from library/Businessprocess/NodeModifyAction.php rename to library/Businessprocess/Modification/NodeModifyAction.php index da2e7fa..627f84b 100644 --- a/library/Businessprocess/NodeModifyAction.php +++ b/library/Businessprocess/Modification/NodeModifyAction.php @@ -1,6 +1,9 @@ properties[$key] = $properties[$key]; @@ -30,15 +40,18 @@ class NodeModifyAction extends NodeAction return $this; } - public function appliesTo(BusinessProcess $bp) + /** + * @inheritdoc + */ + public function appliesTo(BpConfig $config) { $name = $this->getNodeName(); - if (! $bp->hasNode($name)) { + if (! $config->hasNode($name)) { return false; } - $node = $bp->getNode($name); + $node = $config->getNode($name); foreach ($this->properties as $key => $val) { $func = 'get' . ucfirst($key); @@ -50,9 +63,12 @@ class NodeModifyAction extends NodeAction return true; } - public function applyTo(BusinessProcess $bp) + /** + * @inheritdoc + */ + public function applyTo(BpConfig $config) { - $node = $bp->getNode($this->getNodeName()); + $node = $config->getNode($this->getNodeName()); foreach ($this->properties as $key => $val) { $func = 'set' . ucfirst($key); @@ -62,23 +78,37 @@ class NodeModifyAction extends NodeAction return $this; } + /** + * @param $properties + * @return $this + */ public function setProperties($properties) { $this->properties = $properties; return $this; } + /** + * @param $properties + * @return $this + */ public function setFormerProperties($properties) { $this->formerProperties = $properties; return $this; } + /** + * @return array + */ public function getProperties() { return $this->properties; } + /** + * @return array + */ public function getFormerProperties() { return $this->formerProperties; diff --git a/library/Businessprocess/Modification/NodeRemoveAction.php b/library/Businessprocess/Modification/NodeRemoveAction.php new file mode 100644 index 0000000..29e17a9 --- /dev/null +++ b/library/Businessprocess/Modification/NodeRemoveAction.php @@ -0,0 +1,67 @@ +parentName = $parentName; + return $this; + } + + /** + * @return mixed + */ + public function getParentName() + { + return $this->parentName; + } + + /** + * @inheritdoc + */ + public function appliesTo(BpConfig $config) + { + $parent = $this->getParentName(); + if ($parent === null) { + return $config->hasNode($this->getNodeName()); + } else { + return $config->hasNode($this->getNodeName()) && $config->hasNode($this->getParentName()) ; + } + } + + /** + * @inheritdoc + */ + public function applyTo(BpConfig $config) + { + $parent = $this->getParentName(); + if ($parent === null) { + $config->removeNode($this->getNodeName()); + } else { + $node = $config->getNode($this->getNodeName()); + $node->removeParent($parent); + if (! $node->hasParents()) { + $config->removeNode($this->getNodeName()); + } + } + } +} diff --git a/library/Businessprocess/ProcessChanges.php b/library/Businessprocess/Modification/ProcessChanges.php similarity index 54% rename from library/Businessprocess/ProcessChanges.php rename to library/Businessprocess/Modification/ProcessChanges.php index 1fcf17c..b4ffbde 100644 --- a/library/Businessprocess/ProcessChanges.php +++ b/library/Businessprocess/Modification/ProcessChanges.php @@ -1,25 +1,41 @@ getName(); $changes = new ProcessChanges(); @@ -27,7 +43,7 @@ class ProcessChanges if ($actions = $session->get($key)) { foreach ($actions as $string) { - $changes->push(NodeAction::unserialize($string)); + $changes->push(NodeAction::unSerialize($string)); } } $changes->session = $session; @@ -35,6 +51,12 @@ class ProcessChanges return $changes; } + /** + * @param Node $node + * @param $properties + * + * @return $this + */ public function modifyNode(Node $node, $properties) { $action = new NodeModifyAction($node); @@ -42,6 +64,26 @@ class ProcessChanges return $this->push($action); } + /** + * @param Node $node + * @param $properties + * + * @return $this + */ + public function addChildrenToNode($children, Node $node = null) + { + $action = new NodeAddChildrenAction($node); + $action->setChildren($children); + return $this->push($action); + } + + /** + * @param Node|string $nodeName + * @param array $properties + * @param Node $parent + * + * @return $this + */ public function createNode($nodeName, $properties, Node $parent = null) { $action = new NodeCreateAction($nodeName); @@ -52,11 +94,29 @@ class ProcessChanges return $this->push($action); } - public function deleteNode(Node $node) + /** + * @param Node $node + * @param array $path + * + * @return $this + */ + public function deleteNode(Node $node, array $path = null) { - return $this->push(new NodeDeleteAction($node)); + $action = new NodeRemoveAction($node); + if ($path !== null) { + $action->setPath($path); + } + + return $this->push($action); } + /** + * Add a new action to the stack + * + * @param NodeAction $change + * + * @return $this + */ public function push(NodeAction $change) { $this->changes[] = $change; @@ -64,11 +124,21 @@ class ProcessChanges return $this; } + /** + * Get all stacked actions + * + * @return NodeAction[] + */ public function getChanges() { return $this->changes; } + /** + * Forget all changes and remove them from the Session + * + * @return $this + */ public function clear() { $this->hasBeenModified = true; @@ -77,16 +147,31 @@ class ProcessChanges return $this; } + /** + * Whether there are no stacked changes + * + * @return bool + */ public function isEmpty() { return $this->count() === 0; } + /** + * Number of stacked changes + * + * @return bool + */ public function count() { return count($this->changes); } + /** + * Get the first change on the stack, false if empty + * + * @return NodeAction|boolean + */ public function shift() { if ($this->isEmpty()) { @@ -97,6 +182,11 @@ class ProcessChanges return array_shift($this->changes); } + /** + * Get the last change on the stack, false if empty + * + * @return NodeAction|boolean + */ public function pop() { if ($this->isEmpty()) { @@ -107,6 +197,11 @@ class ProcessChanges return array_pop($this->changes); } + /** + * The identifier used for this processes changes in our Session storage + * + * @return string + */ protected function getSessionKey() { return $this->sessionKey; @@ -117,6 +212,9 @@ class ProcessChanges return $this->hasBeenModified; } + /** + * @return array + */ public function serialize() { $serialized = array(); @@ -127,6 +225,9 @@ class ProcessChanges return $serialized; } + /** + * Persist to session on destruction + */ public function __destruct() { if (! $this->hasBeenModified()) { diff --git a/library/Businessprocess/MonitoredNode.php b/library/Businessprocess/MonitoredNode.php new file mode 100644 index 0000000..68e4866 --- /dev/null +++ b/library/Businessprocess/MonitoredNode.php @@ -0,0 +1,19 @@ +isMissing()) { + return Link::create($this->getAlias(), '#'); + } else { + return Link::create($this->getAlias(), $this->getUrl()); + } + } +} diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php index 4133b1c..f8a41f6 100644 --- a/library/Businessprocess/Node.php +++ b/library/Businessprocess/Node.php @@ -2,10 +2,8 @@ namespace Icinga\Module\Businessprocess; -use Icinga\Web\Url; use Icinga\Exception\ProgrammingError; -use Icinga\Data\Filter\Filter; -use Exception; +use Icinga\Module\Businessprocess\Html\Link; abstract class Node { @@ -24,7 +22,7 @@ abstract class Node const ICINGA_UNREACHABLE = 2; const ICINGA_PENDING = 99; - protected static $sortStateToStateMap = array( + protected $sortStateToStateMap = array( 4 => self::ICINGA_CRITICAL, 3 => self::ICINGA_UNKNOWN, 2 => self::ICINGA_WARNING, @@ -32,7 +30,7 @@ abstract class Node 0 => self::ICINGA_OK ); - protected static $stateToSortStateMap = array( + protected $stateToSortStateMap = array( self::ICINGA_PENDING => 1, self::ICINGA_UNKNOWN => 3, self::ICINGA_CRITICAL => 4, @@ -43,7 +41,7 @@ abstract class Node /** * Main business process object * - * @var BusinessProcess + * @var BpConfig */ protected $bp; @@ -96,7 +94,7 @@ abstract class Node protected $className = 'unknown'; - protected static $state_names = array( + protected $stateNames = array( 'OK', 'WARNING', 'CRITICAL', @@ -104,7 +102,7 @@ abstract class Node 99 => 'PENDING' ); - abstract public function __construct(BusinessProcess $bp, $object); + abstract public function __construct(BpConfig $bp, $object); public function setMissing($missing = true) { @@ -127,32 +125,38 @@ abstract class Node return $this->missing; } + public function hasMissingChildren() + { + return count($this->getMissingChildren()) > 0; + } + + public function getMissingChildren() + { + return array(); + } + public function hasInfoUrl() { return false; } - public function addChild(Node $node) - { - if (array_key_exists((string) $node, $this->children)) { - throw new Exception( - sprintf( - 'Node "%s" has been defined more than once', - $node - ) - ); - } - $this->childs[(string) $node] = $node; - $node->addParent($this); - return $this; - } - public function setState($state) { $this->state = (int) $state; return $this; } + /** + * Forget my state + * + * @return $this + */ + public function clearState() + { + $this->state = null; + return $this; + } + public function setAck($ack = true) { $this->ack = $ack; @@ -165,9 +169,19 @@ abstract class Node return $this; } - public function getStateName() + public function getStateName($state = null) { - return static::$state_names[ $this->getState() ]; + $states = $this->enumStateNames(); + if ($state === null) { + return $states[ $this->getState() ]; + } else { + return $states[ $state ]; + } + } + + public function enumStateNames() + { + return $this->stateNames; } public function getState() @@ -180,6 +194,7 @@ abstract class Node ) ); } + return $this->state; } @@ -238,16 +253,6 @@ abstract class Node return $this->ack; } - public function isSimulationMode() - { - return $this->bp->isSimulationMode(); - } - - public function isEditMode() - { - return $this->bp->isEditMode(); - } - public function getChildren($filter = null) { return array(); @@ -283,158 +288,90 @@ abstract class Node return count($this->parents) > 0; } - protected function stateToSortState($state) + public function hasParentName($name) { - if (array_key_exists($state, static::$stateToSortStateMap)) { - return static::$stateToSortStateMap[$state]; + foreach ($this->getParents() as $parent) { + if ($parent->getName() === $name) { + return true; + } } - throw new ProgrammingError('Got invalid state %s', $sort_state); + return false; + } + + public function removeParent($name) + { + $this->parents = array_filter( + $this->parents, + function (BpNode $parent) use ($name) { + return $parent->getName() !== $name; + } + ); + } + + /** + * @return BpNode[] + */ + public function getParents() + { + return $this->parents; + } + + /** + * @return array + */ + public function getPaths() + { + if ($this->bp->hasRootNode($this->getName())) { + return array(array($this->getName())); + } + + $paths = array(); + foreach ($this->parents as $parent) { + foreach ($parent->getPaths() as $path) { + // $path[] = $this->getName(); + $paths[] = $path; + } + + } + // TODO! -> for delete etc + return $paths; + } + + protected function stateToSortState($state) + { + if (array_key_exists($state, $this->stateToSortStateMap)) { + return $this->stateToSortStateMap[$state]; + } + + throw new ProgrammingError( + 'Got invalid state for node %s: %s', + $this->getName(), + var_export($state, 1) . var_export($this->stateToSortStateMap, 1) + ); } protected function sortStateTostate($sortState) { $sortState = $sortState >> self::SHIFT_FLAGS; - - if (array_key_exists($sortState, static::$sortStateToStateMap)) { - return static::$sortStateToStateMap[$sortState]; + if (array_key_exists($sortState, $this->sortStateToStateMap)) { + return $this->sortStateToStateMap[$sortState]; } - throw new ProgrammingError('Got invalid sorting state %s', $sort_state); + throw new ProgrammingError('Got invalid sorting state %s', $sortState); } - protected function renderHtmlForChildren($view) - { - $html = ''; - if ($this->hasChildren()) { - foreach ($this->getChildren() as $name => $child) { - $html .= ''; - } - } - - return $html; - } - - protected function getId($prefix = '') - { - return md5($prefix . (string) $this); - } - - protected function getObjectClassName() + public function getObjectClassName() { return $this->className; } - protected function getStateClassNames() + /** + * @return Link + */ + public function getLink() { - $state = strtolower($this->getStateName()); - - if ($this->isMissing()) { - return array('missing'); - } elseif ($state === 'ok') { - return array('ok'); - } else { - return array('problem', $state); - } - } - - public function renderHtml($view, $prefix = '') - { - $id = $this->getId($prefix); - $handled = $this->isAcknowledged() || $this->isInDowntime(); - - $html = sprintf( - '
' - . $child->renderHtml($view) - . '
', - implode(' ', $this->getStateClassNames()), - $handled ? ' handled' : '', - ($this->hasChildren() ? ' operator ' : ' node '), - $this->getObjectClassName(), - $id - ); - - if ($this->hasChildren()) { - $html .= sprintf( - '%s', - sprintf(' rowspan="%d"', $this->countChildren() + 1), - $this->operatorHtml() - ); - } - - - $title = preg_replace( - '~()~', - implode('', $this->getIcons($view)) . '$1', - $this->renderLink($view) - ); - - $title = preg_replace('##', ' ' . $view->timeSince($this->getLastStateChange()) . '', $title); - $icons = array(); - - foreach ($this->getActionIcons($view) as $icon) { - $icons[] = $icon; - } - - if ($this->hasInfoUrl()) { - $url = $this->getInfoUrl(); - $icons[] = $this->actionIcon( - $view, - 'help', - $url, - sprintf('%s: %s', mt('businessprocess', 'More information'), $url) - ); - } - $title = implode("\n", $icons) . $title; - - $html .= sprintf( - '', - $title - ); - foreach ($this->getChildren() as $name => $child) { - $html .= ''; - } - $html .= "
%s
' . $child->renderHtml($view, $id . '-') . '
\n"; - return $html; - } - - protected function getActionIcons($view) - { - return array(); - } - - protected function actionIcon($view, $icon, $url, $title) - { - if ($url instanceof Url || ! preg_match('~^https?://~', $url)) { - $target = ''; - } else { - $target = ' target="_blank"'; - } - - return sprintf( - ' %s', - $url, - $target, - $view->escape($title), - $view->icon($icon) - ); - } - - public function renderLink($view) - { - return '' . ($this->hasAlias() ? $this->getAlias() : $this->name) . ''; - } - - public function getIcons($view) - { - $icons = array(); - if ($this->isInDowntime()) { - $icons[] = $view->icon('moon'); - } - if ($this->isAcknowledged()) { - $icons[] = $view->icon('ok'); - } - return $icons; + return Link::create($this->getAlias(), '#'); } public function operatorHtml() @@ -442,16 +379,18 @@ abstract class Node return ' '; } - public function toLegacyConfigString(& $rendered = array()) { return '';} - //abstract public function toLegacyConfigString(); - - public function __toString() + public function getName() { return $this->name; } + public function __toString() + { + return $this->getName(); + } + public function __destruct() { - $this->parents = array(); + unset($this->parents); } } diff --git a/library/Businessprocess/NodeAction.php b/library/Businessprocess/NodeAction.php deleted file mode 100644 index 88e0e34..0000000 --- a/library/Businessprocess/NodeAction.php +++ /dev/null @@ -1,93 +0,0 @@ -nodeName = (string) $node; - } - - abstract public function applyTo(BusinessProcess $bp); - - public function getNodeName() - { - return $this->nodeName; - } - - public function is($actionName) - { - return $this->getActionName() === $actionName; - } - - public static function create($actionName, $nodeName) - { - $classname = __NAMESPACE__ . '\\Node' . ucfirst($actionName) . 'Action'; - $object = new $classname($nodeName); - return $object; - } - - public function serialize() - { - $object = (object) array( - 'actionName' => $this->getActionName(), - 'nodeName' => $this->getNodeName(), - 'properties' => array() - ); - - foreach ($this->preserveProperties as $key) { - $func = 'get' . ucfirst($key); - $object->properties[$key] = $this->$func(); - } - - return json_encode($object); - } - - public static function unserialize($string) - { - $object = json_decode($string); - $action = self::create($object->actionName, $object->nodeName); - - foreach ($object->properties as $key => $val) { - $func = 'set' . ucfirst($key); - $action->$func($val); - } - - return $action; - } - - public function getActionName() - { - if ($this->actionName === null) { - if (! preg_match('/\\\Node(\w+)Action$/', get_class($this), $m)) { - throw new ProgrammingError( - '"%s" is not a NodeAction class', - get_class($this) - ); - } - $this->actionName = lcfirst($m[1]); - } - - return $this->actionName; - } - -} diff --git a/library/Businessprocess/NodeCreateAction.php b/library/Businessprocess/NodeCreateAction.php deleted file mode 100644 index c6e5a58..0000000 --- a/library/Businessprocess/NodeCreateAction.php +++ /dev/null @@ -1,69 +0,0 @@ -parentName = $name; - } - - public function hasParent() - { - return $this->parentName !== null; - } - - public function getParentName() - { - return $this->parentName; - } - - public function setParentName($name) - { - $this->parentName = $name; - } - - public function getProperties() - { - return $this->properties; - } - - public function setProperties($properties) - { - $this->properties = $properties; - return $this; - } - - public function appliesTo(BusinessProcess $bp) - { - return ! $bp->hasNode($this->getNodeName()); - } - - public function applyTo(BusinessProcess $bp) - { - $node = new BpNode($bp, (object) array( - 'name' => $this->getNodeName(), - 'operator' => $this->properties->operator, - 'child_names' => $this->properties->childNames - )); - - foreach ($this->properties as $key => $val) { - $func = 'set' . ucfirst($key); - $node->$func($val); - } - - $bp->addNode($this->getNodeName(), $node); - if ($this->hasParent()) { - $node->addParent($bp->getNode($this->getParentName())); - } - - return $node; - } -} diff --git a/library/Businessprocess/NodeRemoveAction.php b/library/Businessprocess/NodeRemoveAction.php deleted file mode 100644 index 916265f..0000000 --- a/library/Businessprocess/NodeRemoveAction.php +++ /dev/null @@ -1,16 +0,0 @@ -hasNode($this->getNodeName()); - } - - public function applyTo(BusinessProcess $bp) - { - $bp->removeNode($this->getNodeName()); - } -} diff --git a/library/Businessprocess/ProvidedHook/Monitoring/HostActions.php b/library/Businessprocess/ProvidedHook/Monitoring/HostActions.php new file mode 100644 index 0000000..57ce8f5 --- /dev/null +++ b/library/Businessprocess/ProvidedHook/Monitoring/HostActions.php @@ -0,0 +1,18 @@ + 'businessprocess/node/impact?name=' + . rawurlencode($host->getName() . ';Hoststatus') + ); + } +} diff --git a/library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php b/library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php new file mode 100644 index 0000000..69a93ae --- /dev/null +++ b/library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php @@ -0,0 +1,25 @@ + sprintf( + 'businessprocess/node/impact?name=%s', + rawurlencode( + sprintf('%s;%s', $service->getHost()->getName(), $service->getName()) + ) + ) + ); + } +} diff --git a/library/Businessprocess/Renderer/Breadcrumb.php b/library/Businessprocess/Renderer/Breadcrumb.php new file mode 100644 index 0000000..a68c442 --- /dev/null +++ b/library/Businessprocess/Renderer/Breadcrumb.php @@ -0,0 +1,66 @@ + 'breadcrumb', + 'data-base-target' => '_main' + ); + + /** + * @param Renderer $renderer + * @return static + */ + public static function create(Renderer $renderer) + { + $bp = $renderer->getBusinessProcess(); + $breadcrumb = new static; + $bpUrl = $renderer->getBaseUrl(); + if ($bpUrl->getParam('action') === 'delete') { + $bpUrl->remove('action'); + } + $breadcrumb->add(Element::create('li')->add( + Link::create($bp->getTitle(), $bpUrl) + )); + $path = $renderer->getCurrentPath(); + + $parts = array(); + while ($node = array_pop($path)) { + array_unshift( + $parts, + static::renderNode($bp->getNode($node), $path, $renderer) + ); + } + $breadcrumb->addContent($parts); + + return $breadcrumb; + } + + /** + * @param BpNode $node + * @param array $path + * @param Renderer $renderer + * + * @return NodeTile + */ + protected static function renderNode(BpNode $node, $path, Renderer $renderer) + { + // TODO: something more generic than NodeTile? + $renderer = clone($renderer); + $renderer->lock()->setIsBreadcrumb(); + $p = new NodeTile($renderer, (string) $node, $node, $path); + $p->attributes()->add('class', $renderer->getNodeClasses($node)); + $p->setTag('li'); + return $p; + } +} diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php new file mode 100644 index 0000000..c6efd46 --- /dev/null +++ b/library/Businessprocess/Renderer/Renderer.php @@ -0,0 +1,328 @@ +config = $config; + $this->parent = $parent; + } + + /** + * @return BpConfig + */ + public function getBusinessProcess() + { + return $this->config; + } + + /** + * Whether this will render all root nodes + * + * @return bool + */ + public function wantsRootNodes() + { + return $this->parent === null; + } + + /** + * Whether this will only render parts of given config + * + * @return bool + */ + public function rendersSubNode() + { + return $this->parent !== null; + } + + /** + * @return BpNode + */ + public function getParentNode() + { + return $this->parent; + } + + /** + * @return BpNode[] + */ + public function getParentNodes() + { + if ($this->wantsRootNodes()) { + return array(); + } + + return $this->parent->getParents(); + } + + /** + * @return BpNode[] + */ + public function getChildNodes() + { + if ($this->wantsRootNodes()) { + return $this->config->getRootNodes(); + } else { + return $this->parent->getChildren(); + } + } + + /** + * @return int + */ + public function countChildNodes() + { + if ($this->wantsRootNodes()) { + return $this->config->countChildren(); + } else { + return $this->parent->countChildren(); + } + } + + /** + * @param $summary + * @return Container + */ + public function renderStateBadges($summary) + { + $container = Container::create( + array('class' => 'badges') + )/* ->renderIfEmpty(false) */; + + foreach ($summary as $state => $cnt) { + if ($cnt === 0 + || $state === 'OK' + || $state === 'UP' + ) { + continue; + } + + $container->addContent( + Element::create( + 'span', + array( + 'class' => array( + 'badge', + 'badge-' . strtolower($state) + ), + // TODO: We should translate this in this module + 'title' => mt('monitoring', $state) + ) + )->setContent($cnt) + ); + } + + return $container; + } + + public function getNodeClasses(Node $node) + { + $classes = array( + strtolower($node->getStateName()) + ); + + if ($node->isHandled()) { + $classes[] = 'handled'; + } + + if ($node instanceof BpNode) { + $classes[] = 'process-node'; + } else { + $classes[] = 'monitored-node'; + } + + return $classes; + } + + public function setPath(array $path) + { + $this->path = $path; + return $this; + } + + /** + * @return string|null + */ + public function getPath() + { + return $this->path; + } + + public function getCurrentPath() + { + $path = $this->getPath(); + if ($this->rendersSubNode()) { + $path[] = (string) $this->parent; + } + return $path; + } + + /** + * @param Url $url + * @return $this + */ + public function setUrl(Url $url) + { + $this->url = $url->without(array( + 'deletenode', + 'deleteparent', + 'editnode', + 'simulationnode' + )); + $this->setBaseUrl($url); + return $this; + } + + /** + * @param Url $url + * @return $this + */ + public function setBaseUrl(Url $url) + { + $this->baseUrl = $url->without(array('node', 'path')); + return $this; + } + + public function getUrl() + { + return $this->url; + } + + /** + * @return Url + * @throws ProgrammingError + */ + public function getBaseUrl() + { + if ($this->baseUrl === null) { + throw new ProgrammingError('Renderer has no baseUrl'); + } + + return clone($this->baseUrl); + } + + /** + * @return bool + */ + public function isLocked() + { + return $this->locked; + } + + /** + * @return $this + */ + public function lock() + { + $this->locked = true; + return $this; + } + + /** + * @return $this + */ + public function unlock() + { + $this->locked = false; + return $this; + } + + /** + * TODO: Get rid of this + * + * @return $this + */ + public function setIsBreadcrumb() + { + $this->isBreadcrumb = true; + return $this; + } + + public function isBreadcrumb() + { + return $this->isBreadcrumb; + } + + public function timeSince($time, $timeOnly = false) + { + if (! $time) { + return HtmlString::create(''); + } + + return Element::create( + 'span', + array( + 'class' => array('relative-time', 'time-since'), + 'title' => DateFormatter::formatDateTime($time), + ) + )->setContent(DateFormatter::timeSince($time, $timeOnly)); + } + + protected function createUnboundParent(BpConfig $bp) + { + $unbound = $bp->getUnboundNodes(); + + $parent = new BpNode($bp, (object) array( + 'name' => '__unbound__', + 'operator' => '|', + 'child_names' => array_keys($unbound) + )); + $parent->getState(); + $parent->setMissing() + ->setDowntime(false) + ->setAck(false) + ->setAlias('Unbound nodes'); + + return $parent; + } + + /** + * Just to be on the safe side + */ + public function __destruct() + { + unset($this->parent); + unset($this->config); + } +} diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php new file mode 100644 index 0000000..9d6218a --- /dev/null +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -0,0 +1,113 @@ +config; + $nodesDiv = Container::create( + array( + 'class' => array( + 'tiles', + $this->howMany() + ), + 'data-base-target' => '_self', + ) + ); + + $nodes = $this->getChildNodes(); + + if (! $this->isLocked() && count($nodes) > 8) { + $this->add($this->addNewNode()); + } + + $path = $this->getCurrentPath(); + foreach ($nodes as $name => $node) { + $this->add(new NodeTile($this, $name, $node, $path)); + } + + if ($this->wantsRootNodes()) { + $unbound = $this->createUnboundParent($bp); + if ($unbound->hasChildren()) { + $name = $unbound->getAlias(); + $this->add(new NodeTile($this, $name, $unbound)); + } + } + + if (! $this->isLocked()) { + $this->add($this->addNewNode()); + } + + $nodesDiv->addContent($this->getContent()); + $this->setContent($nodesDiv); + + return parent::render(); + } + + /** + * A CSS class giving a rough indication of how many nodes we have + * + * This is used to show larger tiles when there are few and smaller + * ones if there are many. + * + * @return string + */ + protected function howMany() + { + $count = $this->countChildNodes(); + $howMany = 'normal'; + + if ($count <= 6) { + $howMany = 'few'; + } elseif ($count > 12) { + $howMany = 'many'; + } + + return $howMany; + } + + protected function addNewNode() + { + $div = Container::create( + array('class' => 'addnew') + ); + + $actions = Container::create( + array( + 'class' => 'actions', + 'data-base-target' => '_self' + ) + ); + + $link = Link::create( + $this->translate('Add'), + $this->getUrl()->with('action', 'add'), + null, + array( + 'title' => $this->translate('Add a new business process node') + ) + ); + $actions->add( + Link::create( + Icon::create('plus'), + $this->getUrl()->with('action', 'add'), + null, + array( + 'title' => $this->translate('Add a new business process node') + ) + ) + ); + + return $div->add($actions)->add($link); + } +} diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php new file mode 100644 index 0000000..95bc651 --- /dev/null +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -0,0 +1,279 @@ +renderer = $renderer; + $this->name = $name; + $this->node = $node; + $this->path = $path; + } + + protected function actions() + { + if ($this->actions === null) { + $this->addActions(); + } + return $this->actions; + } + + protected function addActions() + { + $this->actions = Container::create( + array( + 'class' => 'actions', + 'data-base-target' => '_self' + ) + ); + + return $this->add($this->actions); + } + + public function render() + { + $renderer = $this->renderer; + $node = $this->node; + + $attributes = $this->attributes(); + $attributes->add('class', $renderer->getNodeClasses($node)); + $attributes->add('id', 'bp-' . (string) $node); + + $this->addActions(); + + $link = $this->getMainNodeLink(); + $this->add($link); + + if ($node instanceof BpNode) { + if ($renderer->isBreadcrumb()) { + $link->addContent($renderer->renderStateBadges($node->getStateSummary())); + } else { + $this->addContent($renderer->renderStateBadges($node->getStateSummary())); + } + } + + if (! $renderer->isBreadcrumb()) { + $this->addDetailsActions(); + } + + if (! $renderer->isLocked()) { + $this->addActionLinks(); + } + + return parent::render(); + } + + protected function getMainNodeUrl(Node $node) + { + if ($node instanceof BpNode) { + return $this->makeBpUrl($node); + } else { + /** @var MonitoredNode $node */ + return $node->getUrl(); + } + } + + protected function buildBaseNodeUrl(Node $node) + { + $path = $this->path; + $name = $this->name; // TODO: ?? + $renderer = $this->renderer; + + $bp = $renderer->getBusinessProcess(); + $params = array( + 'config' => $node instanceof ImportedNode ? + $node->getConfigName() : + $bp->getName() + ); + + if ($name !== null) { + $params['node'] = $name; + } + + $url = $renderer->getBaseUrl(); + $p = $url->getParams(); + $p->mergeValues($params); + if (! empty($path)) { + $p->addValues('path', $path); + } + + return $url; + } + + protected function makeBpUrl(BpNode $node) + { + return $this->buildBaseNodeUrl($node); + } + + protected function makeMonitoredNodeUrl(MonitoredNode $node) + { + $path = $this->path; + $name = $this->name; // TODO: ?? + $renderer = $this->renderer; + + $bp = $renderer->getBusinessProcess(); + $params = array( + 'config' => $bp->getName() + ); + + if ($name !== null) { + $params['node'] = $node->getName(); + } + + $url = $renderer->getBaseUrl(); + $p = $url->getParams(); + $p->mergeValues($params); + if (! empty($path)) { + $p->addValues('path', $path); + } + + return $url; + } + + /** + * @return Link + */ + protected function getMainNodeLink() + { + $node = $this->node; + $url = $this->getMainNodeUrl($node); + if ($node instanceof ServiceNode) { + $link = Link::create( + $node->getAlias(), + $url, + null, + array('data-base-target' => '_next') + ); + } elseif ($node instanceof HostNode) { + $link = Link::create( + $node->getHostname(), + $url, + null, + array('data-base-target' => '_next') + ); + } else { + $link = Link::create($node->getAlias(), $url); + } + + return $link; + } + + protected function addDetailsActions() + { + $node = $this->node; + $url = $this->getMainNodeUrl($node); + + if ($node instanceof BpNode) { + $this->actions()->add(Link::create( + Icon::create('dashboard'), + $url->with('mode', 'tile'), + null, + array( + 'title' => $this->translate('Show tiles for this subtree'), + 'data-base-target' => '_next' + ) + ))->add(Link::create( + Icon::create('sitemap'), + $url->with('mode', 'tree'), + null, + array( + 'title' => $this->translate('Show this subtree as a tree'), + 'data-base-target' => '_next' + ) + )); + } else { + // $url = $this->makeMonitoredNodeUrl($node); + if ($node instanceof ServiceNode) { + $this->actions()->add(Link::create( + Icon::create('service'), + $node->getUrl(), + null, + array('data-base-target' => '_next') + )); + + } elseif ($node instanceof HostNode) { + $this->actions()->add(Link::create( + Icon::create('host'), + $node->getUrl(), + null, + array('data-base-target' => '_next') + )); + } + } + + } + + protected function addActionLinks() + { + $node = $this->node; + $renderer = $this->renderer; + + if ($node instanceof BpNode) { + $this->actions()->add(Link::create( + Icon::create('edit'), + $renderer->getUrl()->with('action', 'edit')->with('editnode', $node->getName()), + null, + array('title' => $this->translate('Modify this business process node')) + )); + } elseif ($node instanceof MonitoredNode) { + $this->actions()->add(Link::create( + Icon::create('magic'), + $renderer->getUrl()->with('action', 'simulation') + ->with('simulationnode', $this->name), + null, + array('title' => $this->translate( + 'Show the business impact of this node by simulating a specific state' + )) + )); + } + + $params = array( + 'action' => 'delete', + 'deletenode' => $node->getName(), + ); + + $this->actions()->add(Link::create( + Icon::create('cancel'), + $renderer->getUrl()->with($params), + null, + array('title' => $this->translate('Delete this node')) + )); + } +} diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php new file mode 100644 index 0000000..6069122 --- /dev/null +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -0,0 +1,250 @@ +config; + $this->add(Container::create( + array( + 'id' => $bp->getHtmlId(), + 'class' => 'bp' + ), + $this->renderBp($bp) + )); + + return parent::render(); + } + + /** + * @param BpConfig $bp + * @return string + */ + public function renderBp(BpConfig $bp) + { + $html = array(); + if ($this->wantsRootNodes()) { + $nodes = $bp->getChildren(); + } else { + $nodes = $this->parent->getChildren(); + } + + foreach ($nodes as $name => $node) { + $html[] = $this->renderNode($bp, $node); + } + + return $html; + } + + /** + * @param Node $node + * @param $path + * @return string + */ + protected function getId(Node $node, $path) + { + return md5(implode(';', $path) . (string) $node); + } + + protected function getStateClassNames(Node $node) + { + $state = strtolower($node->getStateName()); + + if ($node->isMissing()) { + return array('missing'); + } elseif ($state === 'ok') { + if ($node->hasMissingChildren()) { + return array('ok', 'missing-children'); + } else { + return array('ok'); + } + } else { + return array('problem', $state); + } + } + + /** + * @param Node $node + * @return Icon[] + */ + public function getNodeIcons(Node $node) + { + $icons = array(); + if ($node->isInDowntime()) { + $icons[] = Icon::create('moon'); + } + if ($node->isAcknowledged()) { + $icons[] = Icon::create('ok'); + } + return $icons; + } + + /** + * @param BpConfig $bp + * @param Node $node + * @param array $path + * + * @return string + */ + public function renderNode(BpConfig $bp, Node $node, $path = array()) + { + $table = Element::create( + 'table', + array( + 'id' => $this->getId($node, $path), + 'class' => array( + 'bp', + $node->getObjectClassName() + ) + ) + ); + $attributes = $table->attributes(); + $attributes->add('class', $this->getStateClassNames($node)); + if ($node->isHandled()) { + $attributes->add('class', 'handled'); + } + if ($node->hasChildren()) { + $attributes->add('class', 'operator'); + } else { + $attributes->add('class', 'node'); + } + + $tbody = $table->createElement('tbody'); + $tr = $tbody->createElement('tr'); + + if ($node->hasChildren()) { + $tr->createElement( + 'th', + array( + 'rowspan' => $node->countChildren() + 1 + ($this->isLocked() ? 0 : 1) + ) + )->createElement( + 'span', + array('class' => 'op') + )->setContent($node->operatorHtml()); + } + $td = $tr->createElement('td'); + + if ($node instanceof BpNode && $node->hasInfoUrl()) { + $td->add($this->createInfoAction($node)); + } + + if (! $this->isLocked()) { + $td->addContent($this->getActionIcons($bp, $node)); + } + + $link = $node->getLink(); + $link->attributes()->set('data-base-target', '_next'); + $link->addContent($this->getNodeIcons($node)); + + if ($node->hasChildren()) { + $link->addContent($this->renderStateBadges($node->getStateSummary())); + } + + if ($time = $node->getLastStateChange()) { + $since = $this->timeSince($time)->prependContent( + sprintf(' (%s ', $node->getStateName()) + )->addContent(')'); + $link->addContent($since); + } + + $td->addContent($link); + + foreach ($node->getChildren() as $name => $child) { + $tbody->createElement('tr')->createElement('td')->setContent( + $this->renderNode($bp, $child, $this->getCurrentPath()) + ); + } + + if (! $this->isLocked() && $node instanceof BpNode) { + $tbody->createElement('tr')->createElement('td')->setContent( + $this->renderAddNewNode($node) + ); + } + + return $table; + } + + protected function getActionIcons(BpConfig $bp, Node $node) + { + if ($node instanceof BpNode) { + return $this->createEditAction($bp, $node); + } else { + return $this->createSimulationAction($bp, $node); + } + } + + protected function createEditAction(BpConfig $bp, BpNode $node) + { + return $this->actionIcon( + 'wrench', + Url::fromPath('businessprocess/node/edit', array( + 'config' => $bp->getName(), + 'node' => $node->getName() + )), + $this->translate('Modify this node') + ); + } + + protected function createSimulationAction(BpConfig $bp, Node $node) + { + return $this->actionIcon( + 'magic', + $this->getUrl()->with(array( + //'config' => $bp->getName(), + 'action' => 'simulation', + 'simulationnode' => $node->getName() + )), + $this->translate('Simulate a specific state') + ); + } + + protected function createInfoAction(BpNode $node) + { + $url = $node->getInfoUrl(); + return $this->actionIcon( + 'help', + $url, + sprintf('%s: %s', $this->translate('More information'), $url) + ); + } + protected function actionIcon($icon, $url, $title) + { + return Link::create( + Icon::create($icon), + $url, + null, + array( + 'title' => $title, + 'style' => 'float: right', + ) + ); + } + + protected function renderAddNewNode($parent) + { + return Link::create( + $this->translate('Add'), + $this->getUrl()->with('action', 'add')->with('node', $parent->getName()), + null, + array( + 'class' => 'addnew icon-plus', + 'title' => $this->translate('Add a new business process node') + ) + ); + } +} diff --git a/library/Businessprocess/ServiceNode.php b/library/Businessprocess/ServiceNode.php index 8360ad6..b827835 100644 --- a/library/Businessprocess/ServiceNode.php +++ b/library/Businessprocess/ServiceNode.php @@ -2,9 +2,9 @@ namespace Icinga\Module\Businessprocess; -use Icinga\Web\Url; +use Icinga\Module\Businessprocess\Web\Url; -class ServiceNode extends Node +class ServiceNode extends MonitoredNode { protected $hostname; @@ -12,7 +12,7 @@ class ServiceNode extends Node protected $className = 'service'; - public function __construct(BusinessProcess $bp, $object) + public function __construct(BpConfig $bp, $object) { $this->name = $object->hostname . ';' . $object->service; $this->hostname = $object->hostname; @@ -25,46 +25,6 @@ class ServiceNode extends Node } } - public function renderLink($view) - { - if ($this->isMissing()) { - return '' . $view->escape($this->getAlias()) . ''; - } - - $params = array( - 'host' => $this->getHostname(), - 'service' => $this->getServiceDescription() - ); - if ($this->bp->hasBackendName()) { - $params['backend'] = $this->bp->getBackendName(); - } - $link = $view->qlink($this->getAlias(), 'monitoring/service/show', $params); - - return $link; - } - - protected function getActionIcons($view) - { - $icons = array(); - - if (! $this->bp->isLocked()) { - - $url = Url::fromPath( 'businessprocess/node/simulate', array( - 'config' => $this->bp->getName(), - 'node' => $this->name - )); - - $icons[] = $this->actionIcon( - $view, - 'magic', - $url, - 'Simulation' - ); - } - - return $icons; - } - public function getHostname() { return $this->hostname; @@ -79,4 +39,18 @@ class ServiceNode extends Node { return $this->hostname . ': ' . $this->service; } + + public function getUrl() + { + $params = array( + 'host' => $this->getHostname(), + 'service' => $this->getServiceDescription() + ); + + if ($this->bp->hasBackendName()) { + $params['backend'] = $this->bp->getBackendName(); + } + + return Url::fromPath('monitoring/service/show', $params); + } } diff --git a/library/Businessprocess/Simulation.php b/library/Businessprocess/Simulation.php index 4babc48..ede70d2 100644 --- a/library/Businessprocess/Simulation.php +++ b/library/Businessprocess/Simulation.php @@ -2,17 +2,18 @@ namespace Icinga\Module\Businessprocess; +use Icinga\Exception\ProgrammingError; use Icinga\Web\Session\SessionNamespace; class Simulation { /** - * @var Session + * @var SessionNamespace */ protected $session; /** - * @var BusinessProcess + * @var BpConfig */ protected $bp; @@ -21,9 +22,12 @@ class Simulation */ protected $key; + /** + * @var + */ protected $simulations; - public function __construct(BusinessProcess $bp, SessionNamespace $session) + public function __construct(BpConfig $bp, SessionNamespace $session) { $this->bp = $bp; $this->session = $session; diff --git a/library/Businessprocess/State/MonitoringState.php b/library/Businessprocess/State/MonitoringState.php new file mode 100644 index 0000000..68284d3 --- /dev/null +++ b/library/Businessprocess/State/MonitoringState.php @@ -0,0 +1,133 @@ +config = $config; + $this->backend = $config->getBackend(); + } + + public static function apply(BpConfig $config) + { + $self = new static($config); + $self->retrieveStatesFromBackend(); + return $config; + } + + public function retrieveStatesFromBackend() + { + $config = $this->config; + + try { + $this->reallyRetrieveStatesFromBackend(); + } catch (Exception $e) { + $config->addError( + $config->translate('Could not retrieve process state: %s'), + $e->getMessage() + ); + } + } + + public function reallyRetrieveStatesFromBackend() + { + $config = $this->config; + + Benchmark::measure('Retrieving states for business process ' . $config->getName()); + $backend = $this->backend; + $hostFilter = $config->listInvolvedHostNames(); + + if ($config->usesHardStates()) { + $hostStateColumn = 'host_hard_state'; + $hostStateChangeColumn = 'host_last_hard_state_change'; + $serviceStateColumn = 'service_hard_state'; + $serviceStateChangeColumn = 'service_last_hard_state_change'; + } else { + $hostStateColumn = 'host_state'; + $hostStateChangeColumn = 'host_last_state_change'; + $serviceStateColumn = 'service_state'; + $serviceStateChangeColumn = 'service_last_state_change'; + } + $filter = Filter::matchAny(); + foreach ($hostFilter as $host) { + $filter->addFilter(Filter::where('host_name', $host)); + } + + if ($filter->isEmpty()) { + return $this; + } + + $hostStatus = $backend->select()->from('hostStatus', array( + 'hostname' => 'host_name', + 'last_state_change' => $hostStateChangeColumn, + 'in_downtime' => 'host_in_downtime', + 'ack' => 'host_acknowledged', + 'state' => $hostStateColumn + ))->applyFilter($filter)->getQuery()->fetchAll(); + + $serviceStatus = $backend->select()->from('serviceStatus', array( + 'hostname' => 'host_name', + 'service' => 'service_description', + 'last_state_change' => $serviceStateChangeColumn, + 'in_downtime' => 'service_in_downtime', + 'ack' => 'service_acknowledged', + 'state' => $serviceStateColumn + ))->applyFilter($filter)->getQuery()->fetchAll(); + + foreach ($serviceStatus as $row) { + $this->handleDbRow($row, $config); + } + + foreach ($hostStatus as $row) { + $this->handleDbRow($row, $config); + } + // TODO: Union, single query? + Benchmark::measure('Got states for business process ' . $config->getName()); + + return $this; + } + + protected function handleDbRow($row, BpConfig $config) + { + $key = $row->hostname; + if (property_exists($row, 'service')) { + $key .= ';' . $row->service; + } else { + $key .= ';Hoststatus'; + } + + // We fetch more states than we need, so skip unknown ones + if (! $config->hasNode($key)) { + return; + } + + $node = $config->getNode($key); + + if ($row->state !== null) { + $node->setState($row->state)->setMissing(false); + } + if ($row->last_state_change !== null) { + $node->setLastStateChange($row->last_state_change); + } + if ((int) $row->in_downtime === 1) { + $node->setDowntime(true); + } + if ((int) $row->ack === 1) { + $node->setAck(true); + } + } +} diff --git a/library/Businessprocess/Storage/ConfigDiff.php b/library/Businessprocess/Storage/ConfigDiff.php new file mode 100644 index 0000000..fc3b1d9 --- /dev/null +++ b/library/Businessprocess/Storage/ConfigDiff.php @@ -0,0 +1,91 @@ +requireVendorLib('Diff.php'); + + if (empty($a)) { + $this->a = array(); + } else { + $this->a = explode("\n", (string) $a); + } + + if (empty($b)) { + $this->b = array(); + } else { + $this->b = explode("\n", (string) $b); + } + + $options = array( + 'context' => 5, + // 'ignoreWhitespace' => true, + // 'ignoreCase' => true, + ); + $this->diff = new Diff($this->a, $this->b, $options); + } + + /** + * @return string + */ + public function render() + { + return $this->renderHtmlSideBySide(); + } + + public function renderHtmlSideBySide() + { + $this->requireVendorLib('Diff/Renderer/Html/SideBySide.php'); + $renderer = new Diff_Renderer_Html_SideBySide; + return $this->diff->render($renderer); + } + + public function renderHtmlInline() + { + $this->requireVendorLib('Diff/Renderer/Html/Inline.php'); + $renderer = new Diff_Renderer_Html_Inline; + return $this->diff->render($renderer); + } + + public function renderTextContext() + { + $this->requireVendorLib('Diff/Renderer/Text/Context.php'); + $renderer = new Diff_Renderer_Text_Context; + return $this->diff->render($renderer); + } + + public function renderTextUnified() + { + $this->requireVendorLib('Diff/Renderer/Text/Unified.php'); + $renderer = new Diff_Renderer_Text_Unified; + return $this->diff->render($renderer); + } + + protected function requireVendorLib($file) + { + require_once dirname(dirname(__DIR__)) . '/vendor/php-diff/lib/' . $file; + } + + public static function create($a, $b) + { + $diff = new static($a, $b); + return $diff; + } +} diff --git a/library/Businessprocess/Storage/LegacyConfigParser.php b/library/Businessprocess/Storage/LegacyConfigParser.php new file mode 100644 index 0000000..a93ead2 --- /dev/null +++ b/library/Businessprocess/Storage/LegacyConfigParser.php @@ -0,0 +1,343 @@ +name = $name; + $this->config = new BpConfig(); + $this->config->setName($name); + } + + /** + * @return BpConfig + */ + public function getParsedConfig() + { + return $this->config; + } + + /** + * @param $name + * @param $filename + * + * @return BpConfig + */ + public static function parseFile($name, $filename) + { + Benchmark::measure('Loading business process ' . $name); + $parser = new static($name); + $parser->reallyParseFile($filename); + Benchmark::measure('Business process ' . $name . ' loaded'); + return $parser->getParsedConfig(); + } + + /** + * @param $name + * @param $string + * + * @return BpConfig + */ + public static function parseString($name, $string) + { + Benchmark::measure('Loading BP config from file: ' . $name); + $parser = new static($name); + foreach (preg_split('/\n/', $string) as $line) { + $parser->parseLine($line); + } + + Benchmark::measure('Business process ' . $name . ' loaded'); + return $parser->getParsedConfig(); + } + + protected function reallyParseFile($filename) + { + $file = $this->currentFilename = $filename; + $fh = @fopen($file, 'r'); + if (! $fh) { + throw new SystemPermissionException('Could not open "%s"', $filename); + } + + $config = $this->config; + $config->setMetadata( + $this::readMetadataFromFileHeader($config->getName(), $filename) + ); + + $this->currentLineNumber = 0; + while ($line = fgets($fh)) { + $this->parseLine($line); + } + + fclose($fh); + unset($this->currentLineNumber); + unset($this->currentFilename); + } + + public static function readMetadataFromFileHeader($name, $filename) + { + $metadata = new Metadata($name); + $fh = fopen($filename, 'r'); + $cnt = 0; + while ($cnt < 15 && false !== ($line = fgets($fh))) { + $cnt++; + static::parseHeaderLine($line, $metadata); + } + + fclose($fh); + return $metadata; + } + + protected function splitCommaSeparated($string) + { + return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY); + } + + protected function readHeaderString($string, Metadata $metadata) + { + foreach (preg_split('/\n/', $string) as $line) { + $this->parseHeaderLine($line, $metadata); + } + + return $metadata; + } + + /** + * @return array + */ + protected function emptyHeader() + { + return array( + 'Title' => null, + 'Description' => null, + 'Owner' => null, + 'AllowedUsers' => null, + 'AllowedGroups' => null, + 'AllowedRoles' => null, + 'Backend' => null, + 'Statetype' => 'soft', + 'SLAHosts' => null + ); + } + + /** + * @param $line + * @param Metadata $metadata + */ + protected static function parseHeaderLine($line, Metadata $metadata) + { + if (preg_match('/^\s*#\s+(.+?)\s*:\s*(.+)$/', $line, $m)) { + if ($metadata->hasKey($m[1])) { + $metadata->set($m[1], $m[2]); + } + } + } + + /** + * @param $line + * @param BpConfig $bp + */ + protected function parseDisplay(& $line, BpConfig $bp) + { + list($display, $name, $desc) = preg_split('~\s*;\s*~', substr($line, 8), 3); + $bp->getBpNode($name)->setAlias($desc)->setDisplay($display); + if ($display > 0) { + $bp->addRootNode($name); + } + } + + /** + * @param $line + * @param BpConfig $bp + */ + protected function parseExternalInfo(& $line, BpConfig $bp) + { + list($name, $script) = preg_split('~\s*;\s*~', substr($line, 14), 2); + $bp->getBpNode($name)->setInfoCommand($script); + } + + protected function parseExtraInfo(& $line, BpConfig $bp) + { + // TODO: Not yet + // list($name, $script) = preg_split('~\s*;\s*~', substr($line, 14), 2); + // $this->getNode($name)->setExtraInfo($script); + } + + protected function parseInfoUrl(& $line, BpConfig $bp) + { + list($name, $url) = preg_split('~\s*;\s*~', substr($line, 9), 2); + $bp->getBpNode($name)->setInfoUrl($url); + } + + protected function parseExtraLine(& $line, $typeLength, BpConfig $bp) + { + $type = substr($line, 0, $typeLength); + if (substr($type, 0, 7) === 'display') { + $this->parseDisplay($line, $bp); + return true; + } + + switch ($type) { + case 'external_info': + $this->parseExternalInfo($line, $bp); + break; + case 'extra_info': + $this->parseExtraInfo($line, $bp); + break; + case 'info_url': + $this->parseInfoUrl($line, $bp); + break; + default: + return false; + } + + return true; + } + + /** + * Parses a single line + * + * Adds eventual new knowledge to the given Business Process config + * + * @param $line + * + * @throws ConfigurationError + */ + protected function parseLine(& $line) + { + $bp = $this->config; + $line = trim($line); + + $this->currentLineNumber++; + + // Skip empty or comment-only lines + if (empty($line) || $line[0] === '#') { + return; + } + + // Semicolon found in the first 14 cols? Might be a line with extra information + $pos = strpos($line, ';'); + if ($pos !== false && $pos < 14) { + if ($this->parseExtraLine($line, $pos, $bp)) { + return; + } + } + + list($name, $value) = preg_split('~\s*=\s*~', $line, 2); + + if (strpos($name, ';') !== false) { + $this->parseError('No semicolon allowed in varname'); + } + + $op = '&'; + if (preg_match_all('~([\|\+&\!])~', $value, $m)) { + $op = implode('', $m[1]); + for ($i = 1; $i < strlen($op); $i++) { + if ($op[$i] !== $op[$i - 1]) { + $this->parseError('Mixing operators is not allowed'); + } + } + } + $op = $op[0]; + $op_name = $op; + + if ($op === '+') { + if (! preg_match('~^(\d+)(?::(\d+))?\s*of:\s*(.+?)$~', $value, $m)) { + $this->parseError('syntax: = of: + [+ ]*'); + } + $op_name = $m[1]; + // New feature: $minWarn = $m[2]; + $value = $m[3]; + } + $cmps = preg_split('~\s*\\' . $op . '\s*~', $value, -1, PREG_SPLIT_NO_EMPTY); + $childNames = array(); + + foreach ($cmps as $val) { + if (strpos($val, ';') !== false) { + if ($bp->hasNode($val)) { + $childNames[] = $val; + continue; + } + + list($host, $service) = preg_split('~;~', $val, 2); + if ($service === 'Hoststatus') { + $bp->createHost($host); + } else { + $bp->createService($host, $service); + } + } + if ($val[0] === '@') { + if (strpos($val, ':') === false) { + throw new ConfigurationError( + "I'm unable to import full external configs, a node needs to be provided for '%s'", + $val + ); + // TODO: this might work: + // $node = $bp->createImportedNode(substr($val, 1)); + } else { + list($config, $nodeName) = preg_split('~:\s*~', substr($val, 1), 2); + $node = $bp->createImportedNode($config, $nodeName); + } + $val = $node->getName(); + } + + $childNames[] = $val; + } + + $node = new BpNode($bp, (object) array( + 'name' => $name, + 'operator' => $op_name, + 'child_names' => $childNames + )); + + $bp->addNode($name, $node); + } + + /** + * @return string + */ + public function getFilename() + { + return $this->currentFilename ?: '[given string]'; + } + + /** + * @param $msg + * @throws ConfigurationError + */ + protected function parseError($msg) + { + throw new ConfigurationError( + sprintf( + 'Parse error on %s:%s: %s', + $this->getFilename(), + $this->currentLineNumber, + $msg + ) + ); + } +} diff --git a/library/Businessprocess/Storage/LegacyConfigRenderer.php b/library/Businessprocess/Storage/LegacyConfigRenderer.php new file mode 100644 index 0000000..6c3a7ba --- /dev/null +++ b/library/Businessprocess/Storage/LegacyConfigRenderer.php @@ -0,0 +1,229 @@ +config = $config; + } + + /** + * @return string + */ + public function render() + { + return $this->renderHeader() . $this->renderNodes(); + } + + /** + * @param BpConfig $config + * @return mixed + */ + public static function renderConfig(BpConfig $config) + { + $renderer = new static($config); + return $renderer->render(); + } + + /** + * @return string + */ + public function renderHeader() + { + $str = "### Business Process Config File ###\n#\n"; + + $meta = $this->config->getMetadata(); + foreach ($meta->getProperties() as $key => $value) { + if ($value === null) { + continue; + } + + $str .= sprintf("# %-15s : %s\n", $key, $value); + } + + $str .= "#\n###################################\n\n"; + + return $str; + } + + /** + * @return string + */ + public function renderNodes() + { + $this->renderedNodes = array(); + + $config = $this->config; + $str = ''; + + foreach ($config->getRootNodes() as $node) { + $str .= $this->requireRenderedBpNode($node); + } + + foreach ($config->getUnboundNodes() as $name => $node) { + $str .= $this->requireRenderedBpNode($node); + } + + return $str . "\n"; + } + + /** + * Rendered node definition, empty string if already rendered + * + * @param BpNode $node + * + * @return string + */ + protected function requireRenderedBpNode(BpNode $node) + { + $name = $node->getName(); + + if (array_key_exists($name, $this->renderedNodes)) { + + return ''; + } else { + $this->renderedNodes[$name] = true; + return $this->renderBpNode($node); + } + } + + /** + * @param BpNode $node + * @return string + */ + protected function renderBpNode(BpNode $node) + { + $name = $node->getName(); + // Doing this before rendering children allows us to store loops + $cfg = ''; + + foreach ($node->getChildBpNodes() as $name => $child) { + $cfg .= $this->requireRenderedBpNode($child) . "\n"; + } + + $cfg .= static::renderSingleBpNode($node); + + return $cfg; + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderEqualSign(BpNode $node) + { + $op = $node->getOperator(); + if (is_numeric($op)) { + return '= ' . $op . ' of:'; + } else { + return '='; + } + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderOperator(BpNode $node) + { + $op = $node->getOperator(); + if (is_numeric($op)) { + return '+'; + } else { + return $op; + } + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderSingleBpNode(BpNode $node) + { + return static::renderExpression($node) + . static::renderDisplay($node) + . static::renderInfoUrl($node); + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderExpression(BpNode $node) + { + return sprintf( + "%s %s %s\n", + $node->getName(), + static::renderEqualSign($node), + static::renderChildNames($node) + ); + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderChildNames(BpNode $node) + { + $op = static::renderOperator($node); + $children = $node->getChildNames(); + $str = implode(' ' . $op . ' ', $children); + + if ((count($children) < 2) && $op !== '&') { + + return $op . ' ' . $str; + } else { + + return $str; + } + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderDisplay(BpNode $node) + { + if ($node->hasAlias() || $node->getDisplay() > 0) { + $prio = $node->getDisplay(); + return sprintf( + "display %s;%s;%s\n", + $prio, + $node->getName(), + $node->getAlias() + ); + } else { + return ''; + } + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderInfoUrl(BpNode $node) + { + if ($node->hasInfoUrl()) { + return sprintf( + "info_url;%s;%s\n", + $node->getName(), + $node->getInfoUrl() + ); + } else { + return ''; + } + } +} diff --git a/library/Businessprocess/Storage/LegacyStorage.php b/library/Businessprocess/Storage/LegacyStorage.php index 0198e40..8cbe89b 100644 --- a/library/Businessprocess/Storage/LegacyStorage.php +++ b/library/Businessprocess/Storage/LegacyStorage.php @@ -2,109 +2,153 @@ namespace Icinga\Module\Businessprocess\Storage; -use Icinga\Application\Icinga; -use Icinga\Data\ConfigObject; -use Icinga\Exception\ConfigurationError; -use Icinga\Module\Businessprocess\BusinessProcess; -use Icinga\Module\Businessprocess\BpNode; -use Icinga\Module\Businessprocess\Storage\Storage; use DirectoryIterator; +use Icinga\Application\Icinga; +use Icinga\Module\Businessprocess\BpConfig; use Icinga\Exception\SystemPermissionException; -use Icinga\Application\Benchmark; class LegacyStorage extends Storage { + /** @var string */ protected $configDir; - protected $parsing_line_number; - - protected $currentFilename; - public function getConfigDir() { if ($this->configDir === null) { - $dir = Icinga::app() - ->getModuleManager() - ->getModule('businessprocess') - ->getConfigDir(); - - // TODO: This is silly. We need Config::requireDirectory(). - if (! is_dir($dir)) { - if (! is_dir(dirname($dir))) { - if (! @mkdir(dirname($dir))) { - throw new SystemPermissionException('Could not create config directory "%s"', dirname($dir)); - } - } - if (! mkdir($dir)) { - throw new SystemPermissionException('Could not create config directory "%s"', $dir); - } - } - $dir = $dir . '/processes'; - if (! is_dir($dir)) { - if (! mkdir($dir)) { - throw new SystemPermissionException('Could not create config directory "%s"', $dir); - } - } - $this->configDir = $dir; + $this->prepareDefaultConfigDir(); } + return $this->configDir; } + protected function prepareDefaultConfigDir() + { + $dir = Icinga::app() + ->getModuleManager() + ->getModule('businessprocess') + ->getConfigDir(); + + // TODO: This is silly. We need Config::requireDirectory(). + if (! is_dir($dir)) { + if (! is_dir(dirname($dir))) { + if (! @mkdir(dirname($dir))) { + throw new SystemPermissionException('Could not create config directory "%s"', dirname($dir)); + } + } + if (! mkdir($dir)) { + throw new SystemPermissionException('Could not create config directory "%s"', $dir); + } + } + $dir = $dir . '/processes'; + if (! is_dir($dir)) { + if (! mkdir($dir)) { + throw new SystemPermissionException('Could not create config directory "%s"', $dir); + } + } + + $this->configDir = $dir; + } + /** - * @return array + * @inheritdoc */ public function listProcesses() { $files = array(); - foreach (new DirectoryIterator($this->getConfigDir()) as $file) { - if($file->isDot()) continue; - $filename = $file->getFilename(); - if (substr($filename, -5) === '.conf') { - $name = substr($filename, 0, -5); - $header = $this->readHeader($file->getPathname(), $name); - if ($header['Title'] === null) { - $files[$name] = $name; - } else { - $files[$name] = sprintf('%s (%s)', $header['Title'], $name); - } + + foreach ($this->listAllProcessNames() as $name) { + $meta = $this->loadMetadata($name); + if (! $meta->canRead()) { + continue; } + + $files[$name] = $meta->getExtendedTitle(); } - natsort($files); + natcasesort($files); return $files; } - protected function readHeader($file, $name) + /** + * @inheritdoc + */ + public function listProcessNames() { - $fh = fopen($file, 'r'); - $cnt = 0; - $header = array( - 'Title' => null, - 'Owner' => null, - 'Backend' => null, - 'Statetype' => 'soft', - 'SLA Hosts' => null - ); - while ($cnt < 15 && false !== ($line = fgets($fh))) { - $cnt++; - if (preg_match('/^\s*#\s+(.+?)\s*:\s*(.+)$/', $line, $m)) { - if (array_key_exists($m[1], $header)) { - $header[$m[1]] = $m[2]; - } + $files = array(); + + foreach ($this->listAllProcessNames() as $name) { + $meta = $this->loadMetadata($name); + if (! $meta->canRead()) { + continue; } + + $files[$name] = $name; } - return $header; + + natcasesort($files); + return $files; } /** + * @inheritdoc */ - public function storeProcess(BusinessProcess $process) + public function listAllProcessNames() + { + $files = array(); + + foreach (new DirectoryIterator($this->getConfigDir()) as $file) { + if ($file->isDot()) { + continue; + } + + $filename = $file->getFilename(); + if (substr($filename, -5) === '.conf') { + $files[] = substr($filename, 0, -5); + } + } + + natcasesort($files); + return $files; + } + + /** + * @inheritdoc + */ + public function loadProcess($name) + { + return LegacyConfigParser::parseFile( + $name, + $this->getFilename($name) + ); + } + + /** + * @inheritdoc + */ + public function storeProcess(BpConfig $process) { - $filename = $this->getFilename($process->getName()); - $content = $process->toLegacyConfigString(); file_put_contents( - $filename, - $content + $this->getFilename($process->getName()), + LegacyConfigRenderer::renderConfig($process) + ); + } + + /** + * @inheritdoc + */ + public function deleteProcess($name) + { + return @unlink($this->getFilename($name)); + } + + /** + * @inheritdoc + */ + public function loadMetadata($name) + { + return LegacyConfigParser::readMetadataFromFileHeader( + $name, + $this->getFilename($name) ); } @@ -118,176 +162,28 @@ class LegacyStorage extends Storage return $this->getConfigDir() . '/' . $name . '.conf'; } + /** + * @param $name + * @param $string + * + * @return BpConfig + */ public function loadFromString($name, $string) { - $bp = new BusinessProcess(); - $bp->setName($name); - $this->parseString($string, $bp); - $this->loadHeader($name, $bp); - return $bp; - } - - public function deleteProcess($name) - { - unlink($this->getFilename($name)); + return LegacyConfigParser::parseString($name, $string); } /** - * @return BusinessProcess + * @param $name + * @return bool */ - public function loadProcess($name) + public function hasProcess($name) { - Benchmark::measure('Loading business process ' . $name); - $bp = new BusinessProcess(); - $bp->setName($name); - $this->parseFile($name, $bp); - $this->loadHeader($name, $bp); - Benchmark::measure('Business process ' . $name . ' loaded'); - return $bp; - } - - protected function loadHeader($name, $bp) - { - // TODO: do not open twice, this is quick and dirty based on existing code - $file = $this->currentFilename = $this->getFilename($name); - $header = $this->readHeader($file, $name); - $bp->setTitle($header['Title']); - if ($header['Backend']) { - $bp->setBackendName($header['Backend']); - } - if ($header['Statetype'] === 'soft') { - $bp->useSoftStates(); - } - } - - protected function parseFile($name, $bp) - { - $file = $this->currentFilename = $this->getFilename($name); - $fh = @fopen($file, 'r'); - if (! $fh) { - throw new SystemPermissionException('Could not open "%s"', $file); + $file = $this->getFilename($name); + if (! is_file($file)) { + return false; } - $this->parsing_line_number = 0; - while ($line = fgets($fh)) { - $this->parseLine($line, $bp); - } - - fclose($fh); - unset($this->parsing_line_number); - unset($this->currentFilename); - } - - protected function parseString($string, $bp) - { - foreach (preg_split('/\n/', $string) as $line) { - $this->parseLine($line, $bp); - } - } - - protected function parseLine(& $line, $bp) - { - $line = trim($line); - - $this->parsing_line_number++; - - if (empty($line)) { - return; - } - if ($line[0] === '#') { - return; - } - - // TODO: substr? - if (preg_match('~^display~', $line)) { - list($display, $name, $desc) = preg_split('~\s*;\s*~', substr($line, 8), 3); - $node = $bp->getNode($name)->setAlias($desc)->setDisplay($display); - if ($display > 0) { - $bp->addRootNode($name); - } - return; - } - - if (preg_match('~^external_info~', $line)) { - list($name, $script) = preg_split('~\s*;\s*~', substr($line, 14), 2); - $node = $bp->getNode($name)->setInfoCommand($script); - return; - } - - // New feature: - // if (preg_match('~^extra_info~', $line)) { - // list($name, $script) = preg_split('~\s*;\s*~', substr($line, 14), 2); - // $node = $this->getNode($name)->setExtraInfo($script); - // } - - if (preg_match('~^info_url~', $line)) { - list($name, $url) = preg_split('~\s*;\s*~', substr($line, 9), 2); - $node = $bp->getNode($name)->setInfoUrl($url); - return; - } - - list($name, $value) = preg_split('~\s*=\s*~', $line, 2); - - if (strpos($name, ';') !== false) { - $this->parseError('No semicolon allowed in varname'); - } - - $op = '&'; - if (preg_match_all('~([\|\+&\!])~', $value, $m)) { - $op = implode('', $m[1]); - for ($i = 1; $i < strlen($op); $i++) { - if ($op[$i] !== $op[$i - 1]) { - $this->parseError('Mixing operators is not allowed'); - } - } - } - $op = $op[0]; - $op_name = $op; - - if ($op === '+') { - if (! preg_match('~^(\d+)\s*of:\s*(.+?)$~', $value, $m)) { - $this->parseError('syntax: = of: + [+ ]*'); - } - $op_name = $m[1]; - $value = $m[2]; - } - $cmps = preg_split('~\s*\\' . $op . '\s*~', $value, -1, PREG_SPLIT_NO_EMPTY); - - foreach ($cmps as & $val) { - if (strpos($val, ';') !== false) { - if ($bp->hasNode($val)) continue; - - list($host, $service) = preg_split('~;~', $val, 2); - if ($service === 'Hoststatus') { - $bp->createHost($host); - } else { - $bp->createService($host, $service); - } - } - if ($val[0] === '@' && strpos($val, ':') !== false) { - list($config, $nodeName) = preg_split('~:\s*~', substr($val, 1), 2); - $bp->createImportedNode($config, $nodeName); - $val = $nodeName; - } - } - - $node = new BpNode($bp, (object) array( - 'name' => $name, - 'operator' => $op_name, - 'child_names' => $cmps - )); - $bp->addNode($name, $node); - } - - protected function parseError($msg) - { - throw new ConfigurationError( - sprintf( - 'Parse error on %s:%s: %s', - $this->currentFilename, - $this->parsing_line_number, - $msg - ) - ); + return $this->loadMetadata($name)->canRead(); } } diff --git a/library/Businessprocess/Storage/Storage.php b/library/Businessprocess/Storage/Storage.php index 21d130f..de3d939 100644 --- a/library/Businessprocess/Storage/Storage.php +++ b/library/Businessprocess/Storage/Storage.php @@ -3,12 +3,20 @@ namespace Icinga\Module\Businessprocess\Storage; use Icinga\Data\ConfigObject; -use Icinga\Module\Businessprocess\BusinessProcess; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Metadata; abstract class Storage { + /** + * @var ConfigObject + */ protected $config; + /** + * Storage constructor. + * @param ConfigObject $config + */ public function __construct(ConfigObject $config) { $this->config = $config; @@ -20,18 +28,65 @@ abstract class Storage } /** + * All processes readable by the current user + * + * The returned array has the form => , sorted + * by title + * * @return array */ abstract public function listProcesses(); /** - * @return BusinessProcess + * All process names readable by the current user + * + * The returned array has the form => and is + * sorted + * + * @return array + */ + abstract public function listProcessNames(); + + /** + * All available process names, regardless of eventual restrictions + * + * @return array + */ + abstract public function listAllProcessNames(); + + /** + * Whether a configuration with the given name exists + * + * @param $name + * + * @return bool + */ + abstract public function hasProcess($name); + + /** + * @param $name + * @return BpConfig */ abstract public function loadProcess($name); /** + * Store eventual changes applied to the given configuration + * + * @param BpConfig $config + * + * @return mixed */ - abstract public function storeProcess(BusinessProcess $name); + abstract public function storeProcess(BpConfig $config); + /** + * @param $name + * @return bool Whether the process has been deleted + */ abstract public function deleteProcess($name); + + /** + * @param string $name + * @return Metadata + */ + abstract public function loadMetadata($name); } diff --git a/library/Businessprocess/Test/BaseTestCase.php b/library/Businessprocess/Test/BaseTestCase.php new file mode 100644 index 0000000..807905d --- /dev/null +++ b/library/Businessprocess/Test/BaseTestCase.php @@ -0,0 +1,76 @@ +app(); + FakeRequest::setConfiguredBaseUrl('/icingaweb2/'); + } + + protected function emptyConfigSection() + { + return Config::module('businessprocess')->getSection('global'); + } + + /*** + * @return BpConfig + */ + protected function makeLoop() + { + return $this->makeInstance()->loadFromString( + 'loop', + "a = b\nb = c\nc = a\nd = a" + ); + } + + /** + * @return LegacyStorage + */ + protected function makeInstance() + { + return new LegacyStorage($this->emptyConfigSection()); + } + + /** + * @param null $subDir + * @return string + */ + protected function getTestsBaseDir($subDir = null) + { + $dir = dirname(dirname(dirname(__DIR__))) . '/test'; + if ($subDir === null) { + return $dir; + } else { + return $dir . '/' . ltrim($subDir, '/'); + } + } + + /** + * @return ApplicationBootstrap + */ + protected function app() + { + if (self::$app === null) { + self::$app = Icinga::app(); + } + + return self::$app; + } +} diff --git a/library/Businessprocess/Test/Bootstrap.php b/library/Businessprocess/Test/Bootstrap.php new file mode 100644 index 0000000..3bd5187 --- /dev/null +++ b/library/Businessprocess/Test/Bootstrap.php @@ -0,0 +1,28 @@ +getModuleManager() + ->loadModule('businessprocess', $basedir); + } +} diff --git a/library/Businessprocess/Web/Component/ActionBar.php b/library/Businessprocess/Web/Component/ActionBar.php new file mode 100644 index 0000000..3453af2 --- /dev/null +++ b/library/Businessprocess/Web/Component/ActionBar.php @@ -0,0 +1,15 @@ + 'action-bar'); +} diff --git a/library/Businessprocess/Web/Component/Content.php b/library/Businessprocess/Web/Component/Content.php new file mode 100644 index 0000000..f8336ce --- /dev/null +++ b/library/Businessprocess/Web/Component/Content.php @@ -0,0 +1,12 @@ + 'content'); +} diff --git a/library/Businessprocess/Web/Component/Controls.php b/library/Businessprocess/Web/Component/Controls.php new file mode 100644 index 0000000..861a852 --- /dev/null +++ b/library/Businessprocess/Web/Component/Controls.php @@ -0,0 +1,12 @@ + 'controls'); +} diff --git a/library/Businessprocess/Web/Component/Dashboard.php b/library/Businessprocess/Web/Component/Dashboard.php new file mode 100644 index 0000000..698daec --- /dev/null +++ b/library/Businessprocess/Web/Component/Dashboard.php @@ -0,0 +1,108 @@ + 'overview-dashboard', + 'data-base-target' => '_next' + ); + + /** @var Auth */ + protected $auth; + + /** @var Storage */ + protected $storage; + + /** + * Dashboard constructor. + * @param Auth $auth + * @param Storage $storage + */ + protected function __construct(Auth $auth, Storage $storage) + { + $this->auth = $auth; + $this->storage = $storage; + // TODO: Auth? + $processes = $storage->listProcessNames(); + $this->add( + HtmlTag::h1($this->translate('Welcome to your Business Process Overview')) + ); + $this->add( + HtmlTag::p( + $this->translate( + 'From here you can reach all your defined Business Process' + . ' configurations, create new or modify existing ones' + ) + ) + ); + if ($auth->hasPermission('businessprocess/create')) { + $this->add( + new DashboardAction( + $this->translate('Create'), + $this->translate('Create a new Business Process configuration'), + 'plus', + 'businessprocess/process/create', + null, + array('class' => 'addnew') + ) + )->add( + new DashboardAction( + $this->translate('Upload'), + $this->translate('Upload an existing Business Process configuration'), + 'upload', + 'businessprocess/process/upload', + null, + array('class' => 'addnew') + ) + ); + + } elseif (empty($processes)) { + $this->addContent( + Container::create() + ->add(HtmlTag::h1($this->translate('Not available'))) + ->add(HtmlTag::p($this->translate('No Business Process has been defined for you'))) + ); + } + + foreach ($processes as $name) { + $meta = $storage->loadMetadata($name); + $title = $meta->get('Title'); + if ($title) { + $title = sprintf('%s (%s)', $title, $name); + } else { + $title = $name; + } + $this->add(new DashboardAction( + $title, + $meta->get('Description'), + 'sitemap', + 'businessprocess/process/show', + array('config' => $name) + )); + } + } + + /** + * @param Auth $auth + * @param Storage $storage + * @return static + */ + public static function create(Auth $auth, Storage $storage) + { + return new static($auth, $storage); + } +} diff --git a/library/Businessprocess/Web/Component/DashboardAction.php b/library/Businessprocess/Web/Component/DashboardAction.php new file mode 100644 index 0000000..b413871 --- /dev/null +++ b/library/Businessprocess/Web/Component/DashboardAction.php @@ -0,0 +1,29 @@ + 'action'); + + public function __construct($title, $description, $icon, $url, $urlParams = null, $attributes = null) + { + $this->add( + Link::create( + Icon::create($icon), + $url, + $urlParams, + $attributes + )->add( + Element::create('span', array('class' => 'header'))->addContent($title) + )->addContent($description) + ); + } +} diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php new file mode 100644 index 0000000..9bb5ad3 --- /dev/null +++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php @@ -0,0 +1,108 @@ +getMetadata(); + + if ($renderer instanceof TreeRenderer) { + $this->add( + Link::create( + $this->translate('Tiles'), + $url->with('mode', 'tile'), + null, + array('class' => 'icon-dashboard') + ) + ); + } else { + $this->add( + Link::create( + $this->translate('Tree'), + $url->with('mode', 'tree'), + null, + array('class' => 'icon-sitemap') + ) + ); + } + + $this->add( + Link::create( + $this->translate('Fullscreen'), + $url->with('showFullscreen', true), + null, + array( + 'class' => 'icon-resize-full-alt', + 'title' => $this->translate('Switch to fullscreen mode'), + 'data-base-target' => '_main', + ) + ) + ); + + $hasChanges = $config->hasSimulations() || $config->hasBeenChanged(); + + if ($renderer->isLocked()) { + $this->add( + Link::create( + $this->translate('Unlock'), + $url->with('unlocked', true), + null, + array( + 'class' => 'icon-lock-open', + 'title' => $this->translate('Unlock this process'), + ) + ) + ); + } elseif (! $hasChanges) { + $this->add( + Link::create( + $this->translate('Lock'), + $url->without('unlocked')->without('action'), + null, + array( + 'class' => 'icon-lock', + 'title' => $this->translate('Lock this process'), + ) + ) + ); + } + + if ($hasChanges || (! $renderer->isLocked()) && $meta->canModify()) { + $this->add( + Link::create( + $this->translate('Config'), + 'businessprocess/process/config', + $this->currentProcessParams($url), + array( + 'class' => 'icon-wrench', + 'title' => $this->translate('Modify this process'), + 'data-base-target' => '_next', + ) + ) + ); + } + } + + protected function currentProcessParams($url) + { + $urlParams = $url->getParams(); + $params = array(); + foreach (array('config', 'node') as $name) { + if ($value = $urlParams->get($name)) { + $params[$name] = $value; + } + } + + return $params; + } +} diff --git a/library/Businessprocess/Web/Component/Tabs.php b/library/Businessprocess/Web/Component/Tabs.php new file mode 100644 index 0000000..a6e5e6a --- /dev/null +++ b/library/Businessprocess/Web/Component/Tabs.php @@ -0,0 +1,10 @@ +getModuleManager(); + if (! $m->hasLoaded('monitoring') && $m->hasInstalled('monitoring')) { + $m->loadModule('monitoring'); + } + $this->controls(); + $this->content(); + $this->url(); + $this->view->showFullscreen + = $this->showFullscreen + = (bool) $this->_helper->layout()->showFullscreen; + + $this->view->compact = $this->params->get('view') === 'compact'; + $this->setViewScript('default'); + } + + /** + * @return Url + */ + protected function url() + { + if ($this->url === null) { + $this->url = Url::fromPath( + $this->getRequest()->getUrl()->getPath() + )->setParams($this->params); + } + + return $this->url; + } + + /** + * @return ActionBar + */ + protected function actions() + { + if ($this->view->actions === null) { + $this->view->actions = new ActionBar(); + } + + return $this->view->actions; + } + + /** + * @return Controls + */ + protected function controls() + { + if ($this->view->controls === null) { + $this->view->controls = Controls::create(); + } + + return $this->view->controls; + } + + /** + * @return Content + */ + protected function content() + { + if ($this->view->content === null) { + $this->view->content = Content::create(); + } + + return $this->view->content; + } + + /** + * @param $label + * @return Tabs + */ + protected function singleTab($label) + { + return $this->tabs()->add( + 'tab', + array( + 'label' => $label, + 'url' => $this->getRequest()->getUrl() + ) + )->activate('tab'); + } + + /** + * @return Tabs + */ + protected function defaultTab() + { + return $this->singleTab($this->translate('Business Process')); + } + + /** + * @return Tabs + */ + protected function overviewTab() + { + return $this->tabs()->add( + 'overview', + array( + 'label' => $this->translate('Business Process'), + 'url' => 'businessprocess' + ) + )->activate('overview'); + } + + /** + * @return Tabs + */ + protected function tabs() + { + // Todo: do not add to view once all of them render controls() + if ($this->mytabs === null) { + $tabs = new Tabs(); + //$this->controls()->add($tabs); + $this->mytabs = $tabs; + } + + return $this->mytabs; + } + + protected function session() + { + return $this->Window()->getSessionNamespace('businessprocess'); + } + + protected function setViewScript($name) + { + $this->_helper->viewRenderer->setNoController(true); + $this->_helper->viewRenderer->setScriptAction($name); + return $this; + } + + protected function setTitle($title) + { + $args = func_get_args(); + array_shift($args); + $this->view->title = vsprintf($title, $args); + return $this; + } + + protected function addTitle($title) + { + $args = func_get_args(); + array_shift($args); + $this->view->title = vsprintf($title, $args); + $this->controls()->add(HtmlTag::h1($this->view->title)); + return $this; + } + + protected function loadModifiedBpConfig() + { + $bp = $this->loadBpConfig(); + $changes = ProcessChanges::construct($bp, $this->session()); + if ($this->params->get('dismissChanges')) { + Notification::success( + sprintf( + $this->translate('%d pending change(s) have been dropped'), + $changes->count() + ) + ); + $changes->clear(); + $this->redirectNow($this->url()->without('dismissChanges')->without('unlocked')); + } + $bp->applyChanges($changes); + return $bp; + } + + protected function doNotRender() + { + $this->_helper->layout()->disableLayout(); + $this->_helper->viewRenderer->setNoRender(true); + return $this; + } + + protected function loadBpConfig() + { + $name = $this->params->get('config'); + $storage = $this->storage(); + + if (! $storage->hasProcess($name)) { + $this->httpNotFound( + $this->translate('No such process config: "%s"'), + $name + ); + } + + $modifications = $this->session()->get('modifications', array()); + if (array_key_exists($name, $modifications)) { + $bp = $storage->loadFromString($name, $modifications[$name]); + } else { + $bp = $storage->loadProcess($name); + } + + // allow URL parameter to override configured state type + if (null !== ($stateType = $this->params->get('state_type'))) { + if ($stateType === 'soft') { + $bp->useSoftStates(); + } + if ($stateType === 'hard') { + $bp->useHardStates(); + } + } + + $this->view->bpconfig = $this->bp = $bp; + $this->view->configName = $bp->getName(); + + return $bp; + } + + public function loadForm($name) + { + return FormLoader::load($name, $this->Module()); + } + + /** + * @return LegacyStorage|Storage + */ + protected function storage() + { + if ($this->storage === null) { + $this->storage = new LegacyStorage( + $this->Config()->getSection('global') + ); + } + + return $this->storage; + } +} diff --git a/library/Businessprocess/Web/FakeRequest.php b/library/Businessprocess/Web/FakeRequest.php new file mode 100644 index 0000000..4e54117 --- /dev/null +++ b/library/Businessprocess/Web/FakeRequest.php @@ -0,0 +1,26 @@ +callZfConstructor($this->handleOptions($options)) + ->initializePrefixPaths(); + } + + protected function callZfConstructor($options = null) + { + parent::__construct($options); + return $this; + } + + protected function initializePrefixPaths() + { + $this->addPrefixPathsForBusinessprocess(); + if ($this->icingaModule && $this->icingaModuleName !== 'businessprocess') { + $this->addPrefixPathsForModule($this->icingaModule); + } + } + + protected function addPrefixPathsForBusinessprocess() + { + $module = Icinga::app() + ->getModuleManager() + ->loadModule('businessprocess') + ->getModule('businessprocess'); + + $this->addPrefixPathsForModule($module); + } + + public function addPrefixPathsForModule(Module $module) + { + $basedir = sprintf( + '%s/%s/Web/Form', + $module->getLibDir(), + ucfirst($module->getName()) + ); + + $this->addPrefixPaths(array( + array( + 'prefix' => __NAMESPACE__ . '\\Element\\', + 'path' => $basedir . '/Element', + 'type' => static::ELEMENT + ) + )); + + return $this; + } + + public function addHidden($name, $value = null) + { + $this->addElement('hidden', $name); + $el = $this->getElement($name); + $el->setDecorators(array('ViewHelper')); + if ($value !== null) { + $this->setDefault($name, $value); + $el->setValue($value); + } + + return $this; + } + + // TODO: Should be an element + public function addHtmlHint($html, $options = array()) + { + return $this->addHtml('
' . $html . '
', $options); + } + + public function addHtml($html, $options = array()) + { + if (array_key_exists('name', $options)) { + $name = $options['name']; + unset($options['name']); + } else { + $name = '_HINT' . ++$this->hintCount; + } + + $this->addElement('simpleNote', $name, $options); + $this->getElement($name) + ->setValue($html) + ->setIgnore(true) + ->setDecorators(array('ViewHelper')); + + return $this; + } + + public function optionalEnum($enum, $nullLabel = null) + { + if ($nullLabel === null) { + $nullLabel = $this->translate('- please choose -'); + } + + return array(null => $nullLabel) + $enum; + } + + protected function handleOptions($options = null) + { + if ($options === null) { + return $options; + } + + if (array_key_exists('icingaModule', $options)) { + /** @var Module icingaModule */ + $this->icingaModule = $options['icingaModule']; + $this->icingaModuleName = $this->icingaModule->getName(); + unset($options['icingaModule']); + } + + return $options; + } + + public function setIcingaModule(Module $module) + { + $this->icingaModule = $module; + return $this; + } + + protected function loadForm($name, Module $module = null) + { + if ($module === null) { + $module = $this->icingaModule; + } + + return FormLoader::load($name, $module); + } + + protected function valueIsEmpty($value) + { + if (is_array($value)) { + return empty($value); + } + + return strlen($value) === 0; + } + + public function translate($string) + { + if ($this->icingaModuleName === null) { + return t($string); + } else { + return mt($this->icingaModuleName, $string); + } + } +} diff --git a/library/Businessprocess/Web/Form/QuickForm.php b/library/Businessprocess/Web/Form/QuickForm.php index 0a5cd19..c657745 100644 --- a/library/Businessprocess/Web/Form/QuickForm.php +++ b/library/Businessprocess/Web/Form/QuickForm.php @@ -3,17 +3,17 @@ namespace Icinga\Module\Businessprocess\Web\Form; use Icinga\Application\Icinga; -use Icinga\Application\Modules\Module; use Icinga\Exception\ProgrammingError; use Icinga\Web\Notification; use Icinga\Web\Request; +use Icinga\Web\Response; use Icinga\Web\Url; -use Zend_Form; +use Exception; /** * QuickForm wants to be a base class for simple forms */ -abstract class QuickForm extends Zend_Form +abstract class QuickForm extends QuickBaseForm { const ID = '__FORM_NAME'; @@ -46,70 +46,111 @@ abstract class QuickForm extends Zend_Form protected $successUrl; - protected $succeeded; - protected $successMessage; protected $submitLabel; protected $submitButtonName; + protected $deleteButtonName; + + protected $fakeSubmitButtonName; + /** * Whether form elements have already been created */ protected $didSetup = false; - /** - * The Icinga module this form belongs to. Usually only set if the - * form is initialized through the FormLoader - */ - protected $icingaModule; - - protected $icingaModuleName; - - protected $hintCount = 0; + protected $isApiRequest = false; public function __construct($options = null) { - parent::__construct($this->handleOptions($options)); + parent::__construct($options); + $this->setMethod('post'); - $this->setAction(Url::fromRequest()); - $this->createIdElement(); - $this->regenerateCsrfToken(); + $this->getActionFromRequest() + ->createIdElement() + ->regenerateCsrfToken() + ->setPreferredDecorators(); } - protected function handleOptions($options = null) + protected function getActionFromRequest() { - if ($options === null) { - return $options; - } + $this->setAction(Url::fromRequest()); + return $this; + } - if (array_key_exists('icingaModule', $options)) { - $this->icingaModule = $options['icingaModule']; - $this->icingaModuleName = $this->icingaModule->getName(); - unset($options['icingaModule']); - } + protected function setPreferredDecorators() + { + $this->setAttrib('class', 'autofocus'); + $this->setDecorators( + array( + 'Description', + array('FormErrors', array('onlyCustomFormErrors' => true)), + 'FormElements', + 'Form' + ) + ); - return $options; + return $this; } protected function addSubmitButtonIfSet() { - if (false !== ($label = $this->getSubmitLabel())) { - $el = $this->createElement('submit', $label)->setLabel($label)->removeDecorator('Label'); - $this->submitButtonName = $el->getName(); - $this->addElement($el); + if (false === ($label = $this->getSubmitLabel())) { + return; } + + if ($this->submitButtonName && $el = $this->getElement($this->submitButtonName)) { + return; + } + + $el = $this->createElement('submit', $label) + ->setLabel($label) + ->setDecorators(array('ViewHelper')); + $this->submitButtonName = $el->getName(); + $this->addElement($el); + + $fakeEl = $this->createElement('submit', '_FAKE_SUBMIT') + ->setLabel($label) + ->setDecorators(array('ViewHelper')); + $this->fakeSubmitButtonName = $fakeEl->getName(); + $this->addElement($fakeEl); + + $this->addDisplayGroup( + array($this->fakeSubmitButtonName), + 'fake_button', + array( + 'decorators' => array('FormElements'), + 'order' => 1, + ) + ); + + $grp = array( + $this->submitButtonName, + $this->deleteButtonName + ); + $this->addDisplayGroup($grp, 'buttons', array( + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'dl')), + 'DtDdWrapper', + ), + 'order' => 1000, + )); } - // TODO: This is ugly, we need to defer button creation - protected function moveSubmitToBottom() + protected function addSimpleDisplayGroup($elements, $name, $options) { - $name = $this->submitButtonName; - if ($name && ($submit = $this->getElement($name))) { - $this->removeElement($name); - $this->addElement($submit); + if (! array_key_exists('decorators', $options)) { + $options['decorators'] = array( + 'FormElements', + array('HtmlTag', array('tag' => 'dl')), + 'Fieldset', + ); } + return $this->addDisplayGroup($elements, $name, $options); + } protected function createIdElement() @@ -117,13 +158,13 @@ abstract class QuickForm extends Zend_Form $this->detectName(); $this->addHidden(self::ID, $this->getName()); $this->getElement(self::ID)->setIgnore(true); + return $this; } - protected function getSentValue($name, $default = null) + public function getSentValue($name, $default = null) { $request = $this->getRequest(); - - if ($request->isPost()) { + if ($request->isPost() && $this->hasBeenSent()) { return $request->getPost($name); } else { return $default; @@ -145,13 +186,15 @@ abstract class QuickForm extends Zend_Form return $this; } - protected function loadForm($name, Module $module = null) + public function setApiRequest($isApiRequest = true) { - if ($module === null) { - $module = $this->icingaModule; - } + $this->isApiRequest = $isApiRequest; + return $this; + } - return FormLoader::load($name, $module); + public function isApiRequest() + { + return $this->isApiRequest; } public function regenerateCsrfToken() @@ -171,54 +214,32 @@ abstract class QuickForm extends Zend_Form return $this; } - public function addHidden($name, $value = null) + public function setSuccessUrl($url, $params = null) { - $this->addElement('hidden', $name); - $el = $this->getElement($name); - $el->setDecorators(array('ViewHelper')); - if ($value !== null) { - $this->setDefault($name, $value); - $el->setValue($value); + if (! $url instanceof Url) { + $url = Url::fromPath($url); + } + if ($params !== null) { + $url->setParams($params); } - - return $this; - } - - public function addHtmlHint($html, $options = array()) - { - return $this->addHtml('
' . $html . '
', $options); - } - - public function addHtml($html, $options = array()) - { - $name = '_HINT' . ++$this->hintCount; - $this->addElement('note', $name, $options); - $this->getElement($name) - ->setValue($html) - ->setIgnore(true) - ->removeDecorator('Label'); - - return $this; - } - - public function optionalEnum($enum) - { - return array( - null => $this->translate('- please choose -') - ) + $enum; - } - - public function succeeded() - { - return $this->succeeded; - } - - public function setSuccessUrl($url) - { $this->successUrl = $url; return $this; } + public function getSuccessUrl() + { + $url = $this->successUrl ?: $this->getAction(); + if (! $url instanceof Url) { + $url = Url::fromPath($url); + } + + return $url; + } + + protected function beforeSetup() + { + } + public function setup() { } @@ -236,35 +257,49 @@ abstract class QuickForm extends Zend_Form return parent::setAction($action); } - public function setIcingaModule(Module $module) - { - $this->icingaModule = $module; - return $this; - } - public function hasBeenSubmitted() { if ($this->hasBeenSubmitted === null) { $req = $this->getRequest(); if ($req->isPost()) { - $post = $req->getPost(); - $name = $this->submitButtonName; - - if ($name === null) { - $this->hasBeenSubmitted = $this->hasBeenSent(); - } else { - $el = $this->getElement($name); - $this->hasBeenSubmitted = array_key_exists($name, $post) - && $post[$name] === $this->getSubmitLabel(); + if (! $this->hasSubmitButton()) { + return $this->hasBeenSubmitted = $this->hasBeenSent(); } + + $this->hasBeenSubmitted = $this->pressedButton( + $this->fakeSubmitButtonName, + $this->getSubmitLabel() + ) || $this->pressedButton( + $this->submitButtonName, + $this->getSubmitLabel() + ); } else { - $this->hasBeenSubmitted === false; + $this->hasBeenSubmitted = false; } } return $this->hasBeenSubmitted; } + protected function hasSubmitButton() + { + return $this->submitButtonName !== null; + } + + protected function pressedButton($name, $label) + { + $req = $this->getRequest(); + if (! $req->isPost()) { + return false; + } + + $req = $this->getRequest(); + $post = $req->getPost(); + + return array_key_exists($name, $post) + && $post[$name] === $label; + } + protected function beforeValidation($data = array()) { } @@ -272,6 +307,7 @@ abstract class QuickForm extends Zend_Form public function prepareElements() { if (! $this->didSetup) { + $this->beforeSetup(); $this->setup(); $this->addSubmitButtonIfSet(); $this->onSetup(); @@ -283,20 +319,27 @@ abstract class QuickForm extends Zend_Form public function handleRequest(Request $request = null) { - if ($request !== null) { + if ($request === null) { + $request = $this->getRequest(); + } else { $this->setRequest($request); } + $this->prepareElements(); + if ($this->hasBeenSent()) { - $post = $this->getRequest()->getPost(); + $post = $request->getPost(); if ($this->hasBeenSubmitted()) { $this->beforeValidation($post); if ($this->isValid($post)) { - $this->onSuccess(); - $this->succeeded = true; + try { + $this->onSuccess(); + } catch (Exception $e) { + $this->addException($e); + $this->onFailure(); + } } else { $this->onFailure(); - $this->succeeded = false; } } else { $this->setDefaults($post); @@ -308,12 +351,21 @@ abstract class QuickForm extends Zend_Form return $this; } - public function translate($string) + public function addException(Exception $e, $elementName = null) { - if ($this->icingaModuleName === null) { - return t($string); + $file = preg_split('/[\/\\\]/', $e->getFile(), -1, PREG_SPLIT_NO_EMPTY); + $file = array_pop($file); + $msg = sprintf( + '%s (%s:%d)', + $e->getMessage(), + $file, + $e->getLine() + ); + + if ($el = $this->getElement($elementName)) { + $el->addError($msg); } else { - return mt($this->icingaModuleName, $string); + $this->addError($msg); } } @@ -341,7 +393,13 @@ abstract class QuickForm extends Zend_Form public function redirectOnSuccess($message = null) { - $url = $this->successUrl ?: $this->getAction(); + if ($this->isApiRequest()) { + // TODO: Set the status line message? + $this->successMessage = $this->getSuccessMessage($message); + return; + } + + $url = $this->getSuccessUrl(); $this->notifySuccess($this->getSuccessMessage($message)); $this->redirectAndExit($url); } @@ -367,7 +425,15 @@ abstract class QuickForm extends Zend_Form protected function redirectAndExit($url) { - Icinga::app()->getFrontController()->getResponse()->redirectAndExit($url); + /** @var Response $response */ + $response = Icinga::app()->getFrontController()->getResponse(); + $response->redirectAndExit($url); + } + + protected function setHttpResponseCode($code) + { + Icinga::app()->getFrontController()->getResponse()->setHttpResponseCode($code); + return $this; } protected function onRequest() @@ -386,10 +452,15 @@ abstract class QuickForm extends Zend_Form return $this; } + /** + * @return Request + */ public function getRequest() { if ($this->request === null) { - $this->setRequest(Icinga::app()->getFrontController()->getRequest()); + /** @var Request $request */ + $request = Icinga::app()->getFrontController()->getRequest(); + $this->setRequest($request); } return $this->request; } @@ -397,13 +468,20 @@ abstract class QuickForm extends Zend_Form public function hasBeenSent() { if ($this->hasBeenSent === null) { - $req = $this->getRequest(); + + /** @var Request $req */ + if ($this->request === null) { + $req = Icinga::app()->getFrontController()->getRequest(); + } else { + $req = $this->request; + } + if ($req->isPost()) { $post = $req->getPost(); $this->hasBeenSent = array_key_exists(self::ID, $post) && $post[self::ID] === $this->getName(); } else { - $this->hasBeenSent === false; + $this->hasBeenSent = false; } } diff --git a/library/Businessprocess/Web/Url.php b/library/Businessprocess/Web/Url.php new file mode 100644 index 0000000..cdd5efb --- /dev/null +++ b/library/Businessprocess/Web/Url.php @@ -0,0 +1,78 @@ +setPath($url); + } + + $parts = parse_url($url); + + $self->setBasePath($request->getBaseUrl()); + if (isset($parts['path'])) { + $self->setPath($parts['path']); + } + + if (isset($urlParts['query'])) { + $params = UrlParams::fromQueryString($urlParts['query'])->mergeValues($params); + } + + if (isset($parts['fragment'])) { + $self->setAnchor($parts['fragment']); + } + + $self->setParams($params); + return $self; + } + + public function setBasePath($basePath) + { + if (property_exists($this, 'basePath')) { + parent::setBasePath($basePath); + } else { + return $this->setBaseUrl($basePath); + } + } + + protected static function getRequest() + { + $app = Icinga::app(); + if ($app->isCli()) { + return new FakeRequest(); + } else { + return $app->getRequest(); + } + } +} diff --git a/library/vendor/PHP-FineDiff/finediff.php b/library/vendor/PHP-FineDiff/finediff.php deleted file mode 100644 index 920365a..0000000 --- a/library/vendor/PHP-FineDiff/finediff.php +++ /dev/null @@ -1,688 +0,0 @@ -copy->insert -* command (swap) for when the inserted segment is exactly the same -* as the deleted one, and with only a copy operation in between. -* TODO: How often this case occurs? Is it worth it? Can only -* be done as a postprocessing method (->optimize()?) -*/ -abstract class FineDiffOp { - abstract public function getFromLen(); - abstract public function getToLen(); - abstract public function getOpcode(); - } - -class FineDiffDeleteOp extends FineDiffOp { - public function __construct($len) { - $this->fromLen = $len; - } - public function getFromLen() { - return $this->fromLen; - } - public function getToLen() { - return 0; - } - public function getOpcode() { - if ( $this->fromLen === 1 ) { - return 'd'; - } - return "d{$this->fromLen}"; - } - } - -class FineDiffInsertOp extends FineDiffOp { - public function __construct($text) { - $this->text = $text; - } - public function getFromLen() { - return 0; - } - public function getToLen() { - return strlen($this->text); - } - public function getText() { - return $this->text; - } - public function getOpcode() { - $to_len = strlen($this->text); - if ( $to_len === 1 ) { - return "i:{$this->text}"; - } - return "i{$to_len}:{$this->text}"; - } - } - -class FineDiffReplaceOp extends FineDiffOp { - public function __construct($fromLen, $text) { - $this->fromLen = $fromLen; - $this->text = $text; - } - public function getFromLen() { - return $this->fromLen; - } - public function getToLen() { - return strlen($this->text); - } - public function getText() { - return $this->text; - } - public function getOpcode() { - if ( $this->fromLen === 1 ) { - $del_opcode = 'd'; - } - else { - $del_opcode = "d{$this->fromLen}"; - } - $to_len = strlen($this->text); - if ( $to_len === 1 ) { - return "{$del_opcode}i:{$this->text}"; - } - return "{$del_opcode}i{$to_len}:{$this->text}"; - } - } - -class FineDiffCopyOp extends FineDiffOp { - public function __construct($len) { - $this->len = $len; - } - public function getFromLen() { - return $this->len; - } - public function getToLen() { - return $this->len; - } - public function getOpcode() { - if ( $this->len === 1 ) { - return 'c'; - } - return "c{$this->len}"; - } - public function increase($size) { - return $this->len += $size; - } - } - -/** -* FineDiff ops -* -* Collection of ops -*/ -class FineDiffOps { - public function appendOpcode($opcode, $from, $from_offset, $from_len) { - if ( $opcode === 'c' ) { - $edits[] = new FineDiffCopyOp($from_len); - } - else if ( $opcode === 'd' ) { - $edits[] = new FineDiffDeleteOp($from_len); - } - else /* if ( $opcode === 'i' ) */ { - $edits[] = new FineDiffInsertOp(substr($from, $from_offset, $from_len)); - } - } - public $edits = array(); - } - -/** -* FineDiff class -* -* TODO: Document -* -*/ -class FineDiff { - - /**------------------------------------------------------------------------ - * - * Public section - * - */ - - /** - * Constructor - * ... - * The $granularityStack allows FineDiff to be configurable so that - * a particular stack tailored to the specific content of a document can - * be passed. - */ - public function __construct($from_text = '', $to_text = '', $granularityStack = null) { - // setup stack for generic text documents by default - $this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity; - $this->edits = array(); - $this->from_text = $from_text; - $this->doDiff($from_text, $to_text); - } - - public function getOps() { - return $this->edits; - } - - public function getOpcodes() { - $opcodes = array(); - foreach ( $this->edits as $edit ) { - $opcodes[] = $edit->getOpcode(); - } - return implode('', $opcodes); - } - - public function renderDiffToHTML() { - $in_offset = 0; - ob_start(); - foreach ( $this->edits as $edit ) { - $n = $edit->getFromLen(); - if ( $edit instanceof FineDiffCopyOp ) { - FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n); - } - else if ( $edit instanceof FineDiffDeleteOp ) { - FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n); - } - else if ( $edit instanceof FineDiffInsertOp ) { - FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen()); - } - else /* if ( $edit instanceof FineDiffReplaceOp ) */ { - FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n); - FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen()); - } - $in_offset += $n; - } - return ob_get_clean(); - } - - /**------------------------------------------------------------------------ - * Return an opcodes string describing the diff between a "From" and a - * "To" string - */ - public static function getDiffOpcodes($from, $to, $granularities = null) { - $diff = new FineDiff($from, $to, $granularities); - return $diff->getOpcodes(); - } - - /**------------------------------------------------------------------------ - * Return an iterable collection of diff ops from an opcodes string - */ - public static function getDiffOpsFromOpcodes($opcodes) { - $diffops = new FineDiffOps(); - FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode')); - return $diffops->edits; - } - - /**------------------------------------------------------------------------ - * Re-create the "To" string from the "From" string and an "Opcodes" string - */ - public static function renderToTextFromOpcodes($from, $opcodes) { - ob_start(); - FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode')); - return ob_get_clean(); - } - - /**------------------------------------------------------------------------ - * Render the diff to an HTML string - */ - public static function renderDiffToHTMLFromOpcodes($from, $opcodes) { - ob_start(); - FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode')); - return ob_get_clean(); - } - - /**------------------------------------------------------------------------ - * Generic opcodes parser, user must supply callback for handling - * single opcode - */ - public static function renderFromOpcodes($from, $opcodes, $callback) { - if ( !is_callable($callback) ) { - return; - } - $opcodes_len = strlen($opcodes); - $from_offset = $opcodes_offset = 0; - while ( $opcodes_offset < $opcodes_len ) { - $opcode = substr($opcodes, $opcodes_offset, 1); - $opcodes_offset++; - $n = intval(substr($opcodes, $opcodes_offset)); - if ( $n ) { - $opcodes_offset += strlen(strval($n)); - } - else { - $n = 1; - } - if ( $opcode === 'c' ) { // copy n characters from source - call_user_func($callback, 'c', $from, $from_offset, $n, ''); - $from_offset += $n; - } - else if ( $opcode === 'd' ) { // delete n characters from source - call_user_func($callback, 'd', $from, $from_offset, $n, ''); - $from_offset += $n; - } - else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes - call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n); - $opcodes_offset += 1 + $n; - } - } - } - - /** - * Stock granularity stacks and delimiters - */ - - const paragraphDelimiters = "\n\r"; - public static $paragraphGranularity = array( - FineDiff::paragraphDelimiters - ); - const sentenceDelimiters = ".\n\r"; - public static $sentenceGranularity = array( - FineDiff::paragraphDelimiters, - FineDiff::sentenceDelimiters - ); - const wordDelimiters = " \t.\n\r"; - public static $wordGranularity = array( - FineDiff::paragraphDelimiters, - FineDiff::sentenceDelimiters, - FineDiff::wordDelimiters - ); - const characterDelimiters = ""; - public static $characterGranularity = array( - FineDiff::paragraphDelimiters, - FineDiff::sentenceDelimiters, - FineDiff::wordDelimiters, - FineDiff::characterDelimiters - ); - - public static $textStack = array( - ".", - " \t.\n\r", - "" - ); - - /**------------------------------------------------------------------------ - * - * Private section - * - */ - - /** - * Entry point to compute the diff. - */ - private function doDiff($from_text, $to_text) { - $this->last_edit = false; - $this->stackpointer = 0; - $this->from_text = $from_text; - $this->from_offset = 0; - // can't diff without at least one granularity specifier - if ( empty($this->granularityStack) ) { - return; - } - $this->_processGranularity($from_text, $to_text); - } - - /** - * This is the recursive function which is responsible for - * handling/increasing granularity. - * - * Incrementally increasing the granularity is key to compute the - * overall diff in a very efficient way. - */ - private function _processGranularity($from_segment, $to_segment) { - $delimiters = $this->granularityStack[$this->stackpointer++]; - $has_next_stage = $this->stackpointer < count($this->granularityStack); - foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) { - // increase granularity - if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) { - $this->_processGranularity( - substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()), - $fragment_edit->getText() - ); - } - // fuse copy ops whenever possible - else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) { - $this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen()); - $this->from_offset += $fragment_edit->getFromLen(); - } - else { - /* $fragment_edit instanceof FineDiffCopyOp */ - /* $fragment_edit instanceof FineDiffDeleteOp */ - /* $fragment_edit instanceof FineDiffInsertOp */ - $this->edits[] = $this->last_edit = $fragment_edit; - $this->from_offset += $fragment_edit->getFromLen(); - } - } - $this->stackpointer--; - } - - /** - * This is the core algorithm which actually perform the diff itself, - * fragmenting the strings as per specified delimiters. - * - * This function is naturally recursive, however for performance purpose - * a local job queue is used instead of outright recursivity. - */ - private static function doFragmentDiff($from_text, $to_text, $delimiters) { - // Empty delimiter means character-level diffing. - // In such case, use code path optimized for character-level - // diffing. - if ( empty($delimiters) ) { - return FineDiff::doCharDiff($from_text, $to_text); - } - - $result = array(); - - // fragment-level diffing - $from_text_len = strlen($from_text); - $to_text_len = strlen($to_text); - $from_fragments = FineDiff::extractFragments($from_text, $delimiters); - $to_fragments = FineDiff::extractFragments($to_text, $delimiters); - - $jobs = array(array(0, $from_text_len, 0, $to_text_len)); - - $cached_array_keys = array(); - - while ( $job = array_pop($jobs) ) { - - // get the segments which must be diff'ed - list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job; - - // catch easy cases first - $from_segment_length = $from_segment_end - $from_segment_start; - $to_segment_length = $to_segment_end - $to_segment_start; - if ( !$from_segment_length || !$to_segment_length ) { - if ( $from_segment_length ) { - $result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length); - } - else if ( $to_segment_length ) { - $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length)); - } - continue; - } - - // find longest copy operation for the current segments - $best_copy_length = 0; - - $from_base_fragment_index = $from_segment_start; - - $cached_array_keys_for_current_segment = array(); - - while ( $from_base_fragment_index < $from_segment_end ) { - $from_base_fragment = $from_fragments[$from_base_fragment_index]; - $from_base_fragment_length = strlen($from_base_fragment); - // performance boost: cache array keys - if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) { - if ( !isset($cached_array_keys[$from_base_fragment]) ) { - $to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true); - } - else { - $to_all_fragment_indices = $cached_array_keys[$from_base_fragment]; - } - // get only indices which falls within current segment - if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) { - $to_fragment_indices = array(); - foreach ( $to_all_fragment_indices as $to_fragment_index ) { - if ( $to_fragment_index < $to_segment_start ) { continue; } - if ( $to_fragment_index >= $to_segment_end ) { break; } - $to_fragment_indices[] = $to_fragment_index; - } - $cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices; - } - else { - $to_fragment_indices = $to_all_fragment_indices; - } - } - else { - $to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment]; - } - // iterate through collected indices - foreach ( $to_fragment_indices as $to_base_fragment_index ) { - $fragment_index_offset = $from_base_fragment_length; - // iterate until no more match - for (;;) { - $fragment_from_index = $from_base_fragment_index + $fragment_index_offset; - if ( $fragment_from_index >= $from_segment_end ) { - break; - } - $fragment_to_index = $to_base_fragment_index + $fragment_index_offset; - if ( $fragment_to_index >= $to_segment_end ) { - break; - } - if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) { - break; - } - $fragment_length = strlen($from_fragments[$fragment_from_index]); - $fragment_index_offset += $fragment_length; - } - if ( $fragment_index_offset > $best_copy_length ) { - $best_copy_length = $fragment_index_offset; - $best_from_start = $from_base_fragment_index; - $best_to_start = $to_base_fragment_index; - } - } - $from_base_fragment_index += strlen($from_base_fragment); - // If match is larger than half segment size, no point trying to find better - // TODO: Really? - if ( $best_copy_length >= $from_segment_length / 2) { - break; - } - // no point to keep looking if what is left is less than - // current best match - if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) { - break; - } - } - - if ( $best_copy_length ) { - $jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start); - $result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length); - $jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end); - } - else { - $result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length)); - } - } - - ksort($result, SORT_NUMERIC); - return array_values($result); - } - - /** - * Perform a character-level diff. - * - * The algorithm is quite similar to doFragmentDiff(), except that - * the code path is optimized for character-level diff -- strpos() is - * used to find out the longest common subequence of characters. - * - * We try to find a match using the longest possible subsequence, which - * is at most the length of the shortest of the two strings, then incrementally - * reduce the size until a match is found. - * - * I still need to study more the performance of this function. It - * appears that for long strings, the generic doFragmentDiff() is more - * performant. For word-sized strings, doCharDiff() is somewhat more - * performant. - */ - private static function doCharDiff($from_text, $to_text) { - $result = array(); - $jobs = array(array(0, strlen($from_text), 0, strlen($to_text))); - while ( $job = array_pop($jobs) ) { - // get the segments which must be diff'ed - list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job; - $from_segment_len = $from_segment_end - $from_segment_start; - $to_segment_len = $to_segment_end - $to_segment_start; - - // catch easy cases first - if ( !$from_segment_len || !$to_segment_len ) { - if ( $from_segment_len ) { - $result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len); - } - else if ( $to_segment_len ) { - $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len)); - } - continue; - } - if ( $from_segment_len >= $to_segment_len ) { - $copy_len = $to_segment_len; - while ( $copy_len ) { - $to_copy_start = $to_segment_start; - $to_copy_start_max = $to_segment_end - $copy_len; - while ( $to_copy_start <= $to_copy_start_max ) { - $from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len)); - if ( $from_copy_start !== false ) { - $from_copy_start += $from_segment_start; - break 2; - } - $to_copy_start++; - } - $copy_len--; - } - } - else { - $copy_len = $from_segment_len; - while ( $copy_len ) { - $from_copy_start = $from_segment_start; - $from_copy_start_max = $from_segment_end - $copy_len; - while ( $from_copy_start <= $from_copy_start_max ) { - $to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len)); - if ( $to_copy_start !== false ) { - $to_copy_start += $to_segment_start; - break 2; - } - $from_copy_start++; - } - $copy_len--; - } - } - // match found - if ( $copy_len ) { - $jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start); - $result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len); - $jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end); - } - // no match, so delete all, insert all - else { - $result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len)); - } - } - ksort($result, SORT_NUMERIC); - return array_values($result); - } - - /** - * Efficiently fragment the text into an array according to - * specified delimiters. - * No delimiters means fragment into single character. - * The array indices are the offset of the fragments into - * the input string. - * A sentinel empty fragment is always added at the end. - * Careful: No check is performed as to the validity of the - * delimiters. - */ - private static function extractFragments($text, $delimiters) { - // special case: split into characters - if ( empty($delimiters) ) { - $chars = str_split($text, 1); - $chars[strlen($text)] = ''; - return $chars; - } - $fragments = array(); - $start = $end = 0; - for (;;) { - $end += strcspn($text, $delimiters, $end); - $end += strspn($text, $delimiters, $end); - if ( $end === $start ) { - break; - } - $fragments[$start] = substr($text, $start, $end - $start); - $start = $end; - } - $fragments[$start] = ''; - return $fragments; - } - - /** - * Stock opcode renderers - */ - private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) { - if ( $opcode === 'c' || $opcode === 'i' ) { - echo substr($from, $from_offset, $from_len); - } - } - - private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) { - if ( $opcode === 'c' ) { - echo htmlentities(substr($from, $from_offset, $from_len)); - } - else if ( $opcode === 'd' ) { - $deletion = substr($from, $from_offset, $from_len); - if ( strcspn($deletion, " \n\r") === 0 ) { - $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion); - } - echo '', htmlentities($deletion), ''; - } - else /* if ( $opcode === 'i' ) */ { - echo '', htmlentities(substr($from, $from_offset, $from_len)), ''; - } - } - } - diff --git a/library/vendor/php-diff/README.md b/library/vendor/php-diff/README.md new file mode 100644 index 0000000..0110b5c --- /dev/null +++ b/library/vendor/php-diff/README.md @@ -0,0 +1,65 @@ +PHP Diff Class +-------------- + +Introduction +------------ +A comprehensive library for generating differences between +two hashable objects (strings or arrays). Generated differences can be +rendered in all of the standard formats including: + * Unified + * Context + * Inline HTML + * Side by Side HTML + +The logic behind the core of the diff engine (ie, the sequence matcher) +is primarily based on the Python difflib package. The reason for doing +so is primarily because of its high degree of accuracy. + +Example Use +----------- +A quick usage example can be found in the example/ directory and under +example.php. + +More complete documentation will be available shortly. + +Merge files using jQuery +------------------------ +Xiphe has build a jQuery plugin with that you can merge the compared +files. Have a look at [jQuery-Merge-for-php-diff](https://github.com/Xiphe/jQuery-Merge-for-php-diff). + +Todo +---- + * Ability to ignore blank line changes + * 3 way diff support + * Performance optimizations + +License (BSD License) +--------------------- +Copyright (c) 2009 Chris Boulton +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + - Neither the name of the Chris Boulton nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +``` +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +``` diff --git a/library/vendor/php-diff/SOURCE b/library/vendor/php-diff/SOURCE new file mode 100644 index 0000000..c083b23 --- /dev/null +++ b/library/vendor/php-diff/SOURCE @@ -0,0 +1,9 @@ +git clone https://github.com/chrisboulton/php-diff.git +# Last used commit: +cd php-diff +git checkout f4db229d7ae8dffa0a4f90e1adbec9bf22c93d99 +rm -rf .git +rm -rf .gitignore +rm -rf composer.json +rm -rf example +cd .. diff --git a/library/vendor/php-diff/lib/Diff.php b/library/vendor/php-diff/lib/Diff.php new file mode 100644 index 0000000..d1eb9da --- /dev/null +++ b/library/vendor/php-diff/lib/Diff.php @@ -0,0 +1,179 @@ + + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package Diff + * @author Chris Boulton + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +class Diff +{ + /** + * @var array The "old" sequence to use as the basis for the comparison. + */ + private $a = null; + + /** + * @var array The "new" sequence to generate the changes for. + */ + private $b = null; + + /** + * @var array Array containing the generated opcodes for the differences between the two items. + */ + private $groupedCodes = null; + + /** + * @var array Associative array of the default options available for the diff class and their default value. + */ + private $defaultOptions = array( + 'context' => 3, + 'ignoreNewLines' => false, + 'ignoreWhitespace' => false, + 'ignoreCase' => false + ); + + /** + * @var array Array of the options that have been applied for generating the diff. + */ + private $options = array(); + + /** + * The constructor. + * + * @param array $a Array containing the lines of the first string to compare. + * @param array $b Array containing the lines for the second string to compare. + */ + public function __construct($a, $b, $options=array()) + { + $this->a = $a; + $this->b = $b; + + if (is_array($options)) + $this->options = array_merge($this->defaultOptions, $options); + else + $this->options = $this->defaultOptions; + } + + /** + * Render a diff using the supplied rendering class and return it. + * + * @param object $renderer An instance of the rendering object to use for generating the diff. + * @return mixed The generated diff. Exact return value depends on the rendered. + */ + public function render(Diff_Renderer_Abstract $renderer) + { + $renderer->diff = $this; + return $renderer->render(); + } + + /** + * Get a range of lines from $start to $end from the first comparison string + * and return them as an array. If no values are supplied, the entire string + * is returned. It's also possible to specify just one line to return only + * that line. + * + * @param int $start The starting number. + * @param int $end The ending number. If not supplied, only the item in $start will be returned. + * @return array Array of all of the lines between the specified range. + */ + public function getA($start=0, $end=null) + { + if($start == 0 && $end === null) { + return $this->a; + } + + if($end === null) { + $length = 1; + } + else { + $length = $end - $start; + } + + return array_slice($this->a, $start, $length); + + } + + /** + * Get a range of lines from $start to $end from the second comparison string + * and return them as an array. If no values are supplied, the entire string + * is returned. It's also possible to specify just one line to return only + * that line. + * + * @param int $start The starting number. + * @param int $end The ending number. If not supplied, only the item in $start will be returned. + * @return array Array of all of the lines between the specified range. + */ + public function getB($start=0, $end=null) + { + if($start == 0 && $end === null) { + return $this->b; + } + + if($end === null) { + $length = 1; + } + else { + $length = $end - $start; + } + + return array_slice($this->b, $start, $length); + } + + /** + * Generate a list of the compiled and grouped opcodes for the differences between the + * two strings. Generally called by the renderer, this class instantiates the sequence + * matcher and performs the actual diff generation and return an array of the opcodes + * for it. Once generated, the results are cached in the diff class instance. + * + * @return array Array of the grouped opcodes for the generated diff. + */ + public function getGroupedOpcodes() + { + if(!is_null($this->groupedCodes)) { + return $this->groupedCodes; + } + + require_once dirname(__FILE__).'/Diff/SequenceMatcher.php'; + $sequenceMatcher = new Diff_SequenceMatcher($this->a, $this->b, null, $this->options); + $this->groupedCodes = $sequenceMatcher->getGroupedOpcodes($this->options['context']); + return $this->groupedCodes; + } +} \ No newline at end of file diff --git a/library/vendor/php-diff/lib/Diff/Renderer/Abstract.php b/library/vendor/php-diff/lib/Diff/Renderer/Abstract.php new file mode 100644 index 0000000..f63c3e7 --- /dev/null +++ b/library/vendor/php-diff/lib/Diff/Renderer/Abstract.php @@ -0,0 +1,82 @@ + + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +abstract class Diff_Renderer_Abstract +{ + /** + * @var object Instance of the diff class that this renderer is generating the rendered diff for. + */ + public $diff; + + /** + * @var array Array of the default options that apply to this renderer. + */ + protected $defaultOptions = array(); + + /** + * @var array Array containing the user applied and merged default options for the renderer. + */ + protected $options = array(); + + /** + * The constructor. Instantiates the rendering engine and if options are passed, + * sets the options for the renderer. + * + * @param array $options Optionally, an array of the options for the renderer. + */ + public function __construct(array $options = array()) + { + $this->setOptions($options); + } + + /** + * Set the options of the renderer to those supplied in the passed in array. + * Options are merged with the default to ensure that there aren't any missing + * options. + * + * @param array $options Array of options to set. + */ + public function setOptions(array $options) + { + $this->options = array_merge($this->defaultOptions, $options); + } +} \ No newline at end of file diff --git a/library/vendor/php-diff/lib/Diff/Renderer/Html/Array.php b/library/vendor/php-diff/lib/Diff/Renderer/Html/Array.php new file mode 100644 index 0000000..76a314d --- /dev/null +++ b/library/vendor/php-diff/lib/Diff/Renderer/Html/Array.php @@ -0,0 +1,231 @@ + + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/../Abstract.php'; + +class Diff_Renderer_Html_Array extends Diff_Renderer_Abstract +{ + /** + * @var array Array of the default options that apply to this renderer. + */ + protected $defaultOptions = array( + 'tabSize' => 4 + ); + + /** + * Render and return an array structure suitable for generating HTML + * based differences. Generally called by subclasses that generate a + * HTML based diff and return an array of the changes to show in the diff. + * + * @return array An array of the generated chances, suitable for presentation in HTML. + */ + public function render() + { + // As we'll be modifying a & b to include our change markers, + // we need to get the contents and store them here. That way + // we're not going to destroy the original data + $a = $this->diff->getA(); + $b = $this->diff->getB(); + + $changes = array(); + $opCodes = $this->diff->getGroupedOpcodes(); + foreach($opCodes as $group) { + $blocks = array(); + $lastTag = null; + $lastBlock = 0; + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + + if($tag == 'replace' && $i2 - $i1 == $j2 - $j1) { + for($i = 0; $i < ($i2 - $i1); ++$i) { + $fromLine = $a[$i1 + $i]; + $toLine = $b[$j1 + $i]; + + list($start, $end) = $this->getChangeExtent($fromLine, $toLine); + if($start != 0 || $end != 0) { + $last = $end + strlen($fromLine); + $fromLine = substr_replace($fromLine, "\0", $start, 0); + $fromLine = substr_replace($fromLine, "\1", $last + 1, 0); + $last = $end + strlen($toLine); + $toLine = substr_replace($toLine, "\0", $start, 0); + $toLine = substr_replace($toLine, "\1", $last + 1, 0); + $a[$i1 + $i] = $fromLine; + $b[$j1 + $i] = $toLine; + } + } + } + + if($tag != $lastTag) { + $blocks[] = array( + 'tag' => $tag, + 'base' => array( + 'offset' => $i1, + 'lines' => array() + ), + 'changed' => array( + 'offset' => $j1, + 'lines' => array() + ) + ); + $lastBlock = count($blocks)-1; + } + + $lastTag = $tag; + + if($tag == 'equal') { + $lines = array_slice($a, $i1, ($i2 - $i1)); + $blocks[$lastBlock]['base']['lines'] += $this->formatLines($lines); + $lines = array_slice($b, $j1, ($j2 - $j1)); + $blocks[$lastBlock]['changed']['lines'] += $this->formatLines($lines); + } + else { + if($tag == 'replace' || $tag == 'delete') { + $lines = array_slice($a, $i1, ($i2 - $i1)); + $lines = $this->formatLines($lines); + $lines = str_replace(array("\0", "\1"), array('', ''), $lines); + $blocks[$lastBlock]['base']['lines'] += $lines; + } + + if($tag == 'replace' || $tag == 'insert') { + $lines = array_slice($b, $j1, ($j2 - $j1)); + $lines = $this->formatLines($lines); + $lines = str_replace(array("\0", "\1"), array('', ''), $lines); + $blocks[$lastBlock]['changed']['lines'] += $lines; + } + } + } + $changes[] = $blocks; + } + return $changes; + } + + /** + * Given two strings, determine where the changes in the two strings + * begin, and where the changes in the two strings end. + * + * @param string $fromLine The first string. + * @param string $toLine The second string. + * @return array Array containing the starting position (0 by default) and the ending position (-1 by default) + */ + private function getChangeExtent($fromLine, $toLine) + { + $start = 0; + $limit = min(strlen($fromLine), strlen($toLine)); + while($start < $limit && $fromLine{$start} == $toLine{$start}) { + ++$start; + } + $end = -1; + $limit = $limit - $start; + while(-$end <= $limit && substr($fromLine, $end, 1) == substr($toLine, $end, 1)) { + --$end; + } + return array( + $start, + $end + 1 + ); + } + + /** + * Format a series of lines suitable for output in a HTML rendered diff. + * This involves replacing tab characters with spaces, making the HTML safe + * for output, ensuring that double spaces are replaced with   etc. + * + * @param array $lines Array of lines to format. + * @return array Array of the formatted lines. + */ + protected function formatLines($lines) + { + $lines = array_map(array($this, 'ExpandTabs'), $lines); + $lines = array_map(array($this, 'HtmlSafe'), $lines); + $callback = array($this, 'fixMatchedSpaces'); + foreach($lines as &$line) { + // $line = preg_replace('# ( +)|^ #e', "\$this->fixSpaces('\\1')", $line); + $line = preg_replace_callback('# ( +)|^ #', $callback, $line); + } + return $lines; + } + + protected function fixMatchedSpaces($m) + { + return $this->fixSpaces($m[1]); + } + + /** + * Replace a string containing spaces with a HTML representation using  . + * + * @param string $spaces The string of spaces. + * @return string The HTML representation of the string. + */ + function fixSpaces($spaces='') + { + $count = strlen($spaces); + if($count == 0) { + return ''; + } + + $div = floor($count / 2); + $mod = $count % 2; + return str_repeat('  ', $div).str_repeat(' ', $mod); + } + + /** + * Replace tabs in a single line with a number of spaces as defined by the tabSize option. + * + * @param string $line The containing tabs to convert. + * @return string The line with the tabs converted to spaces. + */ + private function expandTabs($line) + { + return str_replace("\t", str_repeat(' ', $this->options['tabSize']), $line); + } + + /** + * Make a string containing HTML safe for output on a page. + * + * @param string $string The string. + * @return string The string with the HTML characters replaced by entities. + */ + private function htmlSafe($string) + { + return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8'); + } +} diff --git a/library/vendor/php-diff/lib/Diff/Renderer/Html/Inline.php b/library/vendor/php-diff/lib/Diff/Renderer/Html/Inline.php new file mode 100644 index 0000000..60e8005 --- /dev/null +++ b/library/vendor/php-diff/lib/Diff/Renderer/Html/Inline.php @@ -0,0 +1,143 @@ + + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/Array.php'; + +class Diff_Renderer_Html_Inline extends Diff_Renderer_Html_Array +{ + /** + * Render a and return diff with changes between the two sequences + * displayed inline (under each other) + * + * @return string The generated inline diff. + */ + public function render() + { + $changes = parent::render(); + $html = ''; + if(empty($changes)) { + return $html; + } + + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + foreach($changes as $i => $blocks) { + // If this is a separate block, we're condensing code so output ..., + // indicating a significant portion of the code has been collapsed as + // it is the same + if($i > 0) { + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + + foreach($blocks as $change) { + $html .= ''; + // Equal changes should be shown on both sides of the diff + if($change['tag'] == 'equal') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + } + // Added lines only on the right side + else if($change['tag'] == 'insert') { + foreach($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + } + // Show deleted lines only on the left side + else if($change['tag'] == 'delete') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + } + // Show modified lines on both sides + else if($change['tag'] == 'replace') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + + foreach($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + } + $html .= ''; + } + } + $html .= '
OldNewDifferences
 
'.$fromLine.''.$toLine.''.$line.'
 '.$toLine.''.$line.' 
'.$fromLine.' '.$line.' 
'.$fromLine.' '.$line.'
'.$toLine.' '.$line.'
'; + return $html; + } +} \ No newline at end of file diff --git a/library/vendor/php-diff/lib/Diff/Renderer/Html/SideBySide.php b/library/vendor/php-diff/lib/Diff/Renderer/Html/SideBySide.php new file mode 100644 index 0000000..307af1c --- /dev/null +++ b/library/vendor/php-diff/lib/Diff/Renderer/Html/SideBySide.php @@ -0,0 +1,163 @@ + + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/Array.php'; + +class Diff_Renderer_Html_SideBySide extends Diff_Renderer_Html_Array +{ + /** + * Render a and return diff with changes between the two sequences + * displayed side by side. + * + * @return string The generated side by side diff. + */ + public function render() + { + $changes = parent::render(); + + $html = ''; + if(empty($changes)) { + return $html; + } + + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + foreach($changes as $i => $blocks) { + if($i > 0) { + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + + foreach($blocks as $change) { + $html .= ''; + // Equal changes should be shown on both sides of the diff + if($change['tag'] == 'equal') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + } + // Added lines only on the right side + else if($change['tag'] == 'insert') { + foreach($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + } + // Show deleted lines only on the left side + else if($change['tag'] == 'delete') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + } + // Show modified lines on both sides + else if($change['tag'] == 'replace') { + if(count($change['base']['lines']) >= count($change['changed']['lines'])) { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + if(!isset($change['changed']['lines'][$no])) { + $toLine = ' '; + $changedLine = ' '; + } + else { + $toLine = $change['base']['offset'] + $no + 1; + $changedLine = ''.$change['changed']['lines'][$no].''; + } + $html .= ''; + $html .= ''; + $html .= ''; + } + } + else { + foreach($change['changed']['lines'] as $no => $changedLine) { + if(!isset($change['base']['lines'][$no])) { + $fromLine = ' '; + $line = ' '; + } + else { + $fromLine = $change['base']['offset'] + $no + 1; + $line = ''.$change['base']['lines'][$no].''; + } + $html .= ''; + $html .= ''; + $html .= ''; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + } + } + } + $html .= ''; + } + } + $html .= '
Old VersionNew Version
  
'.$fromLine.''.$line.' '.$toLine.''.$line.' 
  '.$toLine.''.$line.' 
'.$fromLine.''.$line.'   
'.$fromLine.''.$line.' '.$toLine.''.$changedLine.'
'.$fromLine.''.$line.' '.$toLine.''.$changedLine.'
'; + return $html; + } +} \ No newline at end of file diff --git a/library/vendor/php-diff/lib/Diff/Renderer/Text/Context.php b/library/vendor/php-diff/lib/Diff/Renderer/Text/Context.php new file mode 100644 index 0000000..1200b01 --- /dev/null +++ b/library/vendor/php-diff/lib/Diff/Renderer/Text/Context.php @@ -0,0 +1,128 @@ + + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/../Abstract.php'; + +class Diff_Renderer_Text_Context extends Diff_Renderer_Abstract +{ + /** + * @var array Array of the different opcode tags and how they map to the context diff equivalent. + */ + private $tagMap = array( + 'insert' => '+', + 'delete' => '-', + 'replace' => '!', + 'equal' => ' ' + ); + + /** + * Render and return a context formatted (old school!) diff file. + * + * @return string The generated context diff. + */ + public function render() + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpcodes(); + foreach($opCodes as $group) { + $diff .= "***************\n"; + $lastItem = count($group)-1; + $i1 = $group[0][1]; + $i2 = $group[$lastItem][2]; + $j1 = $group[0][3]; + $j2 = $group[$lastItem][4]; + + if($i2 - $i1 >= 2) { + $diff .= '*** '.($group[0][1] + 1).','.$i2." ****\n"; + } + else { + $diff .= '*** '.$i2." ****\n"; + } + + if($j2 - $j1 >= 2) { + $separator = '--- '.($j1 + 1).','.$j2." ----\n"; + } + else { + $separator = '--- '.$j2." ----\n"; + } + + $hasVisible = false; + foreach($group as $code) { + if($code[0] == 'replace' || $code[0] == 'delete') { + $hasVisible = true; + break; + } + } + + if($hasVisible) { + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'insert') { + continue; + } + $diff .= $this->tagMap[$tag].' '.implode("\n".$this->tagMap[$tag].' ', $this->diff->GetA($i1, $i2))."\n"; + } + } + + $hasVisible = false; + foreach($group as $code) { + if($code[0] == 'replace' || $code[0] == 'insert') { + $hasVisible = true; + break; + } + } + + $diff .= $separator; + + if($hasVisible) { + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'delete') { + continue; + } + $diff .= $this->tagMap[$tag].' '.implode("\n".$this->tagMap[$tag].' ', $this->diff->GetB($j1, $j2))."\n"; + } + } + } + return $diff; + } +} \ No newline at end of file diff --git a/library/vendor/php-diff/lib/Diff/Renderer/Text/Unified.php b/library/vendor/php-diff/lib/Diff/Renderer/Text/Unified.php new file mode 100644 index 0000000..e94d951 --- /dev/null +++ b/library/vendor/php-diff/lib/Diff/Renderer/Text/Unified.php @@ -0,0 +1,87 @@ + + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/../Abstract.php'; + +class Diff_Renderer_Text_Unified extends Diff_Renderer_Abstract +{ + /** + * Render and return a unified diff. + * + * @return string The unified diff. + */ + public function render() + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpcodes(); + foreach($opCodes as $group) { + $lastItem = count($group)-1; + $i1 = $group[0][1]; + $i2 = $group[$lastItem][2]; + $j1 = $group[0][3]; + $j2 = $group[$lastItem][4]; + + if($i1 == 0 && $i2 == 0) { + $i1 = -1; + $i2 = -1; + } + + $diff .= '@@ -'.($i1 + 1).','.($i2 - $i1).' +'.($j1 + 1).','.($j2 - $j1)." @@\n"; + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'equal') { + $diff .= ' '.implode("\n ", $this->diff->GetA($i1, $i2))."\n"; + } + else { + if($tag == 'replace' || $tag == 'delete') { + $diff .= '-'.implode("\n-", $this->diff->GetA($i1, $i2))."\n"; + } + + if($tag == 'replace' || $tag == 'insert') { + $diff .= '+'.implode("\n+", $this->diff->GetB($j1, $j2))."\n"; + } + } + } + } + return $diff; + } +} \ No newline at end of file diff --git a/library/vendor/php-diff/lib/Diff/SequenceMatcher.php b/library/vendor/php-diff/lib/Diff/SequenceMatcher.php new file mode 100644 index 0000000..e819e81 --- /dev/null +++ b/library/vendor/php-diff/lib/Diff/SequenceMatcher.php @@ -0,0 +1,742 @@ + + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package Diff + * @author Chris Boulton + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +class Diff_SequenceMatcher +{ + /** + * @var string|array Either a string or an array containing a callback function to determine if a line is "junk" or not. + */ + private $junkCallback = null; + + /** + * @var array The first sequence to compare against. + */ + private $a = null; + + /** + * @var array The second sequence. + */ + private $b = null; + + /** + * @var array Array of characters that are considered junk from the second sequence. Characters are the array key. + */ + private $junkDict = array(); + + /** + * @var array Array of indices that do not contain junk elements. + */ + private $b2j = array(); + + private $options = array(); + + private $defaultOptions = array( + 'ignoreNewLines' => false, + 'ignoreWhitespace' => false, + 'ignoreCase' => false + ); + + /** + * The constructor. With the sequences being passed, they'll be set for the + * sequence matcher and it will perform a basic cleanup & calculate junk + * elements. + * + * @param string|array $a A string or array containing the lines to compare against. + * @param string|array $b A string or array containing the lines to compare. + * @param string|array $junkCallback Either an array or string that references a callback function (if there is one) to determine 'junk' characters. + */ + public function __construct($a, $b, $junkCallback=null, $options) + { + $this->a = null; + $this->b = null; + $this->junkCallback = $junkCallback; + $this->setOptions($options); + $this->setSequences($a, $b); + } + + public function setOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + } + + /** + * Set the first and second sequences to use with the sequence matcher. + * + * @param string|array $a A string or array containing the lines to compare against. + * @param string|array $b A string or array containing the lines to compare. + */ + public function setSequences($a, $b) + { + $this->setSeq1($a); + $this->setSeq2($b); + } + + /** + * Set the first sequence ($a) and reset any internal caches to indicate that + * when calling the calculation methods, we need to recalculate them. + * + * @param string|array $a The sequence to set as the first sequence. + */ + public function setSeq1($a) + { + if(!is_array($a)) { + $a = str_split($a); + } + if($a == $this->a) { + return; + } + + $this->a= $a; + $this->matchingBlocks = null; + $this->opCodes = null; + } + + /** + * Set the second sequence ($b) and reset any internal caches to indicate that + * when calling the calculation methods, we need to recalculate them. + * + * @param string|array $b The sequence to set as the second sequence. + */ + public function setSeq2($b) + { + if(!is_array($b)) { + $b = str_split($b); + } + if($b == $this->b) { + return; + } + + $this->b = $b; + $this->matchingBlocks = null; + $this->opCodes = null; + $this->fullBCount = null; + $this->chainB(); + } + + /** + * Generate the internal arrays containing the list of junk and non-junk + * characters for the second ($b) sequence. + */ + private function chainB() + { + $length = count ($this->b); + $this->b2j = array(); + $popularDict = array(); + + for($i = 0; $i < $length; ++$i) { + $char = $this->b[$i]; + if(isset($this->b2j[$char])) { + if($length >= 200 && count($this->b2j[$char]) * 100 > $length) { + $popularDict[$char] = 1; + unset($this->b2j[$char]); + } + else { + $this->b2j[$char][] = $i; + } + } + else { + $this->b2j[$char] = array( + $i + ); + } + } + + // Remove leftovers + foreach(array_keys($popularDict) as $char) { + unset($this->b2j[$char]); + } + + $this->junkDict = array(); + if(is_callable($this->junkCallback)) { + foreach(array_keys($popularDict) as $char) { + if(call_user_func($this->junkCallback, $char)) { + $this->junkDict[$char] = 1; + unset($popularDict[$char]); + } + } + + foreach(array_keys($this->b2j) as $char) { + if(call_user_func($this->junkCallback, $char)) { + $this->junkDict[$char] = 1; + unset($this->b2j[$char]); + } + } + } + } + + /** + * Checks if a particular character is in the junk dictionary + * for the list of junk characters. + * + * @return boolean $b True if the character is considered junk. False if not. + */ + private function isBJunk($b) + { + if(isset($this->juncDict[$b])) { + return true; + } + + return false; + } + + /** + * Find the longest matching block in the two sequences, as defined by the + * lower and upper constraints for each sequence. (for the first sequence, + * $alo - $ahi and for the second sequence, $blo - $bhi) + * + * Essentially, of all of the maximal matching blocks, return the one that + * startest earliest in $a, and all of those maximal matching blocks that + * start earliest in $a, return the one that starts earliest in $b. + * + * If the junk callback is defined, do the above but with the restriction + * that the junk element appears in the block. Extend it as far as possible + * by matching only junk elements in both $a and $b. + * + * @param int $alo The lower constraint for the first sequence. + * @param int $ahi The upper constraint for the first sequence. + * @param int $blo The lower constraint for the second sequence. + * @param int $bhi The upper constraint for the second sequence. + * @return array Array containing the longest match that includes the starting position in $a, start in $b and the length/size. + */ + public function findLongestMatch($alo, $ahi, $blo, $bhi) + { + $a = $this->a; + $b = $this->b; + + $bestI = $alo; + $bestJ = $blo; + $bestSize = 0; + + $j2Len = array(); + $nothing = array(); + + for($i = $alo; $i < $ahi; ++$i) { + $newJ2Len = array(); + $jDict = $this->arrayGetDefault($this->b2j, $a[$i], $nothing); + foreach($jDict as $jKey => $j) { + if($j < $blo) { + continue; + } + else if($j >= $bhi) { + break; + } + + $k = $this->arrayGetDefault($j2Len, $j -1, 0) + 1; + $newJ2Len[$j] = $k; + if($k > $bestSize) { + $bestI = $i - $k + 1; + $bestJ = $j - $k + 1; + $bestSize = $k; + } + } + + $j2Len = $newJ2Len; + } + + while($bestI > $alo && $bestJ > $blo && !$this->isBJunk($b[$bestJ - 1]) && + !$this->linesAreDifferent($bestI - 1, $bestJ - 1)) { + --$bestI; + --$bestJ; + ++$bestSize; + } + + while($bestI + $bestSize < $ahi && ($bestJ + $bestSize) < $bhi && + !$this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) { + ++$bestSize; + } + + while($bestI > $alo && $bestJ > $blo && $this->isBJunk($b[$bestJ - 1]) && + !$this->isLineDifferent($bestI - 1, $bestJ - 1)) { + --$bestI; + --$bestJ; + ++$bestSize; + } + + while($bestI + $bestSize < $ahi && $bestJ + $bestSize < $bhi && + $this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) { + ++$bestSize; + } + + return array( + $bestI, + $bestJ, + $bestSize + ); + } + + /** + * Check if the two lines at the given indexes are different or not. + * + * @param int $aIndex Line number to check against in a. + * @param int $bIndex Line number to check against in b. + * @return boolean True if the lines are different and false if not. + */ + public function linesAreDifferent($aIndex, $bIndex) + { + $lineA = $this->a[$aIndex]; + $lineB = $this->b[$bIndex]; + + if($this->options['ignoreWhitespace']) { + $replace = array("\t", ' '); + $lineA = str_replace($replace, '', $lineA); + $lineB = str_replace($replace, '', $lineB); + } + + if($this->options['ignoreCase']) { + $lineA = strtolower($lineA); + $lineB = strtolower($lineB); + } + + if($lineA != $lineB) { + return true; + } + + return false; + } + + /** + * Return a nested set of arrays for all of the matching sub-sequences + * in the strings $a and $b. + * + * Each block contains the lower constraint of the block in $a, the lower + * constraint of the block in $b and finally the number of lines that the + * block continues for. + * + * @return array Nested array of the matching blocks, as described by the function. + */ + public function getMatchingBlocks() + { + if(!empty($this->matchingBlocks)) { + return $this->matchingBlocks; + } + + $aLength = count($this->a); + $bLength = count($this->b); + + $queue = array( + array( + 0, + $aLength, + 0, + $bLength + ) + ); + + $matchingBlocks = array(); + while(!empty($queue)) { + list($alo, $ahi, $blo, $bhi) = array_pop($queue); + $x = $this->findLongestMatch($alo, $ahi, $blo, $bhi); + list($i, $j, $k) = $x; + if($k) { + $matchingBlocks[] = $x; + if($alo < $i && $blo < $j) { + $queue[] = array( + $alo, + $i, + $blo, + $j + ); + } + + if($i + $k < $ahi && $j + $k < $bhi) { + $queue[] = array( + $i + $k, + $ahi, + $j + $k, + $bhi + ); + } + } + } + + usort($matchingBlocks, array($this, 'tupleSort')); + + $i1 = 0; + $j1 = 0; + $k1 = 0; + $nonAdjacent = array(); + foreach($matchingBlocks as $block) { + list($i2, $j2, $k2) = $block; + if($i1 + $k1 == $i2 && $j1 + $k1 == $j2) { + $k1 += $k2; + } + else { + if($k1) { + $nonAdjacent[] = array( + $i1, + $j1, + $k1 + ); + } + + $i1 = $i2; + $j1 = $j2; + $k1 = $k2; + } + } + + if($k1) { + $nonAdjacent[] = array( + $i1, + $j1, + $k1 + ); + } + + $nonAdjacent[] = array( + $aLength, + $bLength, + 0 + ); + + $this->matchingBlocks = $nonAdjacent; + return $this->matchingBlocks; + } + + /** + * Return a list of all of the opcodes for the differences between the + * two strings. + * + * The nested array returned contains an array describing the opcode + * which includes: + * 0 - The type of tag (as described below) for the opcode. + * 1 - The beginning line in the first sequence. + * 2 - The end line in the first sequence. + * 3 - The beginning line in the second sequence. + * 4 - The end line in the second sequence. + * + * The different types of tags include: + * replace - The string from $i1 to $i2 in $a should be replaced by + * the string in $b from $j1 to $j2. + * delete - The string in $a from $i1 to $j2 should be deleted. + * insert - The string in $b from $j1 to $j2 should be inserted at + * $i1 in $a. + * equal - The two strings with the specified ranges are equal. + * + * @return array Array of the opcodes describing the differences between the strings. + */ + public function getOpCodes() + { + if(!empty($this->opCodes)) { + return $this->opCodes; + } + + $i = 0; + $j = 0; + $this->opCodes = array(); + + $blocks = $this->getMatchingBlocks(); + foreach($blocks as $block) { + list($ai, $bj, $size) = $block; + $tag = ''; + if($i < $ai && $j < $bj) { + $tag = 'replace'; + } + else if($i < $ai) { + $tag = 'delete'; + } + else if($j < $bj) { + $tag = 'insert'; + } + + if($tag) { + $this->opCodes[] = array( + $tag, + $i, + $ai, + $j, + $bj + ); + } + + $i = $ai + $size; + $j = $bj + $size; + + if($size) { + $this->opCodes[] = array( + 'equal', + $ai, + $i, + $bj, + $j + ); + } + } + return $this->opCodes; + } + + /** + * Return a series of nested arrays containing different groups of generated + * opcodes for the differences between the strings with up to $context lines + * of surrounding content. + * + * Essentially what happens here is any big equal blocks of strings are stripped + * out, the smaller subsets of changes are then arranged in to their groups. + * This means that the sequence matcher and diffs do not need to include the full + * content of the different files but can still provide context as to where the + * changes are. + * + * @param int $context The number of lines of context to provide around the groups. + * @return array Nested array of all of the grouped opcodes. + */ + public function getGroupedOpcodes($context=3) + { + $opCodes = $this->getOpCodes(); + if(empty($opCodes)) { + $opCodes = array( + array( + 'equal', + 0, + 1, + 0, + 1 + ) + ); + } + + if($opCodes[0][0] == 'equal') { + $opCodes[0] = array( + $opCodes[0][0], + max($opCodes[0][1], $opCodes[0][2] - $context), + $opCodes[0][2], + max($opCodes[0][3], $opCodes[0][4] - $context), + $opCodes[0][4] + ); + } + + $lastItem = count($opCodes) - 1; + if($opCodes[$lastItem][0] == 'equal') { + list($tag, $i1, $i2, $j1, $j2) = $opCodes[$lastItem]; + $opCodes[$lastItem] = array( + $tag, + $i1, + min($i2, $i1 + $context), + $j1, + min($j2, $j1 + $context) + ); + } + + $maxRange = $context * 2; + $groups = array(); + $group = array(); + foreach($opCodes as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'equal' && $i2 - $i1 > $maxRange) { + $group[] = array( + $tag, + $i1, + min($i2, $i1 + $context), + $j1, + min($j2, $j1 + $context) + ); + $groups[] = $group; + $group = array(); + $i1 = max($i1, $i2 - $context); + $j1 = max($j1, $j2 - $context); + } + $group[] = array( + $tag, + $i1, + $i2, + $j1, + $j2 + ); + } + + if(!empty($group) && !(count($group) == 1 && $group[0][0] == 'equal')) { + $groups[] = $group; + } + + return $groups; + } + + /** + * Return a measure of the similarity between the two sequences. + * This will be a float value between 0 and 1. + * + * Out of all of the ratio calculation functions, this is the most + * expensive to call if getMatchingBlocks or getOpCodes is yet to be + * called. The other calculation methods (quickRatio and realquickRatio) + * can be used to perform quicker calculations but may be less accurate. + * + * The ratio is calculated as (2 * number of matches) / total number of + * elements in both sequences. + * + * @return float The calculated ratio. + */ + public function Ratio() + { + $matches = array_reduce($this->getMatchingBlocks(), array($this, 'ratioReduce'), 0); + return $this->calculateRatio($matches, count ($this->a) + count ($this->b)); + } + + /** + * Helper function to calculate the number of matches for Ratio(). + * + * @param int $sum The running total for the number of matches. + * @param array $triple Array containing the matching block triple to add to the running total. + * @return int The new running total for the number of matches. + */ + private function ratioReduce($sum, $triple) + { + return $sum + ($triple[count($triple) - 1]); + } + + /** + * Quickly return an upper bound ratio for the similarity of the strings. + * This is quicker to compute than Ratio(). + * + * @return float The calculated ratio. + */ + private function quickRatio() + { + if($this->fullBCount === null) { + $this->fullBCount = array(); + $bLength = count ($b); + for($i = 0; $i < $bLength; ++$i) { + $char = $this->b[$i]; + $this->fullBCount[$char] = $this->arrayGetDefault($this->fullBCount, $char, 0) + 1; + } + } + + $avail = array(); + $matches = 0; + $aLength = count ($this->a); + for($i = 0; $i < $aLength; ++$i) { + $char = $this->a[$i]; + if(isset($avail[$char])) { + $numb = $avail[$char]; + } + else { + $numb = $this->arrayGetDefault($this->fullBCount, $char, 0); + } + $avail[$char] = $numb - 1; + if($numb > 0) { + ++$matches; + } + } + + $this->calculateRatio($matches, count ($this->a) + count ($this->b)); + } + + /** + * Return an upper bound ratio really quickly for the similarity of the strings. + * This is quicker to compute than Ratio() and quickRatio(). + * + * @return float The calculated ratio. + */ + private function realquickRatio() + { + $aLength = count ($this->a); + $bLength = count ($this->b); + + return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength); + } + + /** + * Helper function for calculating the ratio to measure similarity for the strings. + * The ratio is defined as being 2 * (number of matches / total length) + * + * @param int $matches The number of matches in the two strings. + * @param int $length The length of the two strings. + * @return float The calculated ratio. + */ + private function calculateRatio($matches, $length=0) + { + if($length) { + return 2 * ($matches / $length); + } + else { + return 1; + } + } + + /** + * Helper function that provides the ability to return the value for a key + * in an array of it exists, or if it doesn't then return a default value. + * Essentially cleaner than doing a series of if(isset()) {} else {} calls. + * + * @param array $array The array to search. + * @param string $key The key to check that exists. + * @param mixed $default The value to return as the default value if the key doesn't exist. + * @return mixed The value from the array if the key exists or otherwise the default. + */ + private function arrayGetDefault($array, $key, $default) + { + if(isset($array[$key])) { + return $array[$key]; + } + else { + return $default; + } + } + + /** + * Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks + * + * @param array $a First array to compare. + * @param array $b Second array to compare. + * @return int -1, 0 or 1, as expected by the usort function. + */ + private function tupleSort($a, $b) + { + $max = max(count($a), count($b)); + for($i = 0; $i < $max; ++$i) { + if($a[$i] < $b[$i]) { + return -1; + } + else if($a[$i] > $b[$i]) { + return 1; + } + } + + if(count($a) == $count($b)) { + return 0; + } + else if(count($a) < count($b)) { + return -1; + } + else { + return 1; + } + } +} \ No newline at end of file diff --git a/packaging/debian/README.1st b/packaging/debian/README.1st new file mode 100644 index 0000000..9e96470 --- /dev/null +++ b/packaging/debian/README.1st @@ -0,0 +1,18 @@ +Building Debian packages +======================== + +This is work in progress, please expect build instructions to change any time +soon. Currently, to build custom Debian or Ubuntu packages, please proceed as +follows: + +```sh +apt-get install --no-install-recommends \ + debhelper devscripts build-essential fakeroot libparse-debcontrol-perl +# Eventually adjust debian/changelog +cp -a packaging/debian debian +dpkg-buildpackage -us -uc +rm -rf debian +``` + +Please move to your parent directory (`cd ..`) to find your new Debian packages. + diff --git a/packaging/debian/changelog b/packaging/debian/changelog new file mode 100644 index 0000000..9051952 --- /dev/null +++ b/packaging/debian/changelog @@ -0,0 +1,6 @@ +icingaweb2-module-businessprocesss (2.0.0-rc1) stable; urgency=low + + * First packaged release + + -- Thomas Gelf Fri, 09 Jan 2016 10:37:31 +0100 + diff --git a/packaging/debian/compat b/packaging/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/packaging/debian/compat @@ -0,0 +1 @@ +9 diff --git a/packaging/debian/control b/packaging/debian/control new file mode 100644 index 0000000..2e3c046 --- /dev/null +++ b/packaging/debian/control @@ -0,0 +1,15 @@ +Source: icingaweb2-module-businessprocesss +Section: admin +Maintainer: Icinga Development Team +Priority: optional +Build-Depends: debhelper (>=9) +Standards-Version: 3.9.4 +Homepage: https://www.icinga.com + +Package: icingaweb2-module-businessprocess +Architecture: all +Depends: icingaweb2-common (>= 2.2.0), php-curl|php5-curl, ${misc:Depends} +Suggests: icingaweb2 +Description: A businessprocess viewer and modeler + Supports legacy BPaddon config files + diff --git a/packaging/debian/docs b/packaging/debian/docs new file mode 100644 index 0000000..3959d9d --- /dev/null +++ b/packaging/debian/docs @@ -0,0 +1 @@ +REAMDE.md diff --git a/packaging/debian/install b/packaging/debian/install new file mode 100644 index 0000000..8f7da70 --- /dev/null +++ b/packaging/debian/install @@ -0,0 +1,10 @@ +application usr/share/icingaweb2/modules/businessprocess +doc usr/share/icingaweb2/modules/businessprocess +library usr/share/icingaweb2/modules/businessprocess +public usr/share/icingaweb2/modules/businessprocess +test usr/share/icingaweb2/modules/businessprocess +run.php usr/share/icingaweb2/modules/businessprocess +configuration.php usr/share/icingaweb2/modules/businessprocess +module.info usr/share/icingaweb2/modules/businessprocess +phpunit.xml usr/share/icingaweb2/modules/businessprocess +README.md usr/share/icingaweb2/modules/businessprocess diff --git a/packaging/debian/rules b/packaging/debian/rules new file mode 100755 index 0000000..615fcf8 --- /dev/null +++ b/packaging/debian/rules @@ -0,0 +1,26 @@ +#!/usr/bin/make -f +#export DH_VERBOSE=1 + +%: + dh $@ + +clean: + dh_testdir + dh_clean + +build: + dh_testdir + +binary: + dh_testroot + dh_prep + dh_installdirs + dh_install + dh_installchangelogs + dh_installinfo + dh_installinit + dh_fixperms + dh_installdeb + dh_gencontrol + dh_md5sums + dh_builddeb diff --git a/packaging/debian/source/format b/packaging/debian/source/format new file mode 100644 index 0000000..af745b3 --- /dev/null +++ b/packaging/debian/source/format @@ -0,0 +1 @@ +3.0 (git) diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..064e01c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,29 @@ + + + + + test/php + + + + + library/Businessprocess + + library/Businessprocess/Director + + + library/Businessprocess/ProvidedHook + + + + diff --git a/public/css/module.less b/public/css/module.less index 1abba0d..c288a78 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -1,43 +1,24 @@ -/* Default font settings */ -h1 { - font-size: 1em; -} - -/* Normalize CSS */ - -.content { - a { - font-weight: normal; - color: inherit; - } - - .text-small { - color: inherit; - font-size: inherit; - } - - p a { - color: @icinga-blue; - text-decoration: underline; - } - -} -.controls h1 a { - color: inherit; - float: right; - font-weight: normal; -} - -.controls h1 form { - display: inline; -} - -h1 a:focus { +a:focus { outline: none; + text-decoration: underline; + &::before { + text-decoration: none; + } +} + +.action-bar a { + color: @icinga-blue; + &:hover::before { + text-decoration: none; + } + margin-right: 1em; +} + +form a { + color: @icinga-blue; } div.bp { - font-size: 0.82em; margin-bottom: 4px; } @@ -52,7 +33,7 @@ table.bp { width: 100%; margin: 0; padding: 0; - color: #0a0a0a; + color: @text-color; border-collapse: collapse; border-spacing: 0; box-sizing: border-box; @@ -173,7 +154,7 @@ table.bp .icon { table.bp.node { td:before { font-family: ifont; - z-index: 10; + z-index: 1; font-size: 1.25em; position: absolute; margin-left: 1.25em; @@ -207,7 +188,8 @@ table.bp tr, table.bp tbody, table.bp th, table.bp td, table.bp.node td > a, tab } table.bp td > a, table.node.missing td > span { - line-height: 2em; + height: 2.5em; + line-height: 2.5em; padding-left: 0.5em; display: block; } @@ -240,7 +222,7 @@ table.bp.handled > tbody > tr > th, table.bp.ok > tbody > tr > th { /* Operator: upper line */ table.bp.operator > tbody > tr:first-child > * { border-top-width: 1px; - border-top-style: dotted; + border-top-style: solid; } table.bp.operator.hovered > tbody > tr:first-child > * { @@ -311,7 +293,7 @@ table.bp { /* Reduce font size after the 3rd level... */ table.bp table.bp table.bp table.bp { - font-size: 0.9em; + font-size: 0.95em; } /* ...and keep it constant afterwards */ @@ -319,7 +301,7 @@ table.bp table.bp table.bp table.bp table.bp { font-size: 1em; } /* Transitions */ -table.bp { +div.knightrider table.bp { // That's ugly, I know .transition(@val1, @val2) { @@ -345,125 +327,343 @@ table.bp { } } -.toplevel:after { +/** BEGIN Dashboard **/ +.overview-dashboard { + > div.action { + width: 20em; + display: inline-block; + + > a { + text-decoration: none; + color: inherit; + vertical-align: middle; + display: block; + padding: 1em; + word-wrap: break-word; + width: 100%; + height: 12em; + overflow: hidden; + box-sizing: border-box; + + > span.header { + font-weight: bold; + display: block; + font-size: 1.25em; + } + + &:hover { + background-color: @text-color; + color: white; + } + + &.addnew:hover { + background-color: @icinga-blue; + } + + i { + float: left; + font-size: 2.5em; + margin-top: -0.1em; + margin-bottom: 2em; + color: inherit; + } + } + } +} +/** END Dashboard **/ + +/** BEGIN Badges **/ +.badges { + display: block; + padding: 0.5em; + + .badge { + border: 1px solid white; + margin: 0; + margin-right: 1px; + } +} + +div.bp .badges { + display: inline-block; + padding-top: 0; +} + +.badge-critical, .badge-down { background: @colorCritical; } +.badge-unknown, .badge-unreachable { background: @colorUnknown; } +.badge-warning { background: @colorWarning; } +.badge-pending { background: @colorPending; } +.badge-missing { background: #ccc; } +/** END Badges **/ + +/** BEGIN Tiles **/ +.tiles:after { content:''; display:block; clear: both; } -.toplevel.few { - font-size: 3em; -} - -.toplevel.normal { - font-size: 2em; -} - -.toplevel.many { - font-size: 1.5em; -} - - -#layout.twocols, #layout.layout-minimal, .compact { - .toplevel.few { - font-size: 1.8em; - } - - .toplevel.normal { - font-size: 1.4em; - } - - .toplevel.many { - font-size: 0.9em; - } -} - -.toplevel > div { - width: 5em; - height: 5em; +.tiles > div { + color: white; + width: 12em; + display: inline-block; float: left; margin-right: 0.2em; margin-bottom: 0.2em; - color: white; - background: #ccc; + height: 6em; + cursor: pointer; + + .badges { + text-align: center; + font-size: 0.5em; + display: block; + } + + > a { + display: block; + } + + &:hover { + box-shadow: 0px 0px 5px #666; + } + + .actions { + font-size: 0.75em; + margin-left: 0.5em; + padding-top: 0.2em; + height: 1.8em; + + i { + float: none; + display: block; + width: 100%; + font-size: 1em; + line-height: normal; + margin: 0; + padding: 0 0 0 0.25em; + } + a { + margin: 0; + padding: 0; + display: inline-block; + width: 1.5em; + height: 1.5em; + border-radius: 0.3em; + } + + a:hover { + background-color: white; + color: @text-color; + } + } } -.toplevel > div > a { + +.tiles > div.parent::before { + content: '&'; + position: absolute; + font-size: 1.2em; +} + +.tiles > div.parent { + width: 100%; + height: 2em; +} + +.tiles > div > a { text-decoration: none; font-size: 0.5em; color: inherit; vertical-align: middle; text-align: center; - display: block; padding: 1em; font-weight: bold; word-wrap: break-word; - height: 10em; - width: 10em; + width: 100%; box-sizing: border-box; } -.toplevel > div > a:hover { - color: black; +.tiles { + > .critical { background-color: @colorCritical; background-color: @colorCritical; color: white; } + > .critical.handled { background-color: @colorCriticalHandled; } + > .down { background-color: @colorCritical; } + > .down.handled { background-color: @colorCriticalHandled; } + > .unknown { background-color: @colorUnknown; } + > .unknown.handled { background-color: @colorUnknownHandled; } + > .unreachable { background-color: @colorUnknown; } + > .unreachable.handled { background-color: @colorUnknownHandled; } + > .warning { background-color: @colorWarning; } + > .warning.handled { background-color: @colorWarningHandled; } + > .ok { background-color: @colorOk; } + > .up { background-color: @colorOk; } + > .pending { background-color: @colorPending; } + > .missing { background-color: #ccc; } + > .addnew { background-color: @icinga-blue; } } -.toplevel > div.critical, .toplevel .badge-critical { - background: @colorCritical; +/* +.tiles > div:hover { + > .critical { background: white; color: @text-color; } + > .critical.handled { background: @colorCriticalHandled; } + > .down { background: @colorCritical; } + > .down.handled { background: @colorCriticalHandled; } + > .unknown { background: @colorUnknown; } + > .unknown.handled { background: @colorUnknownHandled; } + > .unreachable { background: @colorUnknown; } + > .unreachable.handled { background: @colorUnknownHandled; } + > .warning { background: @colorWarning; } + > .warning.handled { background: @colorWarningHandled; } + > .ok { background: @colorOk; } + > .pending { background: @colorPending; } + > .missing { background: #ccc; } +} +*/ +.tiles .missing a { + pointer-events: none; + cursor: default; } -.toplevel > div.critical.handled { - background: @colorCriticalHandled; +.tiles.few { font-size: 2.5em; } +.tiles.normal { font-size: 2.1em; } +.tiles.many { font-size: 1.8em; } + +#layout.twocols, #layout.layout-minimal, .compact { + .tiles.few { font-size: 1.8em; } + .tiles.normal { font-size: 1.8em; } + .tiles.many { font-size: 1.8em; } } -.toplevel > div.down, .toplevel .badge-down { - background: @colorCritical; +#layout.fullscreen-layout .controls { + padding: 0 1em; } -.toplevel > div.down.handled { - background: @colorCriticalHandled; +/** END of tiles **/ + +/** BEGIN breadcrumb **/ + +.breadcrumb { + list-style: none; + overflow: hidden; + padding: 0; + + .badges { + display: inline-block; + padding: 0 0 0 0.5em; + .badge { + line-height: 1.25em; + font-size: 0.8em; + border: 1px solid white; + margin: 0; + margin-right: 1px; + } + } } -.toplevel > div.unknown, .toplevel .badge-unknown { - background: @colorUnknown; +.breadcrumb { + > .critical a { background: @colorCritical; } + > .critical.handled a { background: @colorCriticalHandled; } + > .unknown a { background: @colorUnknown; } + > .unknown.handled a { background: @colorUnknownHandled; } + > .warning a { background: @colorWarning; } + > .warning.handled a { background: @colorWarningHandled; } + > .ok a { background: @colorOk; } } -.toplevel > div.unknown.handled { - background: @colorUnknownHandled; +.breadcrumb { + > .critical a:after { border-left-color: @colorCritical; } + > .critical.handled a:after { border-left-color: @colorCriticalHandled; } + > .unknown a:after { border-left-color: @colorUnknown; } + > .unknown.handled a:after { border-left-color: @colorUnknownHandled; } + > .warning a:after { border-left-color: @colorWarning; } + > .warning.handled a:after { border-left-color: @colorWarningHandled; } + > .ok a:after { border-left-color: @colorOk; } } -.toplevel > div.unreachable, .toplevel .badge-unreachable { - background: @colorUnknown; +.breadcrumb:after { + content:''; + display:block; + clear: both; +} +.breadcrumb li { + float: left; + cursor: pointer; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + +} +.breadcrumb li a { + color: white; + margin: 0; + font-size: 1.2em; + text-decoration: none; + padding-left: 2em; + line-height: 2.5em; + background: @icinga-blue; + position: relative; + display: block; + float: left; + &:focus { + outline: none; + } } -.toplevel > div.unreachable.handled { - background: @colorUnknownHandled; +.breadcrumb li a:before, .breadcrumb li a:after { + content: " "; + display: block; + width: 0; + height: 0; + border-top: 1.3em solid transparent; + border-bottom: 1.2em solid transparent; + position: absolute; + margin-top: -1.2em; + top: 50%; + left: 100%; } -.toplevel > div.warning, .toplevel .badge-warning { - background: @colorWarning; +.breadcrumb li a:before { + border-left: 1.2em solid white; + margin-left: 1px; + z-index: 1; } -.toplevel > div.warning.handled { - background: @colorWarningHandled; +.breadcrumb li a:after { + border-left: 1.2em solid @icinga-blue; + z-index: 2; } -.toplevel > div.ok { - background: @colorOk; +.breadcrumb li:first-child a { + padding-left: 1em; + padding-right: 0.5em; +} +.breadcrumb li:last-child a { + cursor: default; +} +.breadcrumb li:last-child a:hover { + } -.toplevel > div.pending, .toplevel .badge-pending { - background: @colorPending; +.breadcrumb li:not(:last-child) a:hover { background: @text-color; color: white; } +.breadcrumb li:not(:last-child) a:hover:after { border-left-color: @text-color; } + +.breadcrumb li a:focus { + text-decoration: underline; } -.toplevel > div.missing, .toplevel .badge-missing { - background: #ccc; +#layout.twocols, #layout.layout-minimal, .compact { + .breadcrumb { + font-size: 0.9em; + } } -ul.error { +/** END of breadcrumb **/ + + +ul.error, ul.warning { padding: 0; list-style-type: none; background-color: @colorCritical; - font-size: 0.8em; li { font-weight: bold; @@ -473,9 +673,19 @@ ul.error { li a { color: inherit; + text-decoration: underline; + + &:hover { + text-decoration: none; + } } } + +ul.warning { + background-color: @colorWarning; +} + table.sourcecode { font-family: monospace; white-space: pre-wrap; @@ -514,70 +724,390 @@ table.sourcecode { } } -.badges { - display: block; - padding: 0.5em; +/** Forms stolen from director **/ +.content form { + margin-bottom: 2em; +} - .badge { - border: 1px solid white; - margin: 0; - margin-right: 1px; +.content form.inline { + margin: 0; +} + +.invisible { + position: absolute; + left: -100%; +} + +form input[type=file] { + background-color: white; + padding-right: 1em; +} + + +form input[type=submit] { + .button(); + border-width: 1px; + margin-top: 0.5em; + + &:disabled { + border-color: @gray-light; + background-color: @gray-light; + color: #fff; } } -#bp-overlay-container { - display: none; - position: absolute; - background-color: rgba(250, 250, 255, 0.98); - border: 0.25em solid @icinga-blue; - z-index: 3000; - top: 10%; - left: 20%; - right: 20%; - height: 40em; - max-width: 60em; - - .bp-overlay-controls { - background-color: @icinga-blue; - color: white; - height: 2em; - - a { - float: right; - line-height: 2em; - margin-right: 1em; - } - } +form input[type=submit]:first-of-type { + border-width: 2px; } -#layout.minimal-layout #bp-overlay-container { - position: fixed; - left: 0; - right: 0; - top: 0; - bottom: 0; - width: auto; - height: auto; - border: none; - overflow: auto; +form input[type=submit].link-button { + color: @icinga-blue; + background: none; + border: none; + padding: 0; - #bp-overlay { + text-align: left; + + &:hover { + text-decoration: underline; + } +} + +form p.description { + padding: 1em 1em; + margin: 0; + font-style: italic; + width: 100%; +} + +input, select, select option, textarea { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +form ul.form-errors { + margin-bottom: 0.5em; + + ul.errors li { + background: @color-critical; + font-weight: bold; + padding: 0.5em 1em; + color: white; + } +} + +select::-ms-expand, input::-ms-expand, textarea::-ms-expand { /* for IE 11 */ + display: none; +} + +select { + border: 1px solid #ddd; + cursor: pointer; + background: none; +} + +input[type=text], input[type=password], textarea, select { + max-width: 36em; + min-width: 20em; + width: 100%; + line-height: 2em; + height: 2.4em; + padding-left: 0.5em; + border-style: solid; + border-color: transparent; + border-bottom-color: @gray-lighter; + border-width: 1px 1px 1px 3px; + background-color: white; + + &.search { + background: transparent url("../img/icons/search.png") no-repeat scroll 0.5em center / 1em 1em; + padding-left: 2em; + } +} + +select[multiple] { height: auto; - } } -#layout.poor-layout #bp-overlay-container { - left: 2%; - right: 2%; +select option { + height: 2em; + padding-top: 0.3em; } -#layout.compact-layout #bp-overlay-container { - left: 5%; - right: 5%; -} - -#bp-overlay { - padding: 1em; - overflow: auto; - height: 37.5em; +select[multiple=multiple] { + height: auto; } + +label { + line-height: 2em; +} + +form dl { + margin: 0; + padding: 0; +} + +select::-moz-focus-inner { border: 0; } + +select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #000; +} + +select, input[type=text], textarea { + &:hover { + border-style: dotted solid dotted solid; + border-color: @gray-light; + } + + &:focus, &:focus:hover { + border-style: solid; + border-color: @icinga-blue; + outline: none; + } +} + +select[value=""] { + color: blue; + border: 1px solid #666; + background-color: white; +} + +select option { + color: inherit; + padding-left: 0.5em; + background-color: white; +} + +select option[value=""] { + color: #aaa; + background-color: white; +} + +form dt label { + width: auto; + font-weight: normal; + font-size: inherit; + + &.required { + &::after { + content: '*' + } + } + + &:hover { + text-decoration: underline; + cursor: pointer; + } +} + +form fieldset { + min-width: 36em; +} + +form dd input.related-action[type='submit'] { + display: none; +} + +form dd.active li.active input.related-action[type='submit'] { + display: inline-block; +} + +form dd.active { + p.description { + color: inherit; + font-style: normal; + } +} + +form dd { + padding: 0.3em 0.5em; + margin: 0; +} + +form dt { + padding: 0.5em 0.5em; + margin: 0; +} + +form dt.active, form dd.active { + background-color: @tr-active-color; +} + +form dt { + display: inline-block; + vertical-align: top; + min-width: 12em; + min-height: 2.5em; + width: 30%; + &.errors label { + color: @color-critical; + } +} + +form .errors label { + color: @color-critical; +} + +form dd { + display: inline-block; + width: 63%; + min-height: 2.5em; + vertical-align: top; + margin: 0; + &.errors { + input[type=text], select { + border-color: @color-critical; + } + } + + &.full-width { + padding: 0.5em; + width: 100%; + } +} + +form dd:after { + display: block; + content: ''; +} + +form textarea { + height: auto; +} + +form dd ul.errors { + list-style-type: none; + padding-left: 0.3em; + + li { + color: @colorCritical; + padding: 0.3em; + } +} + +form div.hint { + padding: 1em; + background-color: @tr-hover-color; + margin: 1em 0; + max-width: 65em; + font-size: 1em; + + pre { + font-style: normal; + background-color: white; + margin: 0; + padding: 1em; + } +} + + +form { + #_FAKE_SUBMIT { + position: absolute; + left: -100%; + } +} + +/** END of forms **/ + +/** php-diff **/ +.Differences { + width: 100%; + table-layout: fixed; + empty-cells: show; +} + +.Differences thead { + display: none; +} + +.Differences thead th { + text-align: left; + padding-left: 4 / 14 * 16em; +} +.Differences tbody th { + text-align: right; + width: 4em; + padding: 1px 2px; + border-right: 1px solid @gray-light; + background: @gray-lightest; + font-weight: normal; + vertical-align: top; +} + +.Differences tbody td { + width: 50%; + .preformatted(); + word-break: break-all; +} + +@color-diff-ins: #bfb; +@color-diff-del: #faa; +@color-diff-changed-old: #fdd; +@color-diff-changed-new: #efe; + +.DifferencesSideBySide { + ins, del { + text-decoration: none; + } + + .ChangeInsert { + td.Left { + background: @gray-lighter; + } + td.Right { + background: @color-diff-ins; + } + } + + .ChangeDelete { + td.Left { + background: @color-diff-del; + } + td.Right { + background: @gray-lighter; + } + } + + .ChangeReplace { + td.Left { + background: @color-diff-changed-old; + del { + background: @color-diff-del; + } + } + + td.Right { + background: @color-diff-changed-new; + ins { + background: @color-diff-ins; + } + } + + } +} + +.Differences .Skipped { + background: @gray-lightest; +} + +.DifferencesInline .ChangeReplace .Left, +.DifferencesInline .ChangeDelete .Left { + background: #fdd; +} + +.DifferencesInline .ChangeReplace .Right, +.DifferencesInline .ChangeInsert .Right { + background: #dfd; +} + +.DifferencesInline .ChangeReplace ins { + background: #9e9; +} + +.DifferencesInline .ChangeReplace del { + background: #e99; +} +/** END of php-diff **/ diff --git a/public/js/module.js b/public/js/module.js index 0fe40cd..f524d6d 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -22,42 +22,28 @@ * Tell Icinga about our event handlers */ this.module.on('beforerender', this.rememberOpenedBps); - this.module.on('rendered', this.rendered); + this.module.on('rendered', this.onRendered); this.module.on('click', 'table.bp.process > tbody > tr:first-child > td > a:last-child', this.processTitleClick); this.module.on('click', 'table.bp > tbody > tr:first-child > th', this.processOperatorClick); - this.module.on('click', '.bp-overlay-controls a', this.closeOverlay); - this.module.on('click', 'a', this.checkOverlay); + this.module.on('focus', 'form input, form textarea, form select', this.formElementFocus); this.module.on('mouseenter', 'table.bp > tbody > tr > td > a', this.procMouseOver); this.module.on('mouseenter', 'table.bp > tbody > tr > th', this.procMouseOver); this.module.on('mouseenter', 'table.node.missing > tbody > tr > td > span', this.procMouseOver); this.module.on('mouseleave', 'div.bp', this.procMouseOut); - if ($('#bp-overlay').length < 1) { - $('#layout').append(''); - } - this.module.icinga.logger.debug('BP module loaded'); + this.module.on('click', 'div.tiles > div', this.tileClick); + + this.module.icinga.logger.debug('BP module initialized'); }, - closeOverlay: function(event) { - $('#bp-overlay-container').hide(); - $('#bp-overlay').html(''); - }, - - checkOverlay: function(event) { - $el = $(event.currentTarget); - $sourceContainer = $el.closest('.container'); - $target = $el.closest('[data-base-target]'); - if ($target.length < 1) { return; } - - targetId = $target.data('baseTarget'); - if ($target.data('baseTarget') !== 'bp-overlay') { return; } - - $('#bp-overlay').data('sourceContainer', $sourceContainer.attr('id')); - $('#bp-overlay-container').css({ - 'display': 'block' - }); + onRendered: function (event) { + var $container = $(event.currentTarget); + this.fixFullscreen($container); + this.fixOpenedBps($container); + this.highlightFormErrors($container); + this.hideInactiveFormDescriptions($container); }, processTitleClick: function (event) { @@ -89,6 +75,14 @@ } }, + hideInactiveFormDescriptions: function($container) { + $container.find('dd').not('.active').find('p.description').hide(); + }, + + tileClick: function(event) { + $(event.currentTarget).find('> a').first().trigger('click'); + }, + /** * Add 'hovered' class to hovered title elements * @@ -158,55 +152,59 @@ });*/ }, - rendered: function(event) { - var $container = $(event.currentTarget); - if ($container.attr('id') === 'bp-overlay') { - this.onOverlayRendered(); + fixFullscreen: function($container) { + var $controls = $container.find('div.controls'); + var $layout = $('#layout'); + var icinga = this.module.icinga; + if ($controls.hasClass('want-fullscreen')) { + if (!$layout.hasClass('fullscreen-layout')) { + + $layout.addClass('fullscreen-layout'); + $controls.removeAttr('style'); + $container.find('.fake-controls').remove(); + icinga.ui.currentLayout = 'fullscreen'; + } + } else { + if ($layout.hasClass('fullscreen-layout')) { + $layout.removeClass('fullscreen-layout'); + icinga.ui.layoutHasBeenChanged(); + icinga.ui.initializeControls($container); + } } - this.fixOpenedBps($container); }, fixOpenedBps: function($container) { - var container_id = $container.attr('id'); + var $bpDiv = $container.find('div.bp'); + var bpName = $bpDiv.attr('id'); - if (typeof this.idCache[container_id] === 'undefined') { + if (typeof this.idCache[bpName] === 'undefined') { return; } - var $procs = $('table.process', $container); - $.each(this.idCache[container_id], function(idx, id) { + var $procs = $bpDiv.find('table.process'); + + $.each(this.idCache[bpName], function(idx, id) { var $el = $('#' + id); $procs = $procs.not($el); $el.parents('table.process').each(function (idx, el) { $procs = $procs.not($(el)); - }); }); $procs.addClass('collapsed'); }, - onOverlayRendered: function() - { - // close overlay if required: - $overlay = $('#bp-overlay'); - $overlayContainer = $('#bp-overlay-container'); - if ($overlayContainer.css('display') === 'block' && $overlay.html().match(/^__CLOSEME__/)) { - $source = $('#' + $overlay.data('sourceContainer')); - $overlayContainer.hide(); - self.icinga.loader.loadUrl($source.data('icingaUrl'), $source, undefined, undefined, undefined, true); - } - }, - /** * Get a list of all currently opened BPs. * * Only get the deepest nodes to keep requests as small as possible */ rememberOpenedBps: function (event) { - var $container = $(event.currentTarget); var ids = []; - $('table.process', $container) + var $bpDiv = $(event.currentTarget).find('div.bp'); + var $bpName = $bpDiv.attr('id'); + + $bpDiv.find('table.process') .not('table.process.collapsed') .not('table.process.collapsed table.process') .each(function (key, el) { @@ -216,8 +214,53 @@ return; } - this.idCache[$container.attr('id')] = ids; + this.idCache[$bpName] = ids; + }, + + /** BEGIN Form handling, borrowed from Director **/ + formElementFocus: function(ev) + { + var $input = $(ev.currentTarget); + var $dd = $input.closest('dd'); + $dd.find('p.description').show(); + if ($dd.attr('id') && $dd.attr('id').match(/button/)) { + return; + } + var $li = $input.closest('li'); + var $dt = $dd.prev(); + var $form = $dd.closest('form'); + + $form.find('dt, dd, li').removeClass('active'); + $li.addClass('active'); + $dt.addClass('active'); + $dd.addClass('active'); + $dd.find('p.description.fading-out') + .stop(true) + .removeClass('fading-out') + .fadeIn('fast'); + + $form.find('dd').not($dd) + .find('p.description') + .not('.fading-out') + .addClass('fading-out') + .delay(2000) + .fadeOut('slow', function() { + $(this).removeClass('fading-out').hide() + }); + }, + + highlightFormErrors: function($container) + { + $container.find('dd ul.errors').each(function(idx, ul) { + var $ul = $(ul); + var $dd = $ul.closest('dd'); + var $dt = $dd.prev(); + + $dt.addClass('errors'); + $dd.addClass('errors'); + }); } + /** END Form handling **/ }; Icinga.availableModules.businessprocess = Bp; diff --git a/run.php b/run.php index 6905547..2cde2ee 100644 --- a/run.php +++ b/run.php @@ -1,4 +1,5 @@ provideHook('monitoring/HostActions'); +$this->provideHook('monitoring/ServiceActions'); //$this->provideHook('director/shipConfigFiles'); - diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..40c16cf --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,9 @@ +makeLoop()->getNode('d'); + $bpNode->checkForLoops(); + } + + /** + * @expectedExceptionMessage d -> a -> b -> c -> a + * @expectedException \Icinga\Module\Businessprocess\Exception\NestingError + */ + public function testNestingErrorReportsFullLoop() + { + /** @var BpNode $bpNode */ + $bpNode = $this->makeLoop()->getNode('d'); + $bpNode->checkForLoops(); + } + + public function testStateForALoopGivesUnknown() + { + $loop = $this->makeLoop(); + /** @var BpNode $bpNode */ + $bpNode = $loop->getNode('d'); + $this->assertEquals( + 'UNKNOWN', + $bpNode->getStateName() + ); + } +} diff --git a/test/php/library/Businessprocess/HostNodeTest.php b/test/php/library/Businessprocess/HostNodeTest.php new file mode 100644 index 0000000..2214f67 --- /dev/null +++ b/test/php/library/Businessprocess/HostNodeTest.php @@ -0,0 +1,65 @@ +assertEquals( + 'localhost', + $this->localhost()->getHostname() + ); + } + + public function testReturnsCorrectIdentifierWhenCastedToString() + { + $this->assertEquals( + 'localhost;Hoststatus', + (string) $this->localhost() + ); + } + + public function testReturnsCorrectAlias() + { + $this->assertEquals( + 'localhost', + $this->localhost()->getAlias() + ); + } + + public function testRendersCorrectLink() + { + $this->assertEquals( + '' + . 'localhost', + $this->localhost()->getLink()->render() + ); + } + + /** + * @expectedException \Icinga\Exception\ProgrammingError + */ + public function testWhetherSettingAnInvalidStateFails() + { + $bp = new BpConfig(); + $host = $bp->createHost('localhost')->setState(98); + $bp->createBp('p')->addChild($host)->getState(); + } + + /** + * @return HostNode + */ + protected function localhost() + { + $bp = new BpConfig(); + return new HostNode($bp, (object) array( + 'hostname' => 'localhost', + 'state' => 0, + )); + } +} diff --git a/test/php/library/Businessprocess/Html/AttributeTest.php b/test/php/library/Businessprocess/Html/AttributeTest.php new file mode 100644 index 0000000..af4d82b --- /dev/null +++ b/test/php/library/Businessprocess/Html/AttributeTest.php @@ -0,0 +1,150 @@ +assertInstanceOf( + get_class(new Attribute('a', 'b')), + Attribute::create('a', 'b') + ); + } + + public function testKnowsItsName() + { + $a = new Attribute('target', '_blank'); + $this->assertEquals( + 'target', + $a->getName() + ); + } + + public function testKnowsItsValue() + { + $a = new Attribute('target', '_blank'); + $this->assertEquals( + '_blank', + $a->getValue() + ); + } + + public function testItsValueCanBeModified() + { + $a = new Attribute('target', '_blank'); + $a->setValue('_self'); + $this->assertEquals( + '_self', + $a->getValue() + ); + } + + public function testPreservesComplexValues() + { + $a = new Attribute('special', 'süß "\'&'); + $this->assertEquals( + 'süß "\'&', + $a->getValue() + ); + } + + public function testAllowsToExtendItsValue() + { + $a = new Attribute('class', 'first'); + $a->addValue('second'); + + $this->assertEquals( + array('first', 'second'), + $a->getValue() + ); + + $a->addValue('third'); + + $this->assertEquals( + array('first', 'second', 'third'), + $a->getValue() + ); + + $a->addValue(array('some', 'more')); + + $this->assertEquals( + array('first', 'second', 'third', 'some', 'more'), + $a->getValue() + ); + } + + public function testAcceptsAndReturnsArrayValues() + { + $value = array('first', 'second', 'third'); + $a = new Attribute('class', $value); + + $this->assertEquals( + $value, + $a->getValue() + ); + + $value[] = 'forth'; + + $a->setValue($value); + $this->assertEquals( + $value, + $a->getValue() + ); + } + + public function testCorrectlyRendersItsName() + { + $a = new Attribute('class', 'test'); + $this->assertEquals( + 'class', + $a->renderName() + ); + } + + public function testCorrectlyRendersItsValue() + { + $a = new Attribute('href', '/some/url?a=b&c=d'); + $this->assertEquals( + '/some/url?a=b&c=d', + $a->renderValue() + ); + } + + public function testCorrectlyRendersArrayValues() + { + $a = new Attribute('weird', array('"sü?ß', '/some/url?a=b&c=d')); + $this->assertEquals( + '"sü?ß /some/url?a=b&c=d', + $a->renderValue() + ); + } + + public function testCorrectlyEscapesAName() + { + $this->assertEquals( + 'class', + Attribute::escapeName('class') + ); + } + + public function testCorrectlyEscapesAValue() + { + $this->assertEquals( + ""sü?ß' /some/url?a=b&c=d", + Attribute::escapeValue('"sü?ß\' /some/url?a=b&c=d') + ); + } + + public function testRendersCorrectls() + { + $a = new Attribute('weird', array('"sü?ß', '/some/url?a=b&c=d')); + $this->assertEquals( + 'weird=""sü?ß /some/url?a=b&c=d"', + $a->render() + ); + } +} diff --git a/test/php/library/Businessprocess/Html/AttributesTest.php b/test/php/library/Businessprocess/Html/AttributesTest.php new file mode 100644 index 0000000..9cbfc08 --- /dev/null +++ b/test/php/library/Businessprocess/Html/AttributesTest.php @@ -0,0 +1,111 @@ + array('small', 'nice'), + 'target' => '_blank' + ) + ); + + $this->assertEquals( + ' class="small nice" target="_blank"', + $a->render() + ); + } + + public function testCanBeInstantiatedThroughCreate() + { + $class = get_class(new Attributes()); + + $this->assertInstanceOf( + $class, + Attributes::create() + ); + + $this->assertInstanceOf( + $class, + Attributes::create(array('some' => 'attr')) + ); + } + + public function testCanBeCreatedForArrayOrNullOrAttributes() + { + $empty = Attributes::wantAttributes(null); + $this->assertEquals('', $empty->render()); + + $array = Attributes::wantAttributes(array('a' => 'b')); + $this->assertEquals(' a="b"', $array->render()); + + $attributes = Attributes::wantAttributes( + Attributes::create(array('a' => 'b')) + ); + $this->assertEquals(' a="b"', $attributes->render()); + } + + public function testCanBeExtendedWithAnAttribute() + { + $a = Attributes::create(); + $a->add(Attribute::create('a', 'b')); + $this->assertEquals(' a="b"', $a->render()); + + $a->add(Attribute::create('c', 'd')); + $this->assertEquals(' a="b" c="d"', $a->render()); + + $a->add(Attribute::create('a', 'c')); + $this->assertEquals(' a="b c" c="d"', $a->render()); + } + + public function testCanBeExtendedWithAttributes() + { + $a = Attributes::create(); + $a->add(Attributes::create(array('a' => 'b'))); + $this->assertEquals(' a="b"', $a->render()); + + $a->add(Attributes::create( + array( + 'a' => 'c', + 'c' => 'd' + ) + )); + $this->assertEquals(' a="b c" c="d"', $a->render()); + } + + public function testCanBeExtendedWithANameValuePair() + { + $a = Attributes::create(); + $a->add('a', 'b'); + $this->assertEquals(' a="b"', $a->render()); + } + + public function testAttributesCanBeReplacedWithAnAttribute() + { + $a = Attributes::create(); + $a->add(Attribute::create('a', 'b')); + $a->set(Attribute::create('a', 'c')); + $this->assertEquals(' a="c"', $a->render()); + } + + public function testAttributesCanBeReplacedWithANameValuePair() + { + $a = Attributes::create(); + $a->add(Attribute::create('a', 'b')); + $a->set('a', 'c'); + $this->assertEquals(' a="c"', $a->render()); + } + + public function testCanBeConstructedAndRenderedEmpty() + { + $a = new Attributes(); + $this->assertEquals('', $a->render()); + } +} diff --git a/test/php/library/Businessprocess/Html/HtmlTagTest.php b/test/php/library/Businessprocess/Html/HtmlTagTest.php new file mode 100644 index 0000000..4b24558 --- /dev/null +++ b/test/php/library/Businessprocess/Html/HtmlTagTest.php @@ -0,0 +1,18 @@ +assertEquals( + $h1->render(), + '

Hea & der

' + ); + } +} diff --git a/test/php/library/Businessprocess/Html/HtmlTest.php b/test/php/library/Businessprocess/Html/HtmlTest.php new file mode 100644 index 0000000..87200cd --- /dev/null +++ b/test/php/library/Businessprocess/Html/HtmlTest.php @@ -0,0 +1,42 @@ +setContent('Some content'); + $this->assertEquals( + 'Some content', + $h->render() + ); + } + + public function testContentCanBeExtended() + { + $h = new Html(); + $h->setContent('Some content'); + $h->addContent('More content'); + $this->assertEquals( + 'Some contentMore content', + $h->render() + ); + } + + public function testSeparatorsAreRespected() + { + $h = new Html(); + $h->setContent('Some content'); + $h->setSeparator(', and '); + $h->addContent('More content'); + $this->assertEquals( + 'Some content, and More content', + $h->render() + ); + } +} diff --git a/test/php/library/Businessprocess/Html/LinkTest.php b/test/php/library/Businessprocess/Html/LinkTest.php new file mode 100644 index 0000000..a8d4b37 --- /dev/null +++ b/test/php/library/Businessprocess/Html/LinkTest.php @@ -0,0 +1,43 @@ +assertEquals( + 'Click here', + $l->renderContent() + ); + } + + public function testSimpleLinkRendersCorrectly() + { + $l = Link::create('Click here', 'go/some/where'); + $this->assertEquals( + 'Click here', + $l->render() + ); + } + + public function testLinkWithParamsRendersCorrectly() + { + $l = Link::create( + 'Click here', + 'go/some/where', + array( + 'with' => 'me', + 'and' => 'aDog' + ) + ); + $this->assertEquals( + 'Click here', + $l->render() + ); + } +} diff --git a/test/php/library/Businessprocess/Html/TextTest.php b/test/php/library/Businessprocess/Html/TextTest.php new file mode 100644 index 0000000..bd6e04e --- /dev/null +++ b/test/php/library/Businessprocess/Html/TextTest.php @@ -0,0 +1,43 @@ +assertEquals( + 'A & O', + Text::create('A & O')->getText() + ); + } + + public function testTextIsRenderedAsGivenWhenDeclaredBeingEscaped() + { + $this->assertEquals( + 'A & O', + Text::create('A & O')->setEscaped()->render() + ); + + $this->assertEquals( + 'A & O', + Text::create('A & O')->setEscaped(true)->render() + ); + + $this->assertEquals( + 'A & O', + Text::create('A & O')->setEscaped(false)->render() + ); + } + + public function testTextIsEscapedWhenRendered() + { + $this->assertEquals( + 'A & O', + Text::create('A & O')->render() + ); + } +} diff --git a/test/php/library/Businessprocess/Operators/AndOperatorTest.php b/test/php/library/Businessprocess/Operators/AndOperatorTest.php new file mode 100644 index 0000000..c9b3047 --- /dev/null +++ b/test/php/library/Businessprocess/Operators/AndOperatorTest.php @@ -0,0 +1,214 @@ +emptyConfigSection()); + $expressions = array( + 'a = b', + 'a = b & c & d', + ); + + foreach ($expressions as $expression) { + $this->assertInstanceOf( + 'Icinga\\Module\\Businessprocess\\BpConfig', + $storage->loadFromString('dummy', $expression) + ); + } + } + + public function testThreeTimesCriticalIsCritical() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 2); + $bp->setNodeState('d', 2); + + $this->assertEquals( + 'CRITICAL', + $bp->getNode('a')->getStateName() + ); + } + + public function testTwoTimesCriticalAndOkIsCritical() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 0); + $bp->setNodeState('d', 2); + + $this->assertEquals( + 'CRITICAL', + $bp->getNode('a')->getStateName() + ); + } + + public function testCriticalAndWarningAndOkIsCritical() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 0); + + $this->assertEquals( + 'CRITICAL', + $bp->getNode('a')->getStateName() + ); + } + + public function testUnknownAndWarningAndOkIsUnknown() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 0); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 3); + + $this->assertEquals( + 'UNKNOWN', + $bp->getNode('a')->getStateName() + ); + } + + public function testTwoTimesWarningAndOkIsWarning() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 0); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 1); + + $this->assertEquals( + 'WARNING', + $bp->getNode('a')->getStateName() + ); + } + + public function testThreeTimesOkIsOk() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 0); + $bp->setNodeState('c', 0); + $bp->setNodeState('d', 0); + + $this->assertEquals( + 'OK', + $bp->getNode('a')->getStateName() + ); + } + + public function testWhetherSimpleAndOperationWorks() + { + $bp = new BpConfig(); + $bp->throwErrors(); + $host = $bp->createHost('localhost')->setState(1); + $service = $bp->createService('localhost', 'ping')->setState(1); + $p = $bp->createBp('p'); + $p->addChild($host); + $p->addChild($service); + + $this->assertEquals( + 'DOWN', + $host->getStateName() + ); + + $this->assertEquals( + 'WARNING', + $service->getStateName() + ); + + $this->assertEquals( + 'CRITICAL', + $p->getStateName() + ); + } + + public function testWhetherSimpleOrOperationWorks() + { + $bp = new BpConfig(); + $bp->throwErrors(); + $host = $bp->createHost('localhost')->setState(1); + $service = $bp->createService('localhost', 'ping')->setState(1); + $p = $bp->createBp('p', '|'); + $p->addChild($host); + $p->addChild($service); + + $this->assertEquals('DOWN', $host->getStateName()); + $this->assertEquals('WARNING', $service->getStateName()); + $this->assertEquals('WARNING', $p->getStateName()); + } + + public function testWhetherPendingIsAccepted() + { + $bp = new BpConfig(); + $host = $bp->createHost('localhost')->setState(99); + $service = $bp->createService('localhost', 'ping')->setState(99); + $p = $bp->createBp('p') + ->addChild($host) + ->addChild($service); + + $this->assertEquals( + 'PENDING', + $p->getStateName() + ); + } + + public function testWhetherWarningIsWorseThanPending() + { + $bp = new BpConfig(); + $host = $bp->createHost('localhost')->setState(99); + $service = $bp->createService('localhost', 'ping')->setState(1); + $p = $bp->createBp('p') + ->addChild($host) + ->addChild($service); + + $this->assertEquals( + 'WARNING', + $p->getStateName() + ); + } + + public function testWhetherPendingIsWorseThanUpOrOk() + { + $bp = new BpConfig(); + $host = $bp->createHost('localhost')->setState(99); + $service = $bp->createService('localhost', 'ping')->setState(0); + $p = $bp->createBp('p') + ->addChild($host) + ->addChild($service); + + $this->assertEquals( + 'PENDING', + $p->getStateName() + ); + + $p->clearState(); + $host->setState(0); + $service->setState(99); + + $this->assertEquals( + 'PENDING', + $p->getStateName() + ); + } + + /** + * @return BpConfig + */ + protected function getBp() + { + $storage = new LegacyStorage($this->emptyConfigSection()); + $expression = 'a = b & c & d'; + $bp = $storage->loadFromString('dummy', $expression); + $bp->createBp('b'); + $bp->createBp('c'); + $bp->createBp('d'); + + return $bp; + } +} diff --git a/test/php/library/Businessprocess/Operators/MinOperatorTest.php b/test/php/library/Businessprocess/Operators/MinOperatorTest.php new file mode 100644 index 0000000..713c478 --- /dev/null +++ b/test/php/library/Businessprocess/Operators/MinOperatorTest.php @@ -0,0 +1,118 @@ +emptyConfigSection()); + $expressions = array( + 'a = 1 of: b', + 'a = 2 of: b + c + d', + ); + $this->getName(); + foreach ($expressions as $expression) { + $this->assertInstanceOf( + 'Icinga\\Module\\Businessprocess\\BpConfig', + $storage->loadFromString('dummy', $expression) + ); + } + } + public function testTwoOfThreeTimesCriticalAreAtLeastCritical() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 2); + $bp->setNodeState('d', 2); + + $this->assertEquals( + 'CRITICAL', + $bp->getNode('a')->getStateName() + ); + } + + public function testTwoOfTwoTimesCriticalAndUnknownAreAtLeastCritical() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 3); + $bp->setNodeState('d', 2); + + $this->assertEquals( + 'CRITICAL', + $bp->getNode('a')->getStateName() + ); + } + + public function testTwoOfCriticalAndWarningAndOkAreAtLeastWarning() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 0); + + $this->assertEquals( + 'WARNING', + $bp->getNode('a')->getStateName() + ); + } + + public function testTwoOfUnknownAndWarningAndCriticalAreAtLeastUnknown() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 3); + + $this->assertEquals( + 'UNKNOWN', + $bp->getNode('a')->getStateName() + ); + } + + public function testTwoOfTwoTimesWarningAndUnknownAreAtLeastWarning() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 0); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 1); + + $this->assertEquals( + 'WARNING', + $bp->getNode('a')->getStateName() + ); + } + + public function testTwoOfThreeTimesOkAreAtLeastOk() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 0); + $bp->setNodeState('c', 0); + $bp->setNodeState('d', 0); + + $this->assertEquals( + 'OK', + $bp->getNode('a')->getStateName() + ); + } + + /** + * @return BpConfig + */ + protected function getBp() + { + $storage = new LegacyStorage($this->emptyConfigSection()); + $expression = 'a = 2 of: b + c + d'; + $bp = $storage->loadFromString('dummy', $expression); + $bp->createBp('b'); + $bp->createBp('c'); + $bp->createBp('d'); + + return $bp; + } +} diff --git a/test/php/library/Businessprocess/Operators/NotOperatorTest.php b/test/php/library/Businessprocess/Operators/NotOperatorTest.php new file mode 100644 index 0000000..dad8042 --- /dev/null +++ b/test/php/library/Businessprocess/Operators/NotOperatorTest.php @@ -0,0 +1,154 @@ +emptyConfigSection()); + $expressions = array( + 'a = !b', + 'a = ! b', + 'a = b ! c ! d', + 'a = ! b ! c ! d !', + ); + + foreach ($expressions as $expression) { + $this->assertInstanceOf( + 'Icinga\\Module\\Businessprocess\\BpConfig', + $storage->loadFromString('dummy', $expression) + ); + } + } + + public function testASimpleNegationGivesTheCorrectResult() + { + $storage = new LegacyStorage($this->emptyConfigSection()); + $expression = 'a = !b'; + $bp = $storage->loadFromString('dummy', $expression); + $a = $bp->getNode('a'); + $b = $bp->createBp('b')->setState(3); + $this->assertEquals( + 'OK', + $a->getStateName() + ); + + $a->clearState(); + $b->setState(0); + $this->assertEquals( + 'CRITICAL', + $a->getStateName() + ); + } + + public function testThreeTimesCriticalIsOk() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 2); + $bp->setNodeState('d', 2); + + $this->assertEquals( + 'OK', + $bp->getNode('a')->getStateName() + ); + } + + public function testThreeTimesUnknownIsOk() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 3); + $bp->setNodeState('c', 3); + $bp->setNodeState('d', 3); + + $this->assertEquals( + 'OK', + $bp->getNode('a')->getStateName() + ); + } + + public function testThreeTimesWarningIsWarning() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 1); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 1); + + $this->assertEquals( + 'WARNING', + $bp->getNode('a')->getStateName() + ); + } + + public function testThreeTimesOkIsCritical() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 0); + $bp->setNodeState('c', 0); + $bp->setNodeState('d', 0); + + $this->assertEquals( + 'CRITICAL', + $bp->getNode('a')->getStateName() + ); + } + + public function testNotOkAndWarningAndCriticalIsOk() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 0); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 2); + + $this->assertEquals( + 'OK', + $bp->getNode('a')->getStateName() + ); + } + + public function testNotWarningAndUnknownAndCriticalIsOk() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 3); + $bp->setNodeState('c', 2); + $bp->setNodeState('d', 1); + + $this->assertEquals( + 'OK', + $bp->getNode('a')->getStateName() + ); + } + + public function testNotTwoTimesWarningAndOkIsWarning() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 0); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 1); + + $this->assertEquals( + 'WARNING', + $bp->getNode('a')->getStateName() + ); + } + + /** + * @return BpConfig + */ + protected function getBp() + { + $storage = new LegacyStorage($this->emptyConfigSection()); + $expression = 'a = ! b ! c ! d'; + $bp = $storage->loadFromString('dummy', $expression); + $bp->createBp('b'); + $bp->createBp('c'); + $bp->createBp('d'); + + return $bp; + } +} diff --git a/test/php/library/Businessprocess/Operators/OrOperatorTest.php b/test/php/library/Businessprocess/Operators/OrOperatorTest.php new file mode 100644 index 0000000..a9f5b1a --- /dev/null +++ b/test/php/library/Businessprocess/Operators/OrOperatorTest.php @@ -0,0 +1,119 @@ +emptyConfigSection()); + $expressions = array( + 'a = b', + 'a = b | c | d', + ); + + foreach ($expressions as $expression) { + $this->assertInstanceOf( + 'Icinga\\Module\\Businessprocess\\BpConfig', + $storage->loadFromString('dummy', $expression) + ); + } + } + + public function testThreeTimesCriticalIsCritical() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 2); + $bp->setNodeState('d', 2); + + $this->assertEquals( + 'CRITICAL', + $bp->getNode('a')->getStateName() + ); + } + + public function testTwoTimesCriticalOrUnknownIsUnknown() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 3); + $bp->setNodeState('d', 2); + + $this->assertEquals( + 'UNKNOWN', + $bp->getNode('a')->getStateName() + ); + } + + public function testCriticalOrWarningOrOkIsOk() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 0); + + $this->assertEquals( + 'OK', + $bp->getNode('a')->getStateName() + ); + } + + public function testUnknownOrWarningOrCriticalIsWarning() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 2); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 3); + + $this->assertEquals( + 'WARNING', + $bp->getNode('a')->getStateName() + ); + } + + public function testTwoTimesWarningAndOkIsOk() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 0); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 1); + + $this->assertEquals( + 'OK', + $bp->getNode('a')->getStateName() + ); + } + + public function testThreeTimesWarningIsWarning() + { + $bp = $this->getBp(); + $bp->setNodeState('b', 1); + $bp->setNodeState('c', 1); + $bp->setNodeState('d', 1); + + $this->assertEquals( + 'WARNING', + $bp->getNode('a')->getStateName() + ); + } + + /** + * @return BpConfig + */ + protected function getBp() + { + $storage = new LegacyStorage($this->emptyConfigSection()); + $expression = 'a = b | c | d'; + $bp = $storage->loadFromString('dummy', $expression); + $bp->createBp('b'); + $bp->createBp('c'); + $bp->createBp('d'); + + return $bp; + } +} diff --git a/test/php/library/Businessprocess/ServiceNodeTest.php b/test/php/library/Businessprocess/ServiceNodeTest.php new file mode 100644 index 0000000..152c256 --- /dev/null +++ b/test/php/library/Businessprocess/ServiceNodeTest.php @@ -0,0 +1,56 @@ +assertEquals( + 'localhost', + $this->pingOnLocalhost()->getHostname() + ); + } + + public function testReturnsCorrectServiceDescription() + { + $this->assertEquals( + 'ping <> pong', + $this->pingOnLocalhost()->getServiceDescription() + ); + } + + public function testReturnsCorrectAlias() + { + $this->assertEquals( + 'localhost: ping <> pong', + $this->pingOnLocalhost()->getAlias() + ); + } + + public function testRendersCorrectLink() + { + $this->assertEquals( + '' + . 'localhost: ping <> pong', + $this->pingOnLocalhost()->getLink()->render() + ); + } + + /** + * @return ServiceNode + */ + protected function pingOnLocalhost() + { + $bp = new BpConfig(); + return new ServiceNode($bp, (object) array( + 'hostname' => 'localhost', + 'service' => 'ping <> pong', + 'state' => 0, + )); + } +} diff --git a/test/php/library/Businessprocess/Storage/LegacyStorageTest.php b/test/php/library/Businessprocess/Storage/LegacyStorageTest.php new file mode 100644 index 0000000..038261d --- /dev/null +++ b/test/php/library/Businessprocess/Storage/LegacyStorageTest.php @@ -0,0 +1,130 @@ +assertInstanceOf( + $baseClass, + new LegacyStorage($this->emptyConfigSection()) + ); + } + + public function testDefaultConfigDirIsDiscoveredCorrectly() + { + $this->assertEquals( + $this->getTestsBaseDir('config/modules/businessprocess/processes'), + $this->makeInstance()->getConfigDir() + ); + } + + public function testAllAvailableProcessesAreListed() + { + $keys = array_keys($this->makeInstance()->listProcesses()); + $this->assertEquals( + array( + 'broken_wrong-operator', + 'combined', + 'simple_with-header', + 'simple_without-header', + ), + $keys + ); + } + + public function testHeaderTitlesAreRespectedInProcessList() + { + $keys = array_values($this->makeInstance()->listProcesses()); + $this->assertEquals( + array( + 'broken_wrong-operator', + 'combined', + 'Simple with header (simple_with-header)', + 'simple_without-header', + ), + $keys + ); + } + + public function testProcessFilenameIsReturned() + { + $this->assertEquals( + $this->getTestsBaseDir('config/modules/businessprocess/processes/simple_with-header.conf'), + $this->makeInstance()->getFilename('simple_with-header') + ); + } + + public function testAnExistingProcessExists() + { + $this->assertTrue( + $this->makeInstance()->hasProcess('simple_with-header') + ); + } + + public function testAMissingProcessIsMissing() + { + $this->assertFalse( + $this->makeInstance()->hasProcess('simple_with-headerx') + ); + } + + public function testAValidProcessCanBeLoaded() + { + $this->assertInstanceOf( + $this->processClass, + $this->makeInstance()->loadProcess('simple_with-header') + ); + } + + public function testProcessConfigCanBeLoadedFromAString() + { + $this->assertInstanceOf( + $this->processClass, + $this->makeInstance()->loadFromString('dummy', 'a = Host1;ping & Host2;ping') + ); + } + + public function testFullProcessSourceCanBeFetched() + { + $this->assertEquals( + file_get_contents( + $this->getTestsBaseDir( + 'config/modules/businessprocess/processes/simple_with-header.conf' + ) + ), + $this->makeInstance()->getSource('simple_with-header') + ); + } + + public function testTitleCanBeReadFromConfig() + { + $this->assertEquals( + 'Simple with header', + $this->makeInstance()->loadProcess('simple_with-header')->getMetadata()->get('Title') + ); + } + + public function testAConfiguredLoopCanBeParsed() + { + $this->assertInstanceOf( + $this->processClass, + $this->makeLoop() + ); + } + + public function testImportedNodesCanBeParsed() + { + $this->assertInstanceOf( + $this->processClass, + $this->makeInstance()->loadProcess('combined') + ); + } +}