summaryrefslogtreecommitdiffstats
path: root/library/Businessprocess
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:42:35 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:42:35 +0000
commit18db984057b83ca4962c89b6b79bdce6a660b58f (patch)
tree2c9f23c086b4dfcb3e7eb2ec69210206b0782d3c /library/Businessprocess
parentInitial commit. (diff)
downloadicingaweb2-module-businessprocess-c317dc6bab668eef621846dc909e6b1af78bf49a.tar.xz
icingaweb2-module-businessprocess-c317dc6bab668eef621846dc909e6b1af78bf49a.zip
Adding upstream version 2.4.0.upstream/2.4.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--library/Businessprocess/BpConfig.php1033
-rw-r--r--library/Businessprocess/BpNode.php664
-rw-r--r--library/Businessprocess/Common/EnumList.php168
-rw-r--r--library/Businessprocess/Director/ShipConfigFiles.php22
-rw-r--r--library/Businessprocess/Exception/ModificationError.php9
-rw-r--r--library/Businessprocess/Exception/NestingError.php9
-rw-r--r--library/Businessprocess/Form.php36
-rw-r--r--library/Businessprocess/HostNode.php64
-rw-r--r--library/Businessprocess/IcingaDbObject.php94
-rw-r--r--library/Businessprocess/ImportedNode.php135
-rw-r--r--library/Businessprocess/Metadata.php264
-rw-r--r--library/Businessprocess/Modification/NodeAction.php179
-rw-r--r--library/Businessprocess/Modification/NodeAddChildrenAction.php75
-rw-r--r--library/Businessprocess/Modification/NodeApplyManualOrderAction.php29
-rw-r--r--library/Businessprocess/Modification/NodeCopyAction.php39
-rw-r--r--library/Businessprocess/Modification/NodeCreateAction.php129
-rw-r--r--library/Businessprocess/Modification/NodeModifyAction.php121
-rw-r--r--library/Businessprocess/Modification/NodeMoveAction.php212
-rw-r--r--library/Businessprocess/Modification/NodeRemoveAction.php90
-rw-r--r--library/Businessprocess/Modification/ProcessChanges.php295
-rw-r--r--library/Businessprocess/MonitoredNode.php19
-rw-r--r--library/Businessprocess/MonitoringRestrictions.php65
-rw-r--r--library/Businessprocess/Node.php546
-rw-r--r--library/Businessprocess/ProvidedHook/Icingadb/HostActions.php22
-rw-r--r--library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php10
-rw-r--r--library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php26
-rw-r--r--library/Businessprocess/ProvidedHook/Monitoring/HostActions.php18
-rw-r--r--library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php25
-rw-r--r--library/Businessprocess/Renderer/Breadcrumb.php78
-rw-r--r--library/Businessprocess/Renderer/Renderer.php398
-rw-r--r--library/Businessprocess/Renderer/TileRenderer.php90
-rw-r--r--library/Businessprocess/Renderer/TileRenderer/NodeTile.php357
-rw-r--r--library/Businessprocess/Renderer/TreeRenderer.php357
-rw-r--r--library/Businessprocess/ServiceNode.php84
-rw-r--r--library/Businessprocess/Simulation.php185
-rw-r--r--library/Businessprocess/State/IcingaDbState.php145
-rw-r--r--library/Businessprocess/State/MonitoringState.php151
-rw-r--r--library/Businessprocess/Storage/ConfigDiff.php91
-rw-r--r--library/Businessprocess/Storage/LegacyConfigParser.php409
-rw-r--r--library/Businessprocess/Storage/LegacyConfigRenderer.php255
-rw-r--r--library/Businessprocess/Storage/LegacyStorage.php207
-rw-r--r--library/Businessprocess/Storage/Storage.php107
-rw-r--r--library/Businessprocess/Test/BaseTestCase.php76
-rw-r--r--library/Businessprocess/Test/Bootstrap.php29
-rw-r--r--library/Businessprocess/Web/Component/ActionBar.php15
-rw-r--r--library/Businessprocess/Web/Component/BpDashboardTile.php49
-rw-r--r--library/Businessprocess/Web/Component/Content.php14
-rw-r--r--library/Businessprocess/Web/Component/Controls.php14
-rw-r--r--library/Businessprocess/Web/Component/Dashboard.php126
-rw-r--r--library/Businessprocess/Web/Component/DashboardAction.php26
-rw-r--r--library/Businessprocess/Web/Component/RenderedProcessActionBar.php148
-rw-r--r--library/Businessprocess/Web/Component/Tabs.php9
-rw-r--r--library/Businessprocess/Web/Component/WtfTabs.php22
-rw-r--r--library/Businessprocess/Web/Controller.php269
-rw-r--r--library/Businessprocess/Web/FakeRequest.php26
-rw-r--r--library/Businessprocess/Web/Form/BpConfigBaseForm.php72
-rw-r--r--library/Businessprocess/Web/Form/CsrfToken.php53
-rw-r--r--library/Businessprocess/Web/Form/Element/Checkbox.php8
-rw-r--r--library/Businessprocess/Web/Form/Element/FormElement.php9
-rw-r--r--library/Businessprocess/Web/Form/Element/SimpleNote.php22
-rw-r--r--library/Businessprocess/Web/Form/Element/StateOverrides.php55
-rw-r--r--library/Businessprocess/Web/Form/FormLoader.php37
-rw-r--r--library/Businessprocess/Web/Form/QuickBaseForm.php167
-rw-r--r--library/Businessprocess/Web/Form/QuickForm.php502
-rw-r--r--library/Businessprocess/Web/Form/Validator/NoDuplicateChildrenValidator.php57
-rw-r--r--library/Businessprocess/Web/Url.php26
66 files changed, 9143 insertions, 0 deletions
diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php
new file mode 100644
index 0000000..1e3f119
--- /dev/null
+++ b/library/Businessprocess/BpConfig.php
@@ -0,0 +1,1033 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Exception;
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Businessprocess\Exception\NestingError;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use ipl\Sql\Connection as IcingaDbConnection;
+
+class BpConfig
+{
+ const SOFT_STATE = 0;
+
+ const HARD_STATE = 1;
+
+ /**
+ * Name of the configured monitoring backend
+ *
+ * @var string
+ */
+ protected $backendName;
+
+ /**
+ * Backend to retrieve states from
+ *
+ * @var MonitoringBackend|IcingaDbConnection
+ */
+ protected $backend;
+
+ /**
+ * @var LegacyStorage
+ */
+ protected $storage;
+
+ /** @var Metadata */
+ protected $metadata;
+
+ /**
+ * Business process name
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Business process title
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * State type, soft or hard
+ *
+ * @var int
+ */
+ protected $state_type;
+
+ /**
+ * Warnings, usually filled at process build time
+ *
+ * @var array
+ */
+ protected $warnings = array();
+
+ /**
+ * Errors, usually filled at process build time
+ *
+ * @var array
+ */
+ protected $errors = array();
+
+ /**
+ * All used node objects
+ *
+ * @var array
+ */
+ protected $nodes = array();
+
+ /**
+ * Root node objects
+ *
+ * @var array
+ */
+ protected $root_nodes = array();
+
+ /**
+ * Imported nodes
+ *
+ * @var ImportedNode[]
+ */
+ protected $importedNodes = [];
+
+ /**
+ * Imported configs
+ *
+ * @var BpConfig[]
+ */
+ protected $importedConfigs = [];
+
+ /**
+ * All host names { 'hostA' => true, ... }
+ *
+ * @var array
+ */
+ protected $hosts = array();
+
+ /** @var bool Whether catchable errors should be thrown nonetheless */
+ protected $throwErrors = false;
+
+ protected $loopDetection = array();
+
+ /**
+ * Applied state simulation
+ *
+ * @var Simulation
+ */
+ protected $simulation;
+
+ 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;
+ foreach ($changes->getChanges() as $change) {
+ $cnt++;
+ $change->applyTo($this);
+ }
+ $this->changeCount = $cnt;
+
+ $this->appliedChanges = $changes;
+
+ return $this;
+ }
+
+ /**
+ * Apply a state simulation
+ *
+ * @param Simulation $simulation
+ *
+ * @return $this
+ */
+ public function applySimulation(Simulation $simulation)
+ {
+ $cnt = 0;
+
+ foreach ($simulation->simulations() as $node => $s) {
+ if (! $this->hasNode($node)) {
+ continue;
+ }
+ $cnt++;
+ $this->getNode($node)
+ ->setState($s->state)
+ ->setAck($s->acknowledged)
+ ->setDowntime($s->in_downtime)
+ ->setMissing(false);
+ }
+
+ $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;
+ return $this;
+ }
+
+ public function getTitle()
+ {
+ return $this->getMetadata()->getTitle();
+ }
+
+ public function hasTitle()
+ {
+ return $this->getMetadata()->has('Title');
+ }
+
+ public function getBackendName()
+ {
+ return $this->getMetadata()->get('Backend');
+ }
+
+ public function hasBackendName()
+ {
+ return $this->getMetadata()->has('Backend');
+ }
+
+ public function setBackend($backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+
+ public function getBackend()
+ {
+ if ($this->backend === null) {
+ if (Module::exists('icingadb')
+ && (! $this->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())) {
+ $this->backend = IcingaDbObject::fetchDb();
+ } else {
+ $this->backend = MonitoringBackend::instance(
+ $this->getBackendName()
+ );
+ }
+ }
+
+ return $this->backend;
+ }
+
+ public function hasBackend()
+ {
+ return $this->backend !== null;
+ }
+
+ public function hasBeenChanged()
+ {
+ return false;
+ }
+
+ public function hasSimulations()
+ {
+ return $this->countSimulations() > 0;
+ }
+
+ public function countSimulations()
+ {
+ return $this->simulationCount;
+ }
+
+ public function clearAppliedChanges()
+ {
+ if ($this->appliedChanges !== null) {
+ $this->appliedChanges->clear();
+ }
+ return $this;
+ }
+
+ public function getStateType()
+ {
+ if ($this->state_type === null) {
+ if ($this->getMetadata()->has('Statetype')) {
+ switch ($this->getMetadata()->get('Statetype')) {
+ case 'hard':
+ $this->state_type = self::HARD_STATE;
+ break;
+ case 'soft':
+ $this->state_type = self::SOFT_STATE;
+ break;
+ }
+ } else {
+ $this->state_type = self::HARD_STATE;
+ }
+ }
+
+ return $this->state_type;
+ }
+
+ public function useSoftStates()
+ {
+ $this->state_type = self::SOFT_STATE;
+ return $this;
+ }
+
+ public function useHardStates()
+ {
+ $this->state_type = self::HARD_STATE;
+ return $this;
+ }
+
+ public function usesSoftStates()
+ {
+ return $this->getStateType() === self::SOFT_STATE;
+ }
+
+ public function usesHardStates()
+ {
+ return $this->getStateType() === self::HARD_STATE;
+ }
+
+ public function addRootNode($name)
+ {
+ $this->root_nodes[$name] = $this->getNode($name);
+ return $this;
+ }
+
+ public function removeRootNode($name)
+ {
+ if ($this->isRootNode($name)) {
+ unset($this->root_nodes[$name]);
+ }
+
+ return $this;
+ }
+
+ public function isRootNode($name)
+ {
+ return array_key_exists($name, $this->root_nodes);
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getChildren()
+ {
+ return $this->getRootNodes();
+ }
+
+ /**
+ * @return int
+ */
+ public function countChildren()
+ {
+ return count($this->root_nodes);
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getRootNodes()
+ {
+ if ($this->getMetadata()->isManuallyOrdered()) {
+ uasort($this->root_nodes, function (BpNode $a, BpNode $b) {
+ $a = $a->getDisplay();
+ $b = $b->getDisplay();
+ return $a > $b ? 1 : ($a < $b ? -1 : 0);
+ });
+ } else {
+ ksort($this->root_nodes, SORT_NATURAL | SORT_FLAG_CASE);
+ }
+
+ return $this->root_nodes;
+ }
+
+ public function listRootNodes()
+ {
+ $names = array_keys($this->root_nodes);
+ if ($this->getMetadata()->isManuallyOrdered()) {
+ uasort($names, function ($a, $b) {
+ $a = $this->root_nodes[$a]->getDisplay();
+ $b = $this->root_nodes[$b]->getDisplay();
+ return $a > $b ? 1 : ($a < $b ? -1 : 0);
+ });
+ } else {
+ natcasesort($names);
+ }
+
+ return $names;
+ }
+
+ public function getNodes()
+ {
+ return $this->nodes;
+ }
+
+ public function hasNode($name)
+ {
+ if (array_key_exists($name, $this->nodes)) {
+ return true;
+ } elseif ($name[0] === '@') {
+ list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);
+ return $this->getImportedConfig($configName)->hasNode($nodeName);
+ }
+
+ return false;
+ }
+
+ public function hasRootNode($name)
+ {
+ return array_key_exists($name, $this->root_nodes);
+ }
+
+ public function createService($host, $service)
+ {
+ $node = new ServiceNode(
+ (object) array(
+ 'hostname' => $host,
+ 'service' => $service
+ )
+ );
+ $node->setBpConfig($this);
+ $this->nodes[$host . ';' . $service] = $node;
+ $this->hosts[$host] = true;
+ return $node;
+ }
+
+ public function createHost($host)
+ {
+ $node = new HostNode((object) array('hostname' => $host));
+ $node->setBpConfig($this);
+ $this->nodes[$host . ';Hoststatus'] = $node;
+ $this->hosts[$host] = true;
+ return $node;
+ }
+
+ public function calculateAllStates()
+ {
+ foreach ($this->getRootNodes() as $node) {
+ $node->getState();
+ }
+
+ return $this;
+ }
+
+ public function clearAllStates()
+ {
+ foreach ($this->getBpNodes() as $node) {
+ $node->clearState();
+ }
+
+ return $this;
+ }
+
+ public function listInvolvedHostNames(&$usedConfigs = null)
+ {
+ $hosts = $this->hosts;
+ if (! empty($this->importedNodes)) {
+ $usedConfigs[$this->getName()] = true;
+ foreach ($this->importedNodes as $node) {
+ if (isset($usedConfigs[$node->getConfigName()])) {
+ continue;
+ }
+
+ $hosts += array_flip($node->getBpConfig()->listInvolvedHostNames($usedConfigs));
+ }
+ }
+
+ return array_keys($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((object) array(
+ 'name' => $name,
+ 'operator' => $operator,
+ 'child_names' => array(),
+ ));
+ $node->setBpConfig($this);
+
+ $this->addNode($name, $node);
+ return $node;
+ }
+
+ public function createMissingBp($name)
+ {
+ return $this->createBp($name)->setMissing();
+ }
+
+ public function getMissingChildren()
+ {
+ $missing = array();
+ foreach ($this->getRootNodes() as $root) {
+ $missing += $root->getMissingChildren();
+ }
+
+ return $missing;
+ }
+
+ public function createImportedNode($config, $name = null)
+ {
+ $params = (object) array('configName' => $config);
+ if ($name !== null) {
+ $params->node = $name;
+ }
+
+ $node = new ImportedNode($this, $params);
+ $this->importedNodes[$node->getName()] = $node;
+ $this->nodes[$node->getName()] = $node;
+ return $node;
+ }
+
+ public function getImportedNodes()
+ {
+ return $this->importedNodes;
+ }
+
+ public function getImportedConfig($name)
+ {
+ if (! isset($this->importedConfigs[$name])) {
+ $import = $this->storage()->loadProcess($name);
+
+ if ($this->usesSoftStates()) {
+ $import->useSoftStates();
+ } else {
+ $import->useHardStates();
+ }
+
+ $this->importedConfigs[$name] = $import;
+ }
+
+ return $this->importedConfigs[$name];
+ }
+
+ public function listInvolvedConfigs(&$configs = null)
+ {
+ if ($configs === null) {
+ $configs[$this->getName()] = $this;
+ }
+
+ foreach ($this->importedNodes as $node) {
+ if (! isset($configs[$node->getConfigName()])) {
+ $config = $node->getBpConfig();
+ $configs[$node->getConfigName()] = $config;
+ $config->listInvolvedConfigs($configs);
+ }
+ }
+
+ return $configs;
+ }
+
+ /**
+ * @return LegacyStorage
+ */
+ protected function storage()
+ {
+ if ($this->storage === null) {
+ $this->storage = LegacyStorage::getInstance();
+ }
+
+ return $this->storage;
+ }
+
+ /**
+ * @param string $name
+ * @return Node
+ * @throws Exception
+ */
+ public function getNode($name)
+ {
+ if ($name === '__unbound__') {
+ return $this->getUnboundBaseNode();
+ }
+
+ if (array_key_exists($name, $this->nodes)) {
+ return $this->nodes[$name];
+ }
+
+ if ($name[0] === '@') {
+ list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);
+ return $this->getImportedConfig($configName)->getNode($nodeName);
+ }
+
+ // Fallback: if it is a service, create an empty one:
+ $this->warn(sprintf('The node "%s" doesn\'t exist', $name));
+ $pos = strpos($name, ';');
+ if ($pos !== false) {
+ $host = substr($name, 0, $pos);
+ $service = substr($name, $pos + 1);
+ // TODO: deactivated, this scares me, test it
+ if ($service === 'Hoststatus') {
+ return $this->createHost($host);
+ } else {
+ return $this->createService($host, $service);
+ }
+ }
+
+ throw new Exception(
+ sprintf('The node "%s" doesn\'t exist', $name)
+ );
+ }
+
+ /**
+ * @return BpNode
+ */
+ public function getUnboundBaseNode()
+ {
+ // Hint: state is useless here, but triggers parent/child "calculation"
+ // This is an ugly workaround and should be made obsolete
+ $this->calculateAllStates();
+
+ $names = array_keys($this->getUnboundNodes());
+ $bp = new BpNode((object) array(
+ 'name' => '__unbound__',
+ 'operator' => '&',
+ 'child_names' => $names
+ ));
+ $bp->setBpConfig($this);
+ $bp->setAlias($this->translate('Unbound nodes'));
+ return $bp;
+ }
+
+ /**
+ * @param $name
+ * @return BpNode
+ *
+ * @throws NotFoundError
+ */
+ public function getBpNode($name)
+ {
+ 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;
+ }
+
+ /**
+ * 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(
+ sprintf(
+ mt('businessprocess', 'Node "%s" has been defined twice'),
+ $name
+ )
+ );
+ }
+
+ $this->nodes[$name] = $node;
+
+ if ($node->getDisplay() > 0) {
+ if (! $this->isRootNode($name)) {
+ $this->addRootNode($name);
+ }
+ } else {
+ if ($this->isRootNode($name)) {
+ $this->removeRootNode($name);
+ }
+ }
+
+
+ return $this;
+ }
+
+ /**
+ * 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) {
+ $nodes[$node->getName()] = $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);
+ }
+
+ if ($this->getMetadata()->isManuallyOrdered()) {
+ uasort($nodes, function ($a, $b) {
+ $a = $this->nodes[$a]->getDisplay();
+ $b = $this->nodes[$b]->getDisplay();
+ return $a > $b ? 1 : ($a < $b ? -1 : 0);
+ });
+ } else {
+ natcasesort($nodes);
+ }
+
+ 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->getBpNodes() as $name => $node) {
+ if ($node->hasParents()) {
+ continue;
+ }
+
+ if ($node->getDisplay() === 0) {
+ $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;
+ if ($this->isEmpty()) {
+ $errors[] = sprintf(
+ $this->translate(
+ 'No business process nodes for "%s" have been defined yet'
+ ),
+ $this->getTitle()
+ );
+ }
+ return $errors;
+ }
+
+ /**
+ * 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);
+ $this->warnings[] = vsprintf($msg, $args);
+ }
+
+ /**
+ * @param string $msg,...
+ *
+ * @return $this
+ *
+ * @throws IcingaException
+ */
+ public function addError($msg)
+ {
+ $args = func_get_args();
+ array_shift($args);
+ if (! empty($args)) {
+ $msg = vsprintf($msg, $args);
+ }
+ if ($this->throwErrors) {
+ throw new IcingaException($msg);
+ }
+
+ $this->errors[] = $msg;
+ return $this;
+ }
+
+ /**
+ * Decide whether errors should be thrown or collected
+ *
+ * @param bool $throw
+ *
+ * @return $this
+ */
+ public function throwErrors($throw = true)
+ {
+ $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()
+ {
+ // This is faster
+ if (! empty($this->root_nodes)) {
+ return false;
+ }
+
+ return count($this->listBpNodes()) === 0;
+ }
+
+ /**
+ * Export the config to array
+ *
+ * @param bool $flat If false, children will be added to the array key children, else the array will be flat
+ *
+ * @return array
+ */
+ public function toArray($flat = false)
+ {
+ $data = [
+ 'name' => $this->getTitle(),
+ 'path' => $this->getTitle()
+ ];
+
+ $children = [];
+
+ foreach ($this->getChildren() as $node) {
+ if ($flat) {
+ $children = array_merge($children, $node->toArray($data, $flat));
+ } else {
+ $children[] = $node->toArray($data, $flat);
+ }
+ }
+
+ if ($flat) {
+ $data = [$data];
+
+ if (! empty($children)) {
+ $data = array_merge($data, $children);
+ }
+ } else {
+ $data['children'] = $children;
+ }
+
+ return $data;
+ }
+}
diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php
new file mode 100644
index 0000000..2ea8f8e
--- /dev/null
+++ b/library/Businessprocess/BpNode.php
@@ -0,0 +1,664 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Businessprocess\Exception\NestingError;
+
+class BpNode extends Node
+{
+ const OP_AND = '&';
+ const OP_OR = '|';
+ const OP_NOT = '!';
+ const OP_DEGRADED = '%';
+
+ protected $operator = '&';
+ protected $url;
+ protected $info_command;
+ protected $display = 0;
+
+ /** @var Node[] */
+ protected $children;
+
+ /** @var array */
+ protected $childNames = array();
+ protected $counters;
+ protected $missing = null;
+ protected $empty = null;
+ protected $missingChildren;
+ protected $stateOverrides = [];
+
+ protected static $emptyStateSummary = array(
+ 'CRITICAL' => 0,
+ 'CRITICAL-HANDLED' => 0,
+ 'WARNING' => 0,
+ 'WARNING-HANDLED' => 0,
+ 'UNKNOWN' => 0,
+ 'UNKNOWN-HANDLED' => 0,
+ 'OK' => 0,
+ 'PENDING' => 0,
+ 'MISSING' => 0,
+ );
+
+ protected static $sortStateInversionMap = array(
+ 4 => 0,
+ 3 => 0,
+ 2 => 2,
+ 1 => 1,
+ 0 => 4
+ );
+
+ protected $className = 'process';
+
+ public function __construct($object)
+ {
+ $this->name = $object->name;
+ $this->operator = $object->operator;
+ $this->childNames = $object->child_names;
+ }
+
+ public function getStateSummary()
+ {
+ if ($this->counters === null) {
+ $this->getState();
+ $this->counters = self::$emptyStateSummary;
+
+ foreach ($this->getChildren() as $child) {
+ if ($child->isMissing()) {
+ $this->counters['MISSING']++;
+ } else {
+ $state = $child->getStateName($this->getChildState($child));
+ if ($child->isHandled() && ($state !== 'UP' && $state !== 'OK')) {
+ $state = $state . '-HANDLED';
+ }
+
+ if ($state === 'DOWN') {
+ $this->counters['CRITICAL']++;
+ } elseif ($state === 'DOWN-HANDLED') {
+ $this->counters['CRITICAL-HANDLED']++;
+ } elseif ($state === 'UNREACHABLE') {
+ $this->counters['UNKNOWN']++;
+ } elseif ($state === 'UNREACHABLE-HANDLED') {
+ $this->counters['UNKNOWN-HANDLED']++;
+ } elseif ($state === 'UP') {
+ $this->counters['OK']++;
+ } else {
+ $this->counters[$state]++;
+ }
+ }
+ }
+ }
+ return $this->counters;
+ }
+
+ public function hasProblems()
+ {
+ if ($this->isProblem()) {
+ return true;
+ }
+
+ $okStates = array('OK', 'UP', 'PENDING', 'MISSING');
+
+ foreach ($this->getStateSummary() as $state => $cnt) {
+ if ($cnt !== 0 && ! in_array($state, $okStates)) {
+ return true;
+ }
+ }
+
+ 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;
+ $this->reorderChildren();
+ $node->addParent($this);
+ return $this;
+ }
+
+ public function getProblematicChildren()
+ {
+ $problems = array();
+
+ foreach ($this->getChildren() as $child) {
+ if (isset($this->stateOverrides[$child->getName()])) {
+ $problem = $this->getChildState($child) > 0;
+ } else {
+ $problem = $child->isProblem() || ($child instanceof BpNode && $child->hasProblems());
+ }
+
+ if ($problem) {
+ $problems[] = $child;
+ }
+ }
+
+ return $problems;
+ }
+
+ public function hasChild($name)
+ {
+ return in_array($name, $this->getChildNames());
+ }
+
+ public function removeChild($name)
+ {
+ if (($key = array_search($name, $this->getChildNames())) !== false) {
+ unset($this->childNames[$key]);
+
+ if (! empty($this->children)) {
+ unset($this->children[$name]);
+ }
+ }
+
+ return $this;
+ }
+
+ public function getProblemTree()
+ {
+ $tree = array();
+
+ foreach ($this->getProblematicChildren() as $child) {
+ $name = $child->getName();
+ $tree[$name] = array(
+ 'node' => $child,
+ 'children' => array()
+ );
+ if ($child instanceof BpNode) {
+ $tree[$name]['children'] = $child->getProblemTree();
+ }
+ }
+
+ return $tree;
+ }
+
+ /**
+ * Get the problem nodes as tree reduced to the nodes which have the same state as the business process
+ *
+ * @param bool $rootCause Reduce nodes to the nodes which are responsible for the state of the business process
+ *
+ * @return array
+ */
+ public function getProblemTreeBlame($rootCause = false)
+ {
+ $tree = [];
+ $nodeState = $this->getState();
+
+ if ($nodeState !== 0) {
+ foreach ($this->getChildren() as $child) {
+ $childState = $this->getChildState($child);
+ $childState = $rootCause ? $child->getSortingState($childState) : $childState;
+ if (($rootCause ? $this->getSortingState() : $nodeState) === $childState) {
+ $name = $child->getName();
+ $tree[$name] = [
+ 'children' => [],
+ 'node' => $child
+ ];
+ if ($child instanceof BpNode) {
+ $tree[$name]['children'] = $child->getProblemTreeBlame($rootCause);
+ }
+ }
+ }
+ }
+
+ return $tree;
+ }
+
+
+ public function isMissing()
+ {
+ if ($this->missing === null) {
+ $exists = false;
+ $bp = $this->getBpConfig();
+ $bp->beginLoopDetection($this->name);
+ foreach ($this->getChildren() as $child) {
+ if (! $child->isMissing()) {
+ $exists = true;
+ }
+ }
+ $bp->endLoopDetection($this->name);
+ $this->missing = ! $exists && ! empty($this->getChildren());
+ }
+ return $this->missing;
+ }
+
+ public function isEmpty()
+ {
+ $bp = $this->getBpConfig();
+ $empty = true;
+ if ($this->countChildren()) {
+ $bp->beginLoopDetection($this->name);
+ foreach ($this->getChildren() as $child) {
+ if ($child instanceof MonitoredNode) {
+ $empty = false;
+ break;
+ } elseif (!$child->isEmpty()) {
+ $empty = false;
+ }
+ }
+ $bp->endLoopDetection($this->name);
+ }
+ $this->empty = $empty;
+
+ return $this->empty;
+ }
+
+
+ public function getMissingChildren()
+ {
+ if ($this->missingChildren === null) {
+ $missing = array();
+
+ foreach ($this->getChildren() as $child) {
+ if ($child->isMissing()) {
+ $missing[$child->getName()] = $child;
+ }
+
+ foreach ($child->getMissingChildren() as $m) {
+ $missing[$m->getName()] = $m;
+ }
+ }
+
+ $this->missingChildren = $missing;
+ }
+
+ return $this->missingChildren;
+ }
+
+ public function getOperator()
+ {
+ return $this->operator;
+ }
+
+ public function setOperator($operator)
+ {
+ $this->assertValidOperator($operator);
+ $this->operator = $operator;
+ return $this;
+ }
+
+ protected function assertValidOperator($operator)
+ {
+ switch ($operator) {
+ case self::OP_AND:
+ case self::OP_OR:
+ case self::OP_NOT:
+ case self::OP_DEGRADED:
+ return;
+ default:
+ if (is_numeric($operator)) {
+ return;
+ }
+ }
+
+ throw new ConfigurationError(
+ 'Got invalid operator: %s',
+ $operator
+ );
+ }
+
+ public function setInfoUrl($url)
+ {
+ $this->url = $url;
+ return $this;
+ }
+
+ public function hasInfoUrl()
+ {
+ return ! empty($this->url);
+ }
+
+ public function getInfoUrl()
+ {
+ return $this->url;
+ }
+
+ public function setInfoCommand($cmd)
+ {
+ $this->info_command = $cmd;
+ }
+
+ public function hasInfoCommand()
+ {
+ return $this->info_command !== null;
+ }
+
+ public function getInfoCommand()
+ {
+ return $this->info_command;
+ }
+
+ public function setStateOverrides(array $overrides, $name = null)
+ {
+ if ($name === null) {
+ $this->stateOverrides = $overrides;
+ } else {
+ $this->stateOverrides[$name] = $overrides;
+ }
+
+ return $this;
+ }
+
+ public function getStateOverrides($name = null)
+ {
+ $overrides = null;
+ if ($name !== null) {
+ if (isset($this->stateOverrides[$name])) {
+ $overrides = $this->stateOverrides[$name];
+ }
+ } else {
+ $overrides = $this->stateOverrides;
+ }
+
+ return $overrides;
+ }
+
+ public function getAlias()
+ {
+ return $this->alias ? preg_replace('~_~', ' ', $this->alias) : $this->name;
+ }
+
+ /**
+ * @return int
+ */
+ public function getState()
+ {
+ if ($this->state === null) {
+ try {
+ $this->reCalculateState();
+ } catch (NestingError $e) {
+ $this->getBpConfig()->addError(
+ $this->getBpConfig()->translate('Nesting error detected: %s'),
+ $e->getMessage()
+ );
+
+ // Failing nodes are unknown
+ $this->state = 3;
+ }
+ }
+
+ return $this->state;
+ }
+
+ /**
+ * Get the given child's state, possibly adjusted by override rules
+ *
+ * @param Node|string $child
+ * @return int
+ */
+ public function getChildState($child)
+ {
+ if (! $child instanceof Node) {
+ $child = $this->getChildByName($child);
+ }
+
+ $childName = $child->getName();
+ $childState = $child->getState();
+ if (! isset($this->stateOverrides[$childName][$childState])) {
+ return $childState;
+ }
+
+ return $this->stateOverrides[$childName][$childState];
+ }
+
+ public function getHtmlId()
+ {
+ return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', $this->getName());
+ }
+
+ protected function invertSortingState($state)
+ {
+ return self::$sortStateInversionMap[$state >> self::SHIFT_FLAGS] << self::SHIFT_FLAGS;
+ }
+
+ /**
+ * @return $this
+ */
+ public function reCalculateState()
+ {
+ $bp = $this->getBpConfig();
+
+ $sort_states = array();
+ $lastStateChange = 0;
+
+ if ($this->isEmpty()) {
+ // TODO: delegate this to operators, should mostly fail
+ $this->setState(self::NODE_EMPTY);
+ return $this;
+ }
+
+ foreach ($this->getChildren() as $child) {
+ $bp->beginLoopDetection($this->name);
+ if ($child instanceof MonitoredNode && $child->isMissing()) {
+ if ($child instanceof HostNode) {
+ $child->setState(self::ICINGA_UNREACHABLE);
+ } else {
+ $child->setState(self::ICINGA_UNKNOWN);
+ }
+
+ $child->setMissing();
+ }
+ $sort_states[] = $child->getSortingState($this->getChildState($child));
+ $lastStateChange = max($lastStateChange, $child->getLastStateChange());
+ $bp->endLoopDetection($this->name);
+ }
+
+ $this->setLastStateChange($lastStateChange);
+
+ switch ($this->getOperator()) {
+ case self::OP_AND:
+ $sort_state = max($sort_states);
+ break;
+ case self::OP_NOT:
+ $sort_state = $this->invertSortingState(max($sort_states));
+ break;
+ case self::OP_OR:
+ $sort_state = min($sort_states);
+ break;
+ case self::OP_DEGRADED:
+ $maxState = max($sort_states);
+ $flags = $maxState & 0xf;
+
+ $maxIcingaState = $this->sortStateTostate($maxState);
+ $warningState = ($this->stateToSortState(self::ICINGA_WARNING) << self::SHIFT_FLAGS) + $flags;
+
+ $sort_state = ($maxIcingaState === self::ICINGA_CRITICAL) ? $warningState : $maxState;
+ break;
+ default:
+ // MIN:
+ $sort_state = 3 << self::SHIFT_FLAGS;
+
+ if (count($sort_states) >= $this->operator) {
+ $actualGood = 0;
+ foreach ($sort_states as $s) {
+ if (($s >> self::SHIFT_FLAGS) === self::ICINGA_OK) {
+ $actualGood++;
+ }
+ }
+
+ if ($actualGood >= $this->operator) {
+ // condition is fulfilled
+ $sort_state = self::ICINGA_OK;
+ } else {
+ // worst state if not fulfilled
+ $sort_state = max($sort_states);
+ }
+ }
+ }
+ if ($sort_state & self::FLAG_DOWNTIME) {
+ $this->setDowntime(true);
+ }
+ if ($sort_state & self::FLAG_ACK) {
+ $this->setAck(true);
+ }
+
+ $this->state = $this->sortStateTostate($sort_state);
+ return $this;
+ }
+
+ public function checkForLoops()
+ {
+ $bp = $this->getBpConfig();
+ 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)
+ {
+ $this->display = (int) $display;
+ return $this;
+ }
+
+ public function getDisplay()
+ {
+ return $this->display;
+ }
+
+ public function setChildNames($names)
+ {
+ $this->childNames = $names;
+ $this->children = null;
+ $this->reorderChildren();
+ return $this;
+ }
+
+ public function hasChildren($filter = null)
+ {
+ $childNames = $this->getChildNames();
+ return !empty($childNames);
+ }
+
+ public function getChildNames()
+ {
+ return $this->childNames;
+ }
+
+ public function getChildren($filter = null)
+ {
+ if ($this->children === null) {
+ $this->children = [];
+ $this->reorderChildren();
+ foreach ($this->getChildNames() as $name) {
+ $this->children[$name] = $this->getBpConfig()->getNode($name);
+ $this->children[$name]->addParent($this);
+ }
+ }
+
+ return $this->children;
+ }
+
+ /**
+ * Reorder this node's children, in case manual order is not applied
+ */
+ protected function reorderChildren()
+ {
+ if ($this->getBpConfig()->getMetadata()->isManuallyOrdered()) {
+ return;
+ }
+
+ $childNames = $this->getChildNames();
+ natcasesort($childNames);
+ $this->childNames = array_values($childNames);
+
+ if (! empty($this->children)) {
+ $children = [];
+ foreach ($this->childNames as $name) {
+ $children[$name] = $this->children[$name];
+ }
+
+ $this->children = $children;
+ }
+ }
+
+ /**
+ * return BpNode[]
+ */
+ public function getChildBpNodes()
+ {
+ $children = array();
+
+ foreach ($this->getChildren() as $name => $child) {
+ if ($child instanceof BpNode) {
+ $children[$name] = $child;
+ }
+ }
+
+ return $children;
+ }
+
+ /**
+ * @param $childName
+ * @return Node
+ * @throws NotFoundError
+ */
+ public function getChildByName($childName)
+ {
+ foreach ($this->getChildren() as $name => $child) {
+ if ($name === $childName) {
+ return $child;
+ }
+ }
+
+ throw new NotFoundError('Trying to get missing child %s', $childName);
+ }
+
+ protected function assertNumericOperator()
+ {
+ if (! is_numeric($this->getOperator())) {
+ throw new ConfigurationError('Got invalid operator: %s', $this->operator);
+ }
+ }
+
+ public function operatorHtml()
+ {
+ switch ($this->getOperator()) {
+ case self::OP_AND:
+ return 'AND';
+ break;
+ case self::OP_OR:
+ return 'OR';
+ break;
+ case self::OP_NOT:
+ return 'NOT';
+ break;
+ case self::OP_DEGRADED:
+ return 'DEG';
+ break;
+ default:
+ // MIN
+ $this->assertNumericOperator();
+ return 'min:' . $this->operator;
+ }
+ }
+
+ public function getIcon()
+ {
+ $this->icon = $this->hasParents() ? 'cubes' : 'sitemap';
+ return parent::getIcon();
+ }
+}
diff --git a/library/Businessprocess/Common/EnumList.php b/library/Businessprocess/Common/EnumList.php
new file mode 100644
index 0000000..a1e5b56
--- /dev/null
+++ b/library/Businessprocess/Common/EnumList.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Common;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Businessprocess\IcingaDbObject;
+use Icinga\Module\Businessprocess\MonitoringRestrictions;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+
+trait EnumList
+{
+ protected function enumHostForServiceList()
+ {
+ if ($this->useIcingaDbBackend()) {
+ $names = (new IcingaDbObject())->yieldHostnames();
+ } else {
+ $names = $this->backend
+ ->select()
+ ->from('hostStatus', ['hostname' => 'host_name'])
+ ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
+ ->order('host_name')
+ ->getQuery()
+ ->fetchColumn();
+ }
+
+ // fetchPairs doesn't seem to work when using the same column with
+ // different aliases twice
+ $res = array();
+ foreach ($names as $name) {
+ $res[$name] = $name;
+ }
+
+ return $res;
+ }
+
+ protected function enumHostList()
+ {
+ if ($this->useIcingaDbBackend()) {
+ $names = (new IcingaDbObject())->yieldHostnames();
+ } else {
+ $names = $this->backend
+ ->select()
+ ->from('hostStatus', ['hostname' => 'host_name'])
+ ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
+ ->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)
+ {
+ if ($this->useIcingaDbBackend()) {
+ $names = (new IcingaDbObject())->yieldServicenames($host);
+ } else {
+ $names = $this->backend
+ ->select()
+ ->from('serviceStatus', ['service' => 'service_description'])
+ ->where('host_name', $host)
+ ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
+ ->order('service_description')
+ ->getQuery()
+ ->fetchColumn();
+ }
+
+ $services = array();
+ foreach ($names as $name) {
+ $services[$host . ';' . $name] = $name;
+ }
+
+ return $services;
+ }
+
+ protected function enumHostListByFilter($filter)
+ {
+ if ($this->useIcingaDbBackend()) {
+ $names = (new IcingaDbObject())->yieldHostnames($filter);
+ } else {
+ $names = $this->backend
+ ->select()
+ ->from('hostStatus', ['hostname' => 'host_name'])
+ ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
+ ->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 enumServiceListByFilter($filter)
+ {
+ $services = array();
+
+ if ($this->useIcingaDbBackend()) {
+ $objects = (new IcingaDbObject())->fetchServices($filter);
+ foreach ($objects as $object) {
+ $services[$object->host->name . ';' . $object->name] = $object->host->name . ':' . $object->name;
+ }
+ } else {
+ $objects = $this->backend
+ ->select()
+ ->from('serviceStatus', ['host' => 'host_name', 'service' => 'service_description'])
+ ->applyFilter(Filter::fromQueryString($filter))
+ ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
+ ->order('service_description')
+ ->getQuery()
+ ->fetchAll();
+ foreach ($objects as $object) {
+ $services[$object->host . ';' . $object->service] = $object->host . ':' . $object->service;
+ }
+ }
+
+ return $services;
+ }
+
+ protected function enumHostStateList()
+ {
+ $hostStateList = [
+ 0 => $this->translate('UP'),
+ 1 => $this->translate('DOWN'),
+ 99 => $this->translate('PENDING')
+ ];
+
+ return $hostStateList;
+ }
+
+ protected function enumServiceStateList()
+ {
+ $serviceStateList = [
+ 0 => $this->translate('OK'),
+ 1 => $this->translate('WARNING'),
+ 2 => $this->translate('CRITICAL'),
+ 3 => $this->translate('UNKNOWN'),
+ 99 => $this->translate('PENDING'),
+ ];
+
+ return $serviceStateList;
+ }
+
+ protected function useIcingaDbBackend()
+ {
+ if (Module::exists('icingadb')) {
+ return ! $this->bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend();
+ }
+
+ return false;
+ }
+}
diff --git a/library/Businessprocess/Director/ShipConfigFiles.php b/library/Businessprocess/Director/ShipConfigFiles.php
new file mode 100644
index 0000000..17b9e1f
--- /dev/null
+++ b/library/Businessprocess/Director/ShipConfigFiles.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Director;
+
+use Icinga\Module\Director\Hook\ShipConfigFilesHook;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+class ShipConfigFiles extends ShipConfigFilesHook
+{
+ public function fetchFiles()
+ {
+ $files = array();
+
+ $storage = LegacyStorage::getInstance();
+
+ foreach ($storage->listProcesses() as $name => $title) {
+ $files['processes/' . $name . '.bp'] = $storage->getSource($name);
+ }
+
+ return $files;
+ }
+}
diff --git a/library/Businessprocess/Exception/ModificationError.php b/library/Businessprocess/Exception/ModificationError.php
new file mode 100644
index 0000000..430d513
--- /dev/null
+++ b/library/Businessprocess/Exception/ModificationError.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Exception;
+
+use Icinga\Exception\IcingaException;
+
+class ModificationError extends IcingaException
+{
+}
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 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Exception;
+
+use Icinga\Exception\IcingaException;
+
+class NestingError extends IcingaException
+{
+}
diff --git a/library/Businessprocess/Form.php b/library/Businessprocess/Form.php
new file mode 100644
index 0000000..3270b38
--- /dev/null
+++ b/library/Businessprocess/Form.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Web\Request;
+use Icinga\Web\Form as WebForm;
+
+class Form extends WebForm
+{
+ public function __construct($options = null)
+ {
+ parent::__construct($options);
+ $this->setup();
+ }
+
+ public function addHidden($name, $value = null)
+ {
+ $this->addElement('hidden', $name);
+ $this->getElement($name)->setDecorators(array('ViewHelper'));
+ if ($value !== null) {
+ $this->setDefault($name, $value);
+ }
+ return $this;
+ }
+
+ public function handleRequest(Request $request = null)
+ {
+ parent::handleRequest();
+ return $this;
+ }
+
+ public static function construct()
+ {
+ return new static;
+ }
+}
diff --git a/library/Businessprocess/HostNode.php b/library/Businessprocess/HostNode.php
new file mode 100644
index 0000000..b66f66f
--- /dev/null
+++ b/library/Businessprocess/HostNode.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Module\Businessprocess\Web\Url;
+
+class HostNode extends MonitoredNode
+{
+ protected $sortStateToStateMap = array(
+ 4 => self::ICINGA_DOWN,
+ 3 => self::ICINGA_UNREACHABLE,
+ 1 => self::ICINGA_PENDING,
+ 0 => self::ICINGA_UP
+ );
+
+ protected $stateToSortStateMap = array(
+ self::ICINGA_PENDING => 1,
+ self::ICINGA_UNREACHABLE => 3,
+ self::ICINGA_DOWN => 4,
+ self::ICINGA_UP => 0,
+ );
+
+ protected $stateNames = array(
+ 'UP',
+ 'DOWN',
+ 'UNREACHABLE',
+ 99 => 'PENDING'
+ );
+
+ protected $hostname;
+
+ protected $className = 'host';
+
+ protected $icon = 'host';
+
+ public function __construct($object)
+ {
+ $this->name = $object->hostname . ';Hoststatus';
+ $this->hostname = $object->hostname;
+ if (isset($object->state)) {
+ $this->setState($object->state);
+ } else {
+ $this->setState(0)->setMissing();
+ }
+ }
+
+ public function getHostname()
+ {
+ return $this->hostname;
+ }
+
+ public function getUrl()
+ {
+ $params = array(
+ 'host' => $this->getHostname(),
+ );
+
+ if ($this->getBpConfig()->hasBackendName()) {
+ $params['backend'] = $this->getBpConfig()->getBackendName();
+ }
+
+ return Url::fromPath('businessprocess/host/show', $params);
+ }
+}
diff --git a/library/Businessprocess/IcingaDbObject.php b/library/Businessprocess/IcingaDbObject.php
new file mode 100644
index 0000000..cad459f
--- /dev/null
+++ b/library/Businessprocess/IcingaDbObject.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database as IcingadbDatabase;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Sql\Connection as IcingaDbConnection;
+use ipl\Web\Filter\QueryString;
+
+class IcingaDbObject
+{
+ use IcingadbDatabase;
+
+ use Auth;
+
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var IcingaDbConnection */
+ protected $conn;
+
+ public function __construct()
+ {
+ $this->conn = $this->getDb();
+ }
+
+ public function fetchHosts($filter = null)
+ {
+
+ $hosts = Host::on($this->conn);
+
+ if ($filter !== null) {
+ $filterQuery = QueryString::parse($filter);
+
+ $hosts->filter($filterQuery);
+ }
+
+ $hosts->orderBy('host.name');
+
+ $this->applyIcingaDbRestrictions($hosts);
+
+ return $hosts;
+ }
+
+ public function fetchServices($filter)
+ {
+ $services = Service::on($this->conn)
+ ->with('host');
+
+ if ($filter !== null) {
+ $filterQuery = QueryString::parse($filter);
+
+ $services->filter($filterQuery);
+ }
+
+ $services->orderBy('service.name');
+
+ $this->applyIcingaDbRestrictions($services);
+
+ return $services;
+ }
+
+ public function yieldHostnames($filter = null)
+ {
+ foreach ($this->fetchHosts($filter) as $host) {
+ yield $host->name;
+ }
+ }
+
+ public function yieldServicenames($host)
+ {
+ $filter = "host.name=$host";
+
+ foreach ($this->fetchServices($filter) as $service) {
+ yield $service->name;
+ }
+ }
+
+ public static function applyIcingaDbRestrictions($query)
+ {
+ $object = new self;
+ $object->applyRestrictions($query);
+
+ return $object;
+ }
+
+ public static function fetchDb()
+ {
+ $object = new self;
+ return $object->getDb();
+ }
+}
diff --git a/library/Businessprocess/ImportedNode.php b/library/Businessprocess/ImportedNode.php
new file mode 100644
index 0000000..3f0b460
--- /dev/null
+++ b/library/Businessprocess/ImportedNode.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Exception;
+
+class ImportedNode extends BpNode
+{
+ /** @var BpConfig */
+ protected $parentBp;
+
+ /** @var string */
+ protected $configName;
+
+ /** @var string */
+ protected $nodeName;
+
+ /** @var BpNode */
+ protected $importedNode;
+
+ /** @var string */
+ protected $className = 'process subtree';
+
+ /** @var string */
+ protected $icon = 'download';
+
+ public function __construct(BpConfig $parentBp, $object)
+ {
+ $this->parentBp = $parentBp;
+ $this->configName = $object->configName;
+ $this->nodeName = $object->node;
+
+ parent::__construct((object) [
+ 'name' => '@' . $this->configName . ':' . $this->nodeName,
+ 'operator' => null,
+ 'child_names' => null
+ ]);
+ }
+
+ /**
+ * @return string
+ */
+ public function getConfigName()
+ {
+ return $this->configName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getNodeName()
+ {
+ return $this->nodeName;
+ }
+
+ public function getIdentifier()
+ {
+ return $this->getName();
+ }
+
+ public function getBpConfig()
+ {
+ if ($this->bp === null) {
+ $this->bp = $this->parentBp->getImportedConfig($this->configName);
+ }
+
+ return $this->bp;
+ }
+
+ public function getAlias()
+ {
+ if ($this->alias === null) {
+ $this->alias = $this->importedNode()->getAlias();
+ }
+
+ return $this->alias;
+ }
+
+ public function getOperator()
+ {
+ if ($this->operator === null) {
+ $this->operator = $this->importedNode()->getOperator();
+ }
+
+ return $this->operator;
+ }
+
+ public function getChildNames()
+ {
+ if ($this->childNames === null) {
+ $this->childNames = $this->importedNode()->getChildNames();
+ }
+
+ return $this->childNames;
+ }
+
+ /**
+ * @return BpNode
+ */
+ protected function importedNode()
+ {
+ if ($this->importedNode === null) {
+ try {
+ $this->importedNode = $this->getBpConfig()->getBpNode($this->nodeName);
+ } catch (Exception $e) {
+ return $this->createFailedNode($e);
+ }
+ }
+
+ return $this->importedNode;
+ }
+
+ /**
+ * @param Exception $e
+ *
+ * @return BpNode
+ */
+ protected function createFailedNode(Exception $e)
+ {
+ $this->parentBp->addError($e->getMessage());
+ $node = new BpNode((object) array(
+ 'name' => $this->getName(),
+ 'operator' => '&',
+ 'child_names' => []
+ ));
+ $node->setBpConfig($this->getBpConfig());
+ $node->setState(2);
+ $node->setMissing(false)
+ ->setDowntime(false)
+ ->setAck(false)
+ ->setAlias($e->getMessage());
+
+ return $node;
+ }
+}
diff --git a/library/Businessprocess/Metadata.php b/library/Businessprocess/Metadata.php
new file mode 100644
index 0000000..b640fb8
--- /dev/null
+++ b/library/Businessprocess/Metadata.php
@@ -0,0 +1,264 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\ProgrammingError;
+use Icinga\User;
+
+class Metadata
+{
+ /** @var string Configuration name */
+ protected $name;
+
+ protected $properties = array(
+ 'Title' => null,
+ 'Description' => null,
+ 'Owner' => null,
+ 'AllowedUsers' => null,
+ 'AllowedGroups' => null,
+ 'AllowedRoles' => null,
+ 'AddToMenu' => null,
+ 'Backend' => null,
+ 'Statetype' => null,
+ 'ManualOrder' => 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;
+ }
+
+ $prefixes = $auth->getRestrictions('businessprocess/prefix');
+ if (! empty($prefixes)) {
+ if (! $this->nameIsPrefixedWithOneOf($prefixes)) {
+ return false;
+ }
+ }
+
+ if (! $this->hasRestrictions()) {
+ return true;
+ }
+
+ if (! $auth->isAuthenticated()) {
+ return false;
+ }
+
+ return $this->userCanRead($auth->getUser());
+ }
+
+ public function nameIsPrefixedWithOneOf(array $prefixes)
+ {
+ foreach ($prefixes as $prefix) {
+ if (substr($this->name, 0, strlen($prefix)) === $prefix) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ 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 $group) {
+ if ($user->isMemberOf($group)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function hasOneOfTheAllowedRoles(User $user)
+ {
+ foreach ($this->listAllowedRoles() as $roleName) {
+ foreach ($user->getRoles() as $role) {
+ if ($role->getName() === $roleName) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public function isManuallyOrdered()
+ {
+ return $this->get('ManualOrder') === 'yes';
+ }
+
+ 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..369c3a2
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeAction.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Exception\ModificationError;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Abstract NodeAction class
+ *
+ * Every instance of a NodeAction represents a single applied change. Changes are pushed to
+ * a stack and consumed from there. When persisted, NodeActions are serialized with their name,
+ * node name and optionally additional properties according preserveProperties. For each property
+ * that should be preserved, getter and setter methods have to be defined.
+ *
+ * @package Icinga\Module\Businessprocess
+ */
+abstract class NodeAction
+{
+ /** @var string Name of this action (currently create, modify, remove) */
+ protected $actionName;
+
+ /** @var string Name of the node this action applies to */
+ protected $nodeName;
+
+ /** @var array Properties which should be preserved when serializing this action */
+ protected $preserveProperties = array();
+
+ /**
+ * NodeAction constructor.
+ *
+ * @param Node|string $node
+ */
+ public function __construct($node = null)
+ {
+ if ($node !== null) {
+ $this->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 can be applied to a BusinessProcess
+ *
+ * @param BpConfig $config
+ *
+ * @throws ModificationError
+ *
+ * @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;
+ }
+
+ /**
+ * Throw a ModificationError
+ *
+ * @param string $msg
+ * @param mixed ...
+ *
+ * @throws ModificationError
+ */
+ protected function error($msg)
+ {
+ $error = ModificationError::create(func_get_args());
+ /** @var ModificationError $error */
+ throw $error;
+ }
+
+ /**
+ * 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, JSON_FORCE_OBJECT);
+ $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..5d5ab29
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeAddChildrenAction.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+
+class NodeAddChildrenAction extends NodeAction
+{
+ protected $children = array();
+
+ protected $preserveProperties = array('children');
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ if (! $config->hasBpNode($name)) {
+ $this->error('Process "%s" not found', $name);
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $node = $config->getBpNode($this->getNodeName());
+
+ foreach ($this->children as $name) {
+ if (! $config->hasNode($name) || $config->getNode($name)->getBpConfig()->getName() !== $config->getName()) {
+ if (strpos($name, ';') !== false) {
+ list($host, $service) = preg_split('/;/', $name, 2);
+
+ if ($service === 'Hoststatus') {
+ $config->createHost($host);
+ } else {
+ $config->createService($host, $service);
+ }
+ } elseif ($name[0] === '@' && strpos($name, ':') !== false) {
+ list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);
+ $config->createImportedNode($configName, $nodeName);
+ }
+ }
+ $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/NodeApplyManualOrderAction.php b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php
new file mode 100644
index 0000000..9be77e9
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+
+class NodeApplyManualOrderAction extends NodeAction
+{
+ public function appliesTo(BpConfig $config)
+ {
+ return $config->getMetadata()->get('ManualOrder') !== 'yes';
+ }
+
+ public function applyTo(BpConfig $config)
+ {
+ $i = 0;
+ foreach ($config->getBpNodes() as $name => $node) {
+ if ($node->getDisplay() > 0) {
+ $node->setDisplay(++$i);
+ }
+
+ if ($node->hasChildren()) {
+ $node->setChildNames($node->getChildNames());
+ }
+ }
+
+ $config->getMetadata()->set('ManualOrder', 'yes');
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeCopyAction.php b/library/Businessprocess/Modification/NodeCopyAction.php
new file mode 100644
index 0000000..609d704
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeCopyAction.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+
+class NodeCopyAction extends NodeAction
+{
+ /**
+ * @param BpConfig $config
+ * @return bool
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ if (! $config->hasBpNode($name)) {
+ $this->error('Process "%s" not found', $name);
+ }
+
+ if ($config->hasRootNode($name)) {
+ $this->error('A toplevel node with name "%s" already exists', $name);
+ }
+
+ return true;
+ }
+
+ /**
+ * @param BpConfig $config
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ $rootNodes = $config->getRootNodes();
+ $config->addRootNode($name)
+ ->getBpNode($name)
+ ->setDisplay(end($rootNodes)->getDisplay() + 1);
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeCreateAction.php b/library/Businessprocess/Modification/NodeCreateAction.php
new file mode 100644
index 0000000..167d3bc
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeCreateAction.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Node;
+
+class NodeCreateAction extends NodeAction
+{
+ /** @var string */
+ protected $parentName;
+
+ /** @var array */
+ protected $properties = array();
+
+ /** @var array */
+ protected $preserveProperties = array('parentName', 'properties');
+
+ /**
+ * @param Node $name
+ */
+ public function setParent(Node $name)
+ {
+ $this->parentName = $name->getName();
+ }
+
+ /**
+ * @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)
+ {
+ $name = $this->getNodeName();
+ if ($config->hasNode($name)) {
+ $this->error('A node with name "%s" already exists', $name);
+ }
+
+ $parent = $this->getParentName();
+ if ($parent !== null && !$config->hasBpNode($parent)) {
+ $this->error('Parent process "%s" missing', $parent);
+ }
+
+ return true;
+ }
+
+ /**
+ * @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((object) $properties);
+ $node->setBpConfig($config);
+
+ foreach ($this->getProperties() as $key => $val) {
+ if ($key === 'parentName') {
+ $config->getBpNode($val)->addChild($node);
+ continue;
+ }
+ $func = 'set' . ucfirst($key);
+ $node->$func($val);
+ }
+
+ if ($node->getDisplay() > 1) {
+ $i = $node->getDisplay();
+ foreach ($config->getRootNodes() as $_ => $rootNode) {
+ if ($rootNode->getDisplay() >= $node->getDisplay()) {
+ $rootNode->setDisplay(++$i);
+ }
+ }
+ }
+
+ $config->addNode($name, $node);
+
+ return $node;
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeModifyAction.php b/library/Businessprocess/Modification/NodeModifyAction.php
new file mode 100644
index 0000000..1b33094
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeModifyAction.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Node;
+
+class NodeModifyAction extends NodeAction
+{
+ protected $properties = array();
+
+ protected $formerProperties = array();
+
+ protected $preserveProperties = array('formerProperties', 'properties');
+
+ /**
+ * Set properties for a specific node
+ *
+ * Can be called multiple times
+ *
+ * @param Node $node
+ * @param array $properties
+ *
+ * @return $this
+ */
+ public function setNodeProperties(Node $node, array $properties)
+ {
+ foreach (array_keys($properties) as $key) {
+ $this->properties[$key] = $properties[$key];
+
+ if (array_key_exists($key, $this->formerProperties)) {
+ continue;
+ }
+
+ $func = 'get' . ucfirst($key);
+ $this->formerProperties[$key] = $node->$func();
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ if (! $config->hasNode($name)) {
+ $this->error('Node "%s" not found', $name);
+ }
+
+ $node = $config->getNode($name);
+
+ foreach ($this->properties as $key => $val) {
+ $currentVal = $node->{'get' . ucfirst($key)}();
+ if ($this->formerProperties[$key] !== $currentVal) {
+ $this->error(
+ 'Property %s of node "%s" changed its value from "%s" to "%s"',
+ $key,
+ $name,
+ $this->formerProperties[$key],
+ $currentVal
+ );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $node = $config->getNode($this->getNodeName());
+
+ foreach ($this->properties as $key => $val) {
+ $func = 'set' . ucfirst($key);
+ $node->$func($val);
+ }
+
+ 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/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php
new file mode 100644
index 0000000..5754717
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeMoveAction.php
@@ -0,0 +1,212 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+
+class NodeMoveAction extends NodeAction
+{
+ /**
+ * @var string
+ */
+ protected $parent;
+
+ /**
+ * @var string
+ */
+ protected $newParent;
+
+ /**
+ * @var int
+ */
+ protected $from;
+
+ /**
+ * @var int
+ */
+ protected $to;
+
+ protected $preserveProperties = ['parent', 'newParent', 'from', 'to'];
+
+ public function setParent($name)
+ {
+ $this->parent = $name;
+ }
+
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ public function setNewParent($name)
+ {
+ $this->newParent = $name;
+ }
+
+ public function getNewParent()
+ {
+ return $this->newParent;
+ }
+
+ public function setFrom($from)
+ {
+ $this->from = (int) $from;
+ }
+
+ public function getFrom()
+ {
+ return $this->from;
+ }
+
+ public function setTo($to)
+ {
+ $this->to = (int) $to;
+ }
+
+ public function getTo()
+ {
+ return $this->to;
+ }
+
+ public function appliesTo(BpConfig $config)
+ {
+ if (! $config->getMetadata()->isManuallyOrdered()) {
+ $this->error('Process configuration is not manually ordered yet');
+ }
+
+ $name = $this->getNodeName();
+ if ($this->parent !== null) {
+ if (! $config->hasBpNode($this->parent)) {
+ $this->error('Parent process "%s" missing', $this->parent);
+ }
+ $parent = $config->getBpNode($this->parent);
+ if (! $parent->hasChild($name)) {
+ $this->error('Node "%s" not found in process "%s"', $name, $this->parent);
+ }
+
+ $nodes = $parent->getChildNames();
+ if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) {
+ $this->error('Node "%s" not found at position %d', $name, $this->from);
+ }
+ } else {
+ if (! $config->hasRootNode($name)) {
+ $this->error('Toplevel process "%s" not found', $name);
+ }
+
+ $nodes = $config->listRootNodes();
+ if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) {
+ $this->error('Toplevel process "%s" not found at position %d', $name, $this->from);
+ }
+ }
+
+ if ($this->parent !== $this->newParent) {
+ if ($this->newParent !== null) {
+ if (! $config->hasBpNode($this->newParent)) {
+ $this->error('New parent process "%s" missing', $this->newParent);
+ } elseif ($config->getBpNode($this->newParent)->hasChild($name)) {
+ $this->error(
+ 'New parent process "%s" already has a node with the name "%s"',
+ $this->newParent,
+ $name
+ );
+ }
+
+ $childrenCount = $config->getBpNode($this->newParent)->countChildren();
+ if ($this->to > 0 && $childrenCount < $this->to) {
+ $this->error(
+ 'New parent process "%s" has not enough children. Target position %d out of range',
+ $this->newParent,
+ $this->to
+ );
+ }
+ } else {
+ if ($config->hasRootNode($name)) {
+ $this->error('Process "%s" is already a toplevel process', $name);
+ }
+
+ $childrenCount = $config->countChildren();
+ if ($this->to > 0 && $childrenCount < $this->to) {
+ $this->error(
+ 'Process configuration has not enough toplevel processes. Target position %d out of range',
+ $this->to
+ );
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ if ($this->parent !== null) {
+ $nodes = $config->getBpNode($this->parent)->getChildren();
+ } else {
+ $nodes = $config->getRootNodes();
+ }
+
+ $node = $nodes[$name];
+ $nodes = array_merge(
+ array_slice($nodes, 0, $this->from, true),
+ array_slice($nodes, $this->from + 1, null, true)
+ );
+ if ($this->parent === $this->newParent) {
+ $nodes = array_merge(
+ array_slice($nodes, 0, $this->to, true),
+ [$name => $node],
+ array_slice($nodes, $this->to, null, true)
+ );
+ } else {
+ if ($this->newParent !== null) {
+ $newNodes = $config->getBpNode($this->newParent)->getChildren();
+ } else {
+ $newNodes = $config->getRootNodes();
+ }
+
+ $newNodes = array_merge(
+ array_slice($newNodes, 0, $this->to, true),
+ [$name => $node],
+ array_slice($newNodes, $this->to, null, true)
+ );
+
+ if ($this->newParent !== null) {
+ $config->getBpNode($this->newParent)->setChildNames(array_keys($newNodes));
+ } else {
+ $config->addRootNode($name);
+
+ $i = 0;
+ foreach ($newNodes as $newName => $newNode) {
+ /** @var BpNode $newNode */
+ if ($newNode->getDisplay() > 0 || $newName === $name) {
+ $i += 1;
+ if ($newNode->getDisplay() !== $i) {
+ $newNode->setDisplay($i);
+ }
+ }
+ }
+ }
+ }
+
+ if ($this->parent !== null) {
+ $config->getBpNode($this->parent)->setChildNames(array_keys($nodes));
+ } else {
+ if ($this->newParent !== null) {
+ $config->removeRootNode($name);
+ $node->setDisplay(0);
+ }
+
+ $i = 0;
+ foreach ($nodes as $_ => $oldNode) {
+ /** @var BpNode $oldNode */
+ if ($oldNode->getDisplay() > 0) {
+ $i += 1;
+ if ($oldNode->getDisplay() !== $i) {
+ $oldNode->setDisplay($i);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeRemoveAction.php b/library/Businessprocess/Modification/NodeRemoveAction.php
new file mode 100644
index 0000000..3769764
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeRemoveAction.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+
+/**
+ * NodeRemoveAction
+ *
+ * Tracks removed nodes
+ *
+ * @package Icinga\Module\Businessprocess
+ */
+class NodeRemoveAction extends NodeAction
+{
+ protected $preserveProperties = array('parentName');
+
+ protected $parentName;
+
+ /**
+ * @param $parentName
+ * @return $this
+ */
+ public function setParentName($parentName = null)
+ {
+ $this->parentName = $parentName;
+ return $this;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getParentName()
+ {
+ return $this->parentName;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ $parent = $this->getParentName();
+ if ($parent === null) {
+ if (!$config->hasNode($name)) {
+ $this->error('Toplevel process "%s" not found', $name);
+ }
+ } else {
+ if (! $config->hasNode($parent)) {
+ $this->error('Parent process "%s" missing', $parent);
+ } elseif (! $config->getBpNode($parent)->hasChild($name)) {
+ $this->error('Node "%s" not found in process "%s"', $name, $parent);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ $parentName = $this->getParentName();
+ if ($parentName === null) {
+ $oldDisplay = $config->getBpNode($name)->getDisplay();
+ $config->removeNode($name);
+ if ($config->getMetadata()->isManuallyOrdered()) {
+ foreach ($config->getRootNodes() as $_ => $node) {
+ $nodeDisplay = $node->getDisplay();
+ if ($nodeDisplay > $oldDisplay) {
+ $node->setDisplay($node->getDisplay() - 1);
+ } elseif ($nodeDisplay === $oldDisplay) {
+ break; // Stop immediately to not make things worse ;)
+ }
+ }
+ }
+ } else {
+ $node = $config->getNode($name);
+ $parent = $config->getBpNode($parentName);
+ $parent->removeChild($name);
+ $node->removeParent($parentName);
+ if (! $node->hasParents()) {
+ $config->removeNode($name);
+ }
+ }
+ }
+}
diff --git a/library/Businessprocess/Modification/ProcessChanges.php b/library/Businessprocess/Modification/ProcessChanges.php
new file mode 100644
index 0000000..0ed574c
--- /dev/null
+++ b/library/Businessprocess/Modification/ProcessChanges.php
@@ -0,0 +1,295 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Web\Session\SessionNamespace as Session;
+
+class ProcessChanges
+{
+ /** @var NodeAction[] */
+ protected $changes = array();
+
+ /** @var Session */
+ protected $session;
+
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var bool */
+ protected $hasBeenModified = false;
+
+ /** @var string Session storage key for this processes changes */
+ protected $sessionKey;
+
+ /**
+ * ProcessChanges constructor.
+ *
+ * Direct access is not allowed
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * @param BpConfig $bp
+ * @param Session $session
+ *
+ * @return ProcessChanges
+ */
+ public static function construct(BpConfig $bp, Session $session)
+ {
+ $key = 'changes.' . $bp->getName();
+ $changes = new ProcessChanges();
+ $changes->sessionKey = $key;
+
+ if ($actions = $session->get($key)) {
+ foreach ($actions as $string) {
+ $changes->push(NodeAction::unSerialize($string));
+ }
+ }
+ $changes->session = $session;
+ $changes->config = $bp;
+
+ return $changes;
+ }
+
+ /**
+ * @param Node $node
+ * @param $properties
+ *
+ * @return $this
+ */
+ public function modifyNode(Node $node, $properties)
+ {
+ $action = new NodeModifyAction($node);
+ $action->setNodeProperties($node, $properties);
+ return $this->push($action, true);
+ }
+
+ /**
+ * @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, true);
+ }
+
+ /**
+ * @param Node|string $nodeName
+ * @param array $properties
+ * @param Node $parent
+ *
+ * @return $this
+ */
+ public function createNode($nodeName, $properties, Node $parent = null)
+ {
+ $action = new NodeCreateAction($nodeName);
+ $action->setProperties($properties);
+ if ($parent !== null) {
+ $action->setParent($parent);
+ }
+ return $this->push($action, true);
+ }
+
+ /**
+ * @param $nodeName
+ * @param Node|null $parent
+ * @return $this
+ */
+ public function copyNode($nodeName)
+ {
+ $action = new NodeCopyAction($nodeName);
+ return $this->push($action, true);
+ }
+
+ /**
+ * @param Node $node
+ * @param string $parentName
+ * @return $this
+ */
+ public function deleteNode(Node $node, $parentName = null)
+ {
+ $action = new NodeRemoveAction($node);
+ if ($parentName !== null) {
+ $action->setParentName($parentName);
+ }
+
+ return $this->push($action, true);
+ }
+
+ /**
+ * Move the given node
+ *
+ * @param Node $node
+ * @param int $from
+ * @param int $to
+ * @param string $newParent
+ * @param string $parent
+ *
+ * @return $this
+ */
+ public function moveNode(Node $node, $from, $to, $newParent, $parent = null)
+ {
+ $action = new NodeMoveAction($node);
+ $action->setParent($parent);
+ $action->setNewParent($newParent);
+ $action->setFrom($from);
+ $action->setTo($to);
+
+ return $this->push($action, true);
+ }
+
+ /**
+ * Apply manual order on the entire bp configuration file
+ *
+ * @return $this
+ */
+ public function applyManualOrder()
+ {
+ return $this->push(new NodeApplyManualOrderAction(), true);
+ }
+
+ /**
+ * Add a new action to the stack
+ *
+ * @param NodeAction $change
+ * @param bool $apply
+ *
+ * @return $this
+ */
+ public function push(NodeAction $change, $apply = false)
+ {
+ if ($apply && $change->appliesTo($this->config)) {
+ $change->applyTo($this->config);
+ }
+
+ $this->changes[] = $change;
+ $this->hasBeenModified = true;
+ 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;
+ $this->changes = array();
+ $this->session->set($this->getSessionKey(), null);
+ 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()) {
+ return false;
+ }
+
+ $this->hasBeenModified = true;
+ return array_shift($this->changes);
+ }
+
+ /**
+ * Get the last change on the stack, false if empty
+ *
+ * @return NodeAction|boolean
+ */
+ public function pop()
+ {
+ if ($this->isEmpty()) {
+ return false;
+ }
+
+ $this->hasBeenModified = true;
+ return array_pop($this->changes);
+ }
+
+ /**
+ * The identifier used for this processes changes in our Session storage
+ *
+ * @return string
+ */
+ protected function getSessionKey()
+ {
+ return $this->sessionKey;
+ }
+
+ protected function hasBeenModified()
+ {
+ return $this->hasBeenModified;
+ }
+
+ /**
+ * @return array
+ */
+ public function serialize()
+ {
+ $serialized = array();
+ foreach ($this->getChanges() as $change) {
+ $serialized[] = $change->serialize();
+ }
+
+ return $serialized;
+ }
+
+ /**
+ * Persist to session on destruction
+ */
+ public function __destruct()
+ {
+ if (! $this->hasBeenModified()) {
+ unset($this->session);
+ return;
+ }
+ $session = $this->session;
+ $key = $this->getSessionKey();
+ if (! $this->isEmpty()) {
+ $session->set($key, $this->serialize());
+ }
+ unset($this->session);
+ }
+}
diff --git a/library/Businessprocess/MonitoredNode.php b/library/Businessprocess/MonitoredNode.php
new file mode 100644
index 0000000..3c4167d
--- /dev/null
+++ b/library/Businessprocess/MonitoredNode.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use ipl\Html\Html;
+
+abstract class MonitoredNode extends Node
+{
+ abstract public function getUrl();
+
+ public function getLink()
+ {
+ if ($this->isMissing()) {
+ return Html::tag('a', ['href' => '#'], $this->getAlias());
+ } else {
+ return Html::tag('a', ['href' => $this->getUrl()], $this->getAlias());
+ }
+ }
+}
diff --git a/library/Businessprocess/MonitoringRestrictions.php b/library/Businessprocess/MonitoringRestrictions.php
new file mode 100644
index 0000000..c7d2cef
--- /dev/null
+++ b/library/Businessprocess/MonitoringRestrictions.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\QueryException;
+
+class MonitoringRestrictions
+{
+ /**
+ * Return a filter for the given restriction
+ *
+ * @param string $name Name of the restriction
+ *
+ * @return Filter|null Filter object or null if the authenticated user is not restricted
+ * @throws ConfigurationError If the restriction contains invalid filter columns
+ */
+ public static function getRestriction($name)
+ {
+ // Borrowed from Icinga\Module\Monitoring\Controller
+ $restriction = Filter::matchAny();
+ $restriction->setAllowedFilterColumns(array(
+ 'host_name',
+ 'hostgroup_name',
+ 'instance_name',
+ 'service_description',
+ 'servicegroup_name',
+ function ($c) {
+ return preg_match('/^_(?:host|service)_/i', $c);
+ }
+ ));
+
+ foreach (Auth::getInstance()->getRestrictions($name) as $filter) {
+ if ($filter === '*') {
+ return Filter::matchAny();
+ }
+
+ try {
+ $restriction->addFilter(Filter::fromQueryString($filter));
+ } catch (QueryException $e) {
+ throw new ConfigurationError(
+ mt(
+ 'monitoring',
+ 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s'
+ ),
+ $name,
+ $filter,
+ implode(', ', array(
+ 'instance_name',
+ 'host_name',
+ 'hostgroup_name',
+ 'service_description',
+ 'servicegroup_name',
+ '_(host|service)_<customvar-name>'
+ )),
+ $e
+ );
+ }
+ }
+
+ return $restriction;
+ }
+}
diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php
new file mode 100644
index 0000000..a9eb44c
--- /dev/null
+++ b/library/Businessprocess/Node.php
@@ -0,0 +1,546 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Exception\ProgrammingError;
+use ipl\Html\Html;
+
+abstract class Node
+{
+ const FLAG_DOWNTIME = 1;
+ const FLAG_ACK = 2;
+ const FLAG_MISSING = 4;
+ const FLAG_NONE = 8;
+ const SHIFT_FLAGS = 4;
+
+ const ICINGA_OK = 0;
+ const ICINGA_WARNING = 1;
+ const ICINGA_CRITICAL = 2;
+ const ICINGA_UNKNOWN = 3;
+ const ICINGA_UP = 0;
+ const ICINGA_DOWN = 1;
+ const ICINGA_UNREACHABLE = 2;
+ const ICINGA_PENDING = 99;
+ const NODE_EMPTY = 128;
+
+ /** @var bool Whether to treat acknowledged hosts/services always as UP/OK */
+ protected static $ackIsOk = false;
+
+ /** @var bool Whether to treat hosts/services in downtime always as UP/OK */
+ protected static $downtimeIsOk = false;
+
+ protected $sortStateToStateMap = array(
+ 4 => self::ICINGA_CRITICAL,
+ 3 => self::ICINGA_UNKNOWN,
+ 2 => self::ICINGA_WARNING,
+ 1 => self::ICINGA_PENDING,
+ 0 => self::ICINGA_OK
+ );
+
+ protected $stateToSortStateMap = array(
+ self::ICINGA_PENDING => 1,
+ self::ICINGA_UNKNOWN => 3,
+ self::ICINGA_CRITICAL => 4,
+ self::ICINGA_WARNING => 2,
+ self::ICINGA_OK => 0,
+ self::NODE_EMPTY => 0
+ );
+
+ /** @var string Alias of the node */
+ protected $alias;
+
+ /**
+ * Main business process object
+ *
+ * @var BpConfig
+ */
+ protected $bp;
+
+ /**
+ * Parent nodes
+ *
+ * @var array
+ */
+ protected $parents = array();
+
+ /**
+ * Node identifier
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Node state
+ *
+ * @var int
+ */
+ protected $state;
+
+ /**
+ * Whether this nodes state has been acknowledged
+ *
+ * @var bool
+ */
+ protected $ack;
+
+ /**
+ * Whether this node is in a scheduled downtime
+ *
+ * @var bool
+ */
+ protected $downtime;
+
+ // obsolete
+ protected $duration;
+
+ /**
+ * This node's icon
+ *
+ * @var string
+ */
+ protected $icon;
+
+ /**
+ * Last state change, unix timestamp
+ *
+ * @var int
+ */
+ protected $lastStateChange;
+
+ protected $missing = false;
+
+ protected $empty = false;
+
+ protected $className = 'unknown';
+
+ protected $stateNames = array(
+ 'OK',
+ 'WARNING',
+ 'CRITICAL',
+ 'UNKNOWN',
+ 99 => 'PENDING',
+ 128 => 'EMPTY'
+ );
+
+ /**
+ * Set whether to treat acknowledged hosts/services always as UP/OK
+ *
+ * @param bool $ackIsOk
+ */
+ public static function setAckIsOk($ackIsOk = true)
+ {
+ self::$ackIsOk = $ackIsOk;
+ }
+
+ /**
+ * Set whether to treat hosts/services in downtime always as UP/OK
+ *
+ * @param bool $downtimeIsOk
+ */
+ public static function setDowntimeIsOk($downtimeIsOk = true)
+ {
+ self::$downtimeIsOk = $downtimeIsOk;
+ }
+
+ public function setBpConfig(BpConfig $bp)
+ {
+ $this->bp = $bp;
+ return $this;
+ }
+
+ public function getBpConfig()
+ {
+ return $this->bp;
+ }
+
+ public function setMissing($missing = true)
+ {
+ $this->missing = $missing;
+ return $this;
+ }
+
+ public function isProblem()
+ {
+ return $this->getState() > 0;
+ }
+
+ public function hasBeenChanged()
+ {
+ return false;
+ }
+
+ public function isMissing()
+ {
+ return $this->missing;
+ }
+
+ public function hasMissingChildren()
+ {
+ return count($this->getMissingChildren()) > 0;
+ }
+
+ public function getMissingChildren()
+ {
+ return array();
+ }
+
+ public function hasInfoUrl()
+ {
+ return false;
+ }
+
+ public function setState($state)
+ {
+ $this->state = (int) $state;
+ $this->missing = false;
+ return $this;
+ }
+
+ /**
+ * Forget my state
+ *
+ * @return $this
+ */
+ public function clearState()
+ {
+ $this->state = null;
+ return $this;
+ }
+
+ public function setAck($ack = true)
+ {
+ $this->ack = $ack;
+ return $this;
+ }
+
+ public function setDowntime($downtime = true)
+ {
+ $this->downtime = $downtime;
+ return $this;
+ }
+
+ public function getStateName($state = null)
+ {
+ $states = $this->enumStateNames();
+ if ($state === null) {
+ return $states[ $this->getState() ];
+ } else {
+ return $states[ $state ];
+ }
+ }
+
+ public function enumStateNames()
+ {
+ return $this->stateNames;
+ }
+
+ public function getState()
+ {
+ if ($this->state === null) {
+ throw new ProgrammingError(
+ sprintf(
+ 'Node %s is unable to retrieve it\'s state',
+ $this->name
+ )
+ );
+ }
+
+ return $this->state;
+ }
+
+ public function getSortingState($state = null)
+ {
+ if ($state === null) {
+ $state = $this->getState();
+ }
+
+ if (self::$ackIsOk && $this->isAcknowledged()) {
+ $state = self::ICINGA_OK;
+ }
+
+ if (self::$downtimeIsOk && $this->isInDowntime()) {
+ $state = self::ICINGA_OK;
+ }
+
+ $sort = $this->stateToSortState($state);
+ $sort = ($sort << self::SHIFT_FLAGS)
+ + ($this->isInDowntime() ? self::FLAG_DOWNTIME : 0)
+ + ($this->isAcknowledged() ? self::FLAG_ACK : 0);
+ if (! ($sort & (self::FLAG_DOWNTIME | self::FLAG_ACK))) {
+ $sort |= self::FLAG_NONE;
+ }
+
+ return $sort;
+ }
+
+ public function getLastStateChange()
+ {
+ return $this->lastStateChange;
+ }
+
+ public function setLastStateChange($timestamp)
+ {
+ $this->lastStateChange = $timestamp;
+ return $this;
+ }
+
+ public function addParent(Node $parent)
+ {
+ $this->parents[] = $parent;
+ return $this;
+ }
+
+ public function getDuration()
+ {
+ return $this->duration;
+ }
+
+ public function isHandled()
+ {
+ return $this->isInDowntime() || $this->isAcknowledged();
+ }
+
+ public function isInDowntime()
+ {
+ if ($this->downtime === null) {
+ $this->getState();
+ }
+ return $this->downtime;
+ }
+
+ public function isAcknowledged()
+ {
+ if ($this->ack === null) {
+ $this->getState();
+ }
+ return $this->ack;
+ }
+
+ public function getChildren($filter = null)
+ {
+ return array();
+ }
+
+ public function countChildren($filter = null)
+ {
+ return count($this->getChildren($filter));
+ }
+
+ public function hasChildren($filter = null)
+ {
+ return $this->countChildren($filter) > 0;
+ }
+
+ public function isEmpty()
+ {
+ return $this->countChildren() === 0;
+ }
+
+ public function hasAlias()
+ {
+ return $this->alias !== null;
+ }
+
+ /**
+ * Get the alias of the node
+ *
+ * @return string
+ */
+ public function getAlias()
+ {
+ return $this->alias;
+ }
+
+ /**
+ * Set the alias of the node
+ *
+ * @param string $alias
+ *
+ * @return $this
+ */
+ public function setAlias($alias)
+ {
+ $this->alias = $alias;
+
+ return $this;
+ }
+
+ public function hasParents()
+ {
+ return count($this->parents) > 0;
+ }
+
+ public function hasParentName($name)
+ {
+ foreach ($this->getParents() as $parent) {
+ if ($parent->getName() === $name) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function removeParent($name)
+ {
+ $this->parents = array_filter(
+ $this->parents,
+ function (BpNode $parent) use ($name) {
+ return $parent->getName() !== $name;
+ }
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getParents()
+ {
+ return $this->parents;
+ }
+
+ /**
+ * @param BpConfig $rootConfig
+ *
+ * @return array
+ */
+ public function getPaths($rootConfig = null)
+ {
+ $differentConfig = false;
+ if ($rootConfig === null) {
+ $rootConfig = $this->getBpConfig();
+ } else {
+ $differentConfig = $this->getBpConfig()->getName() !== $rootConfig->getName();
+ }
+
+ $paths = [];
+ foreach ($this->parents as $parent) {
+ foreach ($parent->getPaths($rootConfig) as $path) {
+ $path[] = $differentConfig ? $this->getIdentifier() : $this->getName();
+ $paths[] = $path;
+ }
+ }
+
+ if (! $this instanceof ImportedNode && $this->getBpConfig()->hasRootNode($this->getName())) {
+ $paths[] = [$differentConfig ? $this->getIdentifier() : $this->getName()];
+ } elseif (! $this->hasParents()) {
+ $paths[] = ['__unbound__', $differentConfig ? $this->getIdentifier() : $this->getName()];
+ }
+
+ 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, $this->sortStateToStateMap)) {
+ return $this->sortStateToStateMap[$sortState];
+ }
+
+ throw new ProgrammingError('Got invalid sorting state %s', $sortState);
+ }
+
+ public function getObjectClassName()
+ {
+ return $this->className;
+ }
+
+ public function getLink()
+ {
+ return Html::tag('a', ['href' => '#', 'class' => 'toggle'], Html::tag('i', [
+ 'class' => 'icon icon-down-dir'
+ ]));
+ }
+
+ public function getIcon()
+ {
+ return Html::tag('i', ['class' => 'icon icon-' . ($this->icon ?: 'attention-circled')]);
+ }
+
+ public function operatorHtml()
+ {
+ return '&nbsp;';
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getIdentifier()
+ {
+ return '@' . $this->getBpConfig()->getName() . ':' . $this->getName();
+ }
+
+ public function __toString()
+ {
+ return $this->getName();
+ }
+
+ public function __destruct()
+ {
+ unset($this->parents);
+ }
+
+ /**
+ * Export the node to array
+ *
+ * @param array $parent The node's parent. Used to construct the path to the node
+ * @param bool $flat If false, children will be added to the array key children, else the array will be flat
+ *
+ * @return array
+ */
+ public function toArray(array $parent = null, $flat = false)
+ {
+ $data = [
+ 'name' => $this->getAlias(),
+ 'state' => $this->getStateName(),
+ 'since' => $this->getLastStateChange(),
+ 'in_downtime' => $this->isInDowntime() ? true : false
+ ];
+
+ if ($parent !== null) {
+ $data['path'] = $parent['path'] . '!' . $this->getAlias();
+ } else {
+ $data['path'] = $this->getAlias();
+ }
+
+ $children = [];
+
+ foreach ($this->getChildren() as $node) {
+ if ($flat) {
+ $children = array_merge($children, $node->toArray($data, $flat));
+ } else {
+ $children[] = $node->toArray($data, $flat);
+ }
+ }
+
+ if ($flat) {
+ $data = [$data];
+
+ if (! empty($children)) {
+ $data = array_merge($data, $children);
+ }
+ } else {
+ $data['children'] = $children;
+ }
+
+ return $data;
+ }
+}
diff --git a/library/Businessprocess/ProvidedHook/Icingadb/HostActions.php b/library/Businessprocess/ProvidedHook/Icingadb/HostActions.php
new file mode 100644
index 0000000..27f4551
--- /dev/null
+++ b/library/Businessprocess/ProvidedHook/Icingadb/HostActions.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
+
+use Icinga\Module\Icingadb\Hook\HostActionsHook;
+use Icinga\Module\Icingadb\Model\Host;
+use ipl\Web\Widget\Link;
+
+class HostActions extends HostActionsHook
+{
+ public function getActionsForObject(Host $host): array
+ {
+ $label = mt('businessprocess', 'Business Impact');
+ return array(
+ new Link(
+ $label,
+ 'businessprocess/node/impact?name='
+ . rawurlencode($host->name . ';Hoststatus')
+ )
+ );
+ }
+}
diff --git a/library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php b/library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php
new file mode 100644
index 0000000..1ff37d3
--- /dev/null
+++ b/library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
+
+use Icinga\Module\Icingadb\Hook\IcingadbSupportHook;
+
+class IcingadbSupport extends IcingadbSupportHook
+{
+
+}
diff --git a/library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php b/library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php
new file mode 100644
index 0000000..24e6829
--- /dev/null
+++ b/library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
+
+use Icinga\Module\Icingadb\Hook\ServiceActionsHook;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Web\Widget\Link;
+
+class ServiceActions extends ServiceActionsHook
+{
+ public function getActionsForObject(Service $service): array
+ {
+ $label = mt('businessprocess', 'Business Impact');
+ return array(
+ new Link(
+ $label,
+ sprintf(
+ 'businessprocess/node/impact?name=%s',
+ rawurlencode(
+ sprintf('%s;%s', $service->host->name, $service->name)
+ )
+ )
+ )
+ );
+ }
+}
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 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring;
+
+use Icinga\Module\Monitoring\Hook\HostActionsHook;
+use Icinga\Module\Monitoring\Object\Host;
+
+class HostActions extends HostActionsHook
+{
+ public function getActionsForHost(Host $host)
+ {
+ $label = mt('businessprocess', 'Business Impact');
+ return array(
+ $label => '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 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Module\Monitoring\Hook\ServiceActionsHook;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Url;
+
+class ServiceActions extends ServiceActionsHook
+{
+ public function getActionsForService(Service $service)
+ {
+ $label = mt('businessprocess', 'Business Impact');
+ return array(
+ $label => 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..56c41aa
--- /dev/null
+++ b/library/Businessprocess/Renderer/Breadcrumb.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile;
+use Icinga\Module\Businessprocess\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+
+class Breadcrumb extends BaseHtmlElement
+{
+ protected $tag = 'ul';
+
+ protected $defaultAttributes = array(
+ 'class' => '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(Html::tag('li')->add(
+ Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess'),
+ 'title' => mt('businessprocess', 'Show Overview')
+ ],
+ Html::tag('i', ['class' => 'icon icon-home'])
+ )
+ ));
+ $breadcrumb->add(Html::tag('li')->add(
+ Html::tag('a', ['href' => $bpUrl], $bp->getTitle())
+ ));
+ $path = $renderer->getCurrentPath();
+
+ $parts = array();
+ while ($nodeName = array_pop($path)) {
+ $node = $bp->getNode($nodeName);
+ $renderer->setParentNode($node);
+ array_unshift(
+ $parts,
+ static::renderNode($node, $path, $renderer)
+ );
+ }
+ $breadcrumb->add($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, $node, $path);
+ $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..58e1c4d
--- /dev/null
+++ b/library/Businessprocess/Renderer/Renderer.php
@@ -0,0 +1,398 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\MonitoredNode;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Web\Widget\StateBadge;
+
+abstract class Renderer extends HtmlDocument
+{
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var BpNode */
+ protected $parent;
+
+ /** @var bool Administrative actions are hidden unless unlocked */
+ protected $locked = true;
+
+ /** @var Url */
+ protected $url;
+
+ /** @var Url */
+ protected $baseUrl;
+
+ /** @var array */
+ protected $path = array();
+
+ /** @var bool */
+ protected $isBreadcrumb = false;
+
+ /**
+ * Renderer constructor.
+ *
+ * @param BpConfig $config
+ * @param BpNode|null $parent
+ */
+ public function __construct(BpConfig $config, BpNode $parent = null)
+ {
+ $this->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;
+ }
+
+ public function rendersImportedNode()
+ {
+ return $this->parent !== null && $this->parent->getBpConfig()->getName() !== $this->config->getName();
+ }
+
+ public function setParentNode(BpNode $node)
+ {
+ $this->parent = $node;
+ return $this;
+ }
+
+ /**
+ * @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 BaseHtmlElement
+ */
+ public function renderStateBadges($summary, $totalChildren)
+ {
+ $elements = [];
+
+ $itemCount = Html::tag(
+ 'span',
+ [
+ 'class' => [
+ 'item-count',
+ ]
+ ],
+ sprintf(mtp('businessprocess', '%u Child', '%u Children', $totalChildren), $totalChildren)
+ );
+
+ $elements[] = array_filter([
+ $this->createBadgeGroup($summary, 'CRITICAL'),
+ $this->createBadgeGroup($summary, 'UNKNOWN'),
+ $this->createBadgeGroup($summary, 'WARNING'),
+ $this->createBadge($summary, 'MISSING'),
+ $this->createBadge($summary, 'PENDING')
+ ]);
+
+ if (!empty($elements)) {
+ $container = Html::tag('ul', ['class' => 'state-badges']);
+ $container->add($itemCount);
+ foreach ($elements as $element) {
+ $container->add($element);
+ }
+
+ return $container;
+ }
+ return null;
+ }
+
+ protected function createBadge($summary, $state)
+ {
+ if ($summary[$state] !== 0) {
+ return Html::tag('li', new StateBadge($summary[$state], strtolower($state)));
+ }
+
+ return null;
+ }
+
+ protected function createBadgeGroup($summary, $state)
+ {
+ $content = [];
+ if ($summary[$state] !== 0) {
+ $content[] = Html::tag('li', new StateBadge($summary[$state], strtolower($state)));
+ }
+
+ if ($summary[$state . '-HANDLED'] !== 0) {
+ $content[] = Html::tag('li', new StateBadge($summary[$state . '-HANDLED'], strtolower($state), true));
+ }
+
+ if (empty($content)) {
+ return null;
+ }
+
+ return Html::tag('li', Html::tag('ul', $content));
+ }
+
+ public function getNodeClasses(Node $node)
+ {
+ if ($node->isMissing()) {
+ $classes = array('missing');
+ } else {
+ if ($node->isEmpty() && ! $node instanceof MonitoredNode) {
+ $classes = array('empty');
+ } else {
+ $classes = [strtolower($node->getStateName(
+ $this->parent !== null ? $this->parent->getChildState($node) : null
+ ))];
+ }
+ if ($node->hasMissingChildren()) {
+ $classes[] = 'missing-children';
+ }
+ }
+
+ if ($node->isHandled()) {
+ $classes[] = 'handled';
+ }
+
+ if ($node instanceof BpNode) {
+ $classes[] = 'process-node';
+ } else {
+ $classes[] = 'monitored-node';
+ }
+ // TODO: problem?
+ return $classes;
+ }
+
+ /**
+ * Return the url to the given node's source configuration
+ *
+ * @param BpNode $node
+ *
+ * @return Url
+ */
+ public function getSourceUrl(BpNode $node)
+ {
+ if ($node instanceof ImportedNode) {
+ $name = $node->getNodeName();
+ $paths = $node->getBpConfig()->getBpNode($name)->getPaths();
+ } else {
+ $name = $node->getName();
+ $paths = $node->getPaths();
+ }
+
+ $url = clone $this->getUrl();
+ $url->setParams([
+ 'config' => $node->getBpConfig()->getName(),
+ 'node' => $name
+ ]);
+ // This depends on the fact that the node's root path is the last element in $paths
+ $url->getParams()->addValues('path', array_slice(array_pop($paths), 0, -1));
+ if (! $this->isLocked()) {
+ $url->getParams()->add('unlocked', true);
+ }
+
+ return $url;
+ }
+
+ /**
+ * @param Node $node
+ * @param $path
+ * @return string
+ */
+ public function getId(Node $node, $path)
+ {
+ return md5((empty($path) ? '' : implode(';', $path)) . $node->getName());
+ }
+
+ public function setPath(array $path)
+ {
+ $this->path = $path;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ public function getCurrentPath()
+ {
+ $path = $this->getPath();
+ if ($this->rendersSubNode()) {
+ $path[] = $this->rendersImportedNode()
+ ? $this->parent->getIdentifier()
+ : $this->parent->getName();
+ }
+
+ return $path;
+ }
+
+ /**
+ * @param Url $url
+ * @return $this
+ */
+ public function setUrl(Url $url)
+ {
+ $this->url = $url->without(array(
+ 'action',
+ 'deletenode',
+ 'deleteparent',
+ 'editnode',
+ 'simulationnode',
+ 'view'
+ ));
+ $this->setBaseUrl($this->url);
+ return $this;
+ }
+
+ /**
+ * @param Url $url
+ * @return $this
+ */
+ protected 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;
+ }
+
+ protected function createUnboundParent(BpConfig $bp)
+ {
+ return $bp->getNode('__unbound__');
+ }
+
+ /**
+ * 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..f1f779e
--- /dev/null
+++ b/library/Businessprocess/Renderer/TileRenderer.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile;
+use Icinga\Module\Businessprocess\Web\Form\CsrfToken;
+use ipl\Html\Html;
+
+class TileRenderer extends Renderer
+{
+ /**
+ * @inheritdoc
+ */
+ public function render()
+ {
+ $bp = $this->config;
+ $nodesDiv = Html::tag(
+ 'div',
+ [
+ 'class' => ['sortable', 'tiles', $this->howMany()],
+ 'data-base-target' => '_self',
+ 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false',
+ 'data-sortable-data-id-attr' => 'id',
+ 'data-sortable-direction' => 'horizontal', // Otherwise movement is buggy on small lists
+ 'data-csrf-token' => CsrfToken::generate()
+ ]
+ );
+
+ if ($this->wantsRootNodes()) {
+ $nodesDiv->getAttributes()->add(
+ 'data-action-url',
+ $this->getUrl()->with(['config' => $bp->getName()])->getAbsoluteUrl()
+ );
+ } else {
+ $nodeName = $this->parent instanceof ImportedNode
+ ? $this->parent->getNodeName()
+ : $this->parent->getName();
+ $nodesDiv->getAttributes()
+ ->add('data-node-name', $nodeName)
+ ->add('data-action-url', $this->getUrl()
+ ->with([
+ 'config' => $this->parent->getBpConfig()->getName(),
+ 'node' => $nodeName
+ ])
+ ->getAbsoluteUrl());
+ }
+
+ $nodes = $this->getChildNodes();
+
+ $path = $this->getCurrentPath();
+ foreach ($nodes as $name => $node) {
+ $this->add(new NodeTile($this, $node, $path));
+ }
+
+ if ($this->wantsRootNodes()) {
+ $unbound = $this->createUnboundParent($bp);
+ if ($unbound->hasChildren()) {
+ $this->add(new NodeTile($this, $unbound));
+ }
+ }
+
+ $nodesDiv->add($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;
+ }
+}
diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php
new file mode 100644
index 0000000..67bb4a6
--- /dev/null
+++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php
@@ -0,0 +1,357 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer\TileRenderer;
+
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\HostNode;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\MonitoredNode;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Renderer\Renderer;
+use Icinga\Module\Businessprocess\ServiceNode;
+use Icinga\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Web\Widget\StateBall;
+
+class NodeTile extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $renderer;
+
+ protected $name;
+
+ protected $node;
+
+ protected $path;
+
+ /**
+ * @var BaseHtmlElement
+ */
+ private $actions;
+
+ /**
+ * NodeTile constructor.
+ * @param Renderer $renderer
+ * @param $name
+ * @param Node $node
+ * @param null $path
+ */
+ public function __construct(Renderer $renderer, Node $node, $path = null)
+ {
+ $this->renderer = $renderer;
+ $this->node = $node;
+ $this->path = $path;
+ }
+
+ protected function actions()
+ {
+ if ($this->actions === null) {
+ $this->addActions();
+ }
+ return $this->actions;
+ }
+
+ protected function addActions()
+ {
+ $this->actions = Html::tag(
+ 'div',
+ [
+ 'class' => 'actions'
+ ]
+ );
+
+ return $this->add($this->actions);
+ }
+
+ public function render()
+ {
+ $renderer = $this->renderer;
+ $node = $this->node;
+
+ $attributes = $this->getAttributes();
+ $attributes->add('class', $renderer->getNodeClasses($node));
+ $attributes->add('id', $renderer->getId($node, $this->path));
+ if (! $renderer->isLocked()) {
+ $attributes->add('data-node-name', $node->getName());
+ }
+
+ if (! $renderer->isBreadcrumb()) {
+ $this->addDetailsActions();
+
+ if (! $renderer->isLocked()) {
+ $this->addActionLinks();
+ }
+ }
+ if (! $node instanceof ImportedNode || $node->getBpConfig()->hasNode($node->getName())) {
+ $link = $this->getMainNodeLink();
+ if ($renderer->isBreadcrumb()) {
+ $state = strtolower($node->getStateName());
+ if ($node->isHandled()) {
+ $state = $state . ' handled';
+ }
+ $link->prepend((new StateBall($state, StateBall::SIZE_MEDIUM))->addAttributes([
+ 'title' => sprintf(
+ '%s %s',
+ $state,
+ DateFormatter::timeSince($node->getLastStateChange())
+ )
+ ]));
+ }
+
+ $this->add($link);
+ } else {
+ $this->add(Html::tag(
+ 'a',
+ Html::tag(
+ 'span',
+ ['style' => 'font-size: 75%'],
+ sprintf('Trying to access a missing business process node "%s"', $node->getNodeName())
+ )
+ ));
+ }
+
+ if ($this->renderer->rendersSubNode()
+ && $this->renderer->getParentNode()->getChildState($node) !== $node->getState()
+ ) {
+ $this->add(
+ (new StateBall(strtolower($node->getStateName()), StateBall::SIZE_MEDIUM))
+ ->addAttributes([
+ 'class' => 'overridden-state',
+ 'title' => sprintf(
+ '%s',
+ $node->getStateName()
+ )
+ ])
+ );
+ }
+
+ if ($node instanceof BpNode && !$renderer->isBreadcrumb()) {
+ $this->add($renderer->renderStateBadges($node->getStateSummary(), $node->countChildren()));
+ }
+
+ 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)
+ {
+ $url = $this->renderer->getBaseUrl();
+
+ $p = $url->getParams();
+ if ($node instanceof ImportedNode
+ && $this->renderer->getBusinessProcess()->getName() === $node->getBpConfig()->getName()
+ ) {
+ $p->set('node', $node->getNodeName());
+ } elseif ($this->renderer->rendersImportedNode()) {
+ $p->set('node', $node->getIdentifier());
+ } else {
+ $p->set('node', $node->getName());
+ }
+
+ if (! empty($this->path)) {
+ $p->addValues('path', $this->path);
+ }
+
+ return $url;
+ }
+
+ protected function makeBpUrl(BpNode $node)
+ {
+ return $this->buildBaseNodeUrl($node);
+ }
+
+ /**
+ * @return BaseHtmlElement
+ */
+ protected function getMainNodeLink()
+ {
+ $node = $this->node;
+ $url = $this->getMainNodeUrl($node);
+ if ($node instanceof MonitoredNode) {
+ $link = Html::tag('a', ['href' => $url, 'data-base-target' => '_next'], $node->getAlias());
+ } else {
+ $link = Html::tag('a', ['href' => $url], $node->getAlias());
+ }
+
+ return $link;
+ }
+
+ protected function addDetailsActions()
+ {
+ $node = $this->node;
+ $url = $this->getMainNodeUrl($node);
+
+ if ($node instanceof BpNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('mode', 'tile'),
+ 'title' => mt('businessprocess', 'Show tiles for this subtree')
+ ],
+ Html::tag('i', ['class' => 'icon icon-dashboard'])
+ ))->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('mode', 'tree'),
+ 'title' => mt('businessprocess', 'Show this subtree as a tree')
+ ],
+ Html::tag('i', ['class' => 'icon icon-sitemap'])
+ ));
+ if ($node instanceof ImportedNode) {
+ if ($node->getBpConfig()->hasNode($node->getName())) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'data-base-target' => '_next',
+ 'href' => $this->renderer->getSourceUrl($node)->getAbsoluteUrl(),
+ 'title' => mt(
+ 'businessprocess',
+ 'Show this process as part of its original configuration'
+ )
+ ],
+ Html::tag('i', ['class' => 'icon icon-forward'])
+ ));
+ }
+ }
+
+ $url = $node->getInfoUrl();
+
+ if ($url !== null) {
+ $link = Html::tag(
+ 'a',
+ [
+ 'href' => $url,
+ 'class' => 'node-info',
+ 'title' => sprintf('%s: %s', mt('businessprocess', 'More information'), $url)
+ ],
+ Html::tag('i', ['class' => 'icon icon-info-circled'])
+ );
+ if (preg_match('#^http(?:s)?://#', $url)) {
+ $link->addAttributes(['target' => '_blank']);
+ }
+ $this->actions()->add($link);
+ }
+ } else {
+ // $url = $this->makeMonitoredNodeUrl($node);
+ if ($node instanceof ServiceNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ ['href' => $node->getUrl(), 'data-base-target' => '_next'],
+ Html::tag('i', ['class' => 'icon icon-service'])
+ ));
+ } elseif ($node instanceof HostNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ ['href' => $node->getUrl(), 'data-base-target' => '_next'],
+ Html::tag('i', ['class' => 'icon icon-host'])
+ ));
+ }
+ }
+ }
+
+ protected function addActionLinks()
+ {
+ $parent = $this->renderer->getParentNode();
+ if ($parent !== null) {
+ $baseUrl = Url::fromPath('businessprocess/process/show', [
+ 'config' => $parent->getBpConfig()->getName(),
+ 'node' => $parent instanceof ImportedNode
+ ? $parent->getNodeName()
+ : $parent->getName(),
+ 'unlocked' => true
+ ]);
+ } else {
+ $baseUrl = Url::fromPath('businessprocess/process/show', [
+ 'config' => $this->node->getBpConfig()->getName(),
+ 'unlocked' => true
+ ]);
+ }
+
+ if ($this->node instanceof MonitoredNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl
+ ->with('action', 'simulation')
+ ->with('simulationnode', $this->node->getName()),
+ 'title' => mt(
+ 'businessprocess',
+ 'Show the business impact of this node by simulating a specific state'
+ )
+ ],
+ Html::tag('i', ['class' => 'icon icon-magic'])
+ ));
+
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl
+ ->with('action', 'editmonitored')
+ ->with('editmonitorednode', $this->node->getName()),
+ 'title' => mt('businessprocess', 'Modify this monitored node')
+ ],
+ Html::tag('i', ['class' => 'icon icon-edit'])
+ ));
+ }
+
+ if ($this->renderer->getBusinessProcess()->getMetadata()->canModify()
+ && $this->node->getBpConfig()->getName() === $this->renderer->getBusinessProcess()->getName()
+ && $this->node->getName() !== '__unbound__'
+ ) {
+ if ($this->node instanceof BpNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl
+ ->with('action', 'edit')
+ ->with('editnode', $this->node->getName()),
+ 'title' => mt('businessprocess', 'Modify this business process node')
+ ],
+ Html::tag('i', ['class' => 'icon icon-edit'])
+ ));
+
+ $addUrl = $baseUrl->with([
+ 'node' => $this->node->getName(),
+ 'action' => 'add'
+ ]);
+ $addUrl->getParams()->addValues('path', $this->path);
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $addUrl,
+ 'title' => mt('businessprocess', 'Add a new sub-node to this business process')
+ ],
+ Html::tag('i', ['class' => 'icon icon-plus'])
+ ));
+ }
+ }
+
+ if ($this->renderer->getBusinessProcess()->getMetadata()->canModify()) {
+ $params = array(
+ 'action' => 'delete',
+ 'deletenode' => $this->node->getName(),
+ );
+
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl->with($params),
+ 'title' => mt('businessprocess', 'Delete this node')
+ ],
+ Html::tag('i', ['class' => 'icon icon-cancel'])
+ ));
+ }
+ }
+}
diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php
new file mode 100644
index 0000000..c71a4f9
--- /dev/null
+++ b/library/Businessprocess/Renderer/TreeRenderer.php
@@ -0,0 +1,357 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Web\Form\CsrfToken;
+use Icinga\Module\Icingadb\Model\State;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Web\Widget\StateBall;
+
+class TreeRenderer extends Renderer
+{
+ /**
+ * @inheritdoc
+ */
+ public function render()
+ {
+ $bp = $this->config;
+ $htmlId = $bp->getHtmlId();
+ $tree = Html::tag(
+ 'ul',
+ [
+ 'id' => $htmlId,
+ 'class' => ['bp', 'sortable', $this->wantsRootNodes() ? '' : 'process'],
+ 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false',
+ 'data-sortable-data-id-attr' => 'id',
+ 'data-sortable-direction' => 'vertical',
+ 'data-sortable-group' => json_encode([
+ 'name' => $this->wantsRootNodes() ? 'root' : $htmlId,
+ 'put' => 'function:rowPutAllowed'
+ ]),
+ 'data-sortable-invert-swap' => 'true',
+ 'data-is-root-config' => $this->wantsRootNodes() ? 'true' : 'false',
+ 'data-csrf-token' => CsrfToken::generate()
+ ],
+ $this->renderBp($bp)
+ );
+ if ($this->wantsRootNodes()) {
+ $tree->getAttributes()->add(
+ 'data-action-url',
+ $this->getUrl()->with(['config' => $bp->getName()])->getAbsoluteUrl()
+ );
+ } else {
+ $nodeName = $this->parent instanceof ImportedNode
+ ? $this->parent->getNodeName()
+ : $this->parent->getName();
+ $tree->getAttributes()
+ ->add('data-node-name', $nodeName)
+ ->add('data-action-url', $this->getUrl()
+ ->with([
+ 'config' => $this->parent->getBpConfig()->getName(),
+ 'node' => $nodeName
+ ])
+ ->getAbsoluteUrl());
+ }
+
+ $this->add($tree);
+ 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) {
+ if ($node instanceof BpNode) {
+ $html[] = $this->renderNode($bp, $node);
+ } else {
+ $html[] = $this->renderChild($bp, $this->parent, $node);
+ }
+ }
+
+ return $html;
+ }
+
+ 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
+ * @param array $path
+ * @param BpNode $parent
+ * @return BaseHtmlElement[]
+ */
+ public function getNodeIcons(Node $node, array $path = null, BpNode $parent = null)
+ {
+ $icons = [];
+ if (empty($path) && $node instanceof BpNode) {
+ $icons[] = Html::tag('i', ['class' => 'icon icon-sitemap']);
+ } else {
+ $icons[] = $node->getIcon();
+ }
+ $state = strtolower($node->getStateName($parent !== null ? $parent->getChildState($node) : null));
+ if ($node->isHandled()) {
+ $state = $state . ' handled';
+ }
+ $icons[] = (new StateBall($state, StateBall::SIZE_MEDIUM))->addAttributes([
+ 'title' => sprintf(
+ '%s %s',
+ $state,
+ DateFormatter::timeSince($node->getLastStateChange())
+ )
+ ]);
+ if ($node->isInDowntime()) {
+ $icons[] = Html::tag('i', ['class' => 'icon icon-plug']);
+ }
+ if ($node->isAcknowledged()) {
+ $icons[] = Html::tag('i', ['class' => 'icon icon-ok']);
+ }
+ return $icons;
+ }
+
+ public function getOverriddenState($fakeState, Node $node)
+ {
+ $overriddenState = Html::tag('div', ['class' => 'overridden-state']);
+ $overriddenState->add(
+ (new StateBall(strtolower($node->getStateName()), StateBall::SIZE_MEDIUM))
+ ->addAttributes([
+ 'title' => sprintf(
+ '%s',
+ $node->getStateName()
+ )
+ ])
+ );
+ $overriddenState->add(Html::tag('i', ['class' => 'icon icon-right-small']));
+ $overriddenState->add(
+ (new StateBall(strtolower($node->getStateName($fakeState)), StateBall::SIZE_MEDIUM))
+ ->addAttributes([
+ 'title' => sprintf(
+ '%s',
+ $node->getStateName($fakeState)
+ ),
+ 'class' => 'last'
+ ])
+ );
+
+ return $overriddenState;
+ }
+
+ /**
+ * @param BpConfig $bp
+ * @param Node $node
+ * @param array $path
+ *
+ * @return string
+ */
+ public function renderNode(BpConfig $bp, Node $node, $path = array())
+ {
+ $htmlId = $this->getId($node, $path);
+ $li = Html::tag(
+ 'li',
+ [
+ 'id' => $htmlId,
+ 'class' => ['bp', 'movable', $node->getObjectClassName()],
+ 'data-node-name' => $node instanceof ImportedNode
+ ? $node->getNodeName()
+ : $node->getName()
+ ]
+ );
+ $attributes = $li->getAttributes();
+ $attributes->add('class', $this->getStateClassNames($node));
+ if ($node->isHandled()) {
+ $attributes->add('class', 'handled');
+ }
+ if ($node instanceof BpNode) {
+ $attributes->add('class', 'operator');
+ } else {
+ $attributes->add('class', 'node');
+ }
+
+ $div = Html::tag('div');
+ $li->add($div);
+
+ $div->add($node->getLink());
+ $div->add($this->getNodeIcons($node, $path));
+
+ $div->add(Html::tag('span', null, $node->getAlias()));
+
+ if ($node instanceof BpNode) {
+ $div->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml()));
+ }
+
+ if ($node instanceof BpNode && $node->hasInfoUrl()) {
+ $div->add($this->createInfoAction($node));
+ }
+
+ $differentConfig = $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName();
+ if (! $this->isLocked() && !$differentConfig) {
+ $div->add($this->getActionIcons($bp, $node));
+ } elseif ($differentConfig) {
+ $div->add($this->actionIcon(
+ 'forward',
+ $this->getSourceUrl($node)->addParams(['mode' => 'tree'])->getAbsoluteUrl(),
+ mt('businessprocess', 'Show this process as part of its original configuration')
+ )->addAttributes(['data-base-target' => '_next']));
+ }
+
+ $ul = Html::tag('ul', [
+ 'class' => ['bp', 'sortable'],
+ 'data-sortable-disabled' => ($this->isLocked() || $differentConfig) ? 'true' : 'false',
+ 'data-sortable-invert-swap' => 'true',
+ 'data-sortable-data-id-attr' => 'id',
+ 'data-sortable-draggable' => '.movable',
+ 'data-sortable-direction' => 'vertical',
+ 'data-sortable-group' => json_encode([
+ 'name' => $htmlId, // Unique, so that the function below is the only deciding factor
+ 'put' => 'function:rowPutAllowed'
+ ]),
+ 'data-csrf-token' => CsrfToken::generate(),
+ 'data-action-url' => $this->getUrl()
+ ->with([
+ 'config' => $node->getBpConfig()->getName(),
+ 'node' => $node instanceof ImportedNode
+ ? $node->getNodeName()
+ : $node->getName()
+ ])
+ ->getAbsoluteUrl()
+ ]);
+ $li->add($ul);
+
+ $path[] = $differentConfig ? $node->getIdentifier() : $node->getName();
+ foreach ($node->getChildren() as $name => $child) {
+ if ($child instanceof BpNode) {
+ $ul->add($this->renderNode($bp, $child, $path));
+ } else {
+ $ul->add($this->renderChild($bp, $node, $child, $path));
+ }
+ }
+
+ return $li;
+ }
+
+ protected function renderChild($bp, BpNode $parent, Node $node, $path = null)
+ {
+ $li = Html::tag('li', [
+ 'class' => 'movable',
+ 'id' => $this->getId($node, $path ?: []),
+ 'data-node-name' => $node->getName()
+ ]);
+
+ $li->add($this->getNodeIcons($node, $path, $parent));
+
+ $link = $node->getLink();
+ $link->getAttributes()->set('data-base-target', '_next');
+ $li->add($link);
+
+ if (($overriddenState = $parent->getChildState($node)) !== $node->getState()) {
+ $li->add($this->getOverriddenState($overriddenState, $node));
+ }
+
+ if (! $this->isLocked() && $node->getBpConfig()->getName() === $this->getBusinessProcess()->getName()) {
+ $li->add($this->getActionIcons($bp, $node));
+ }
+
+ return $li;
+ }
+
+ protected function getActionIcons(BpConfig $bp, Node $node)
+ {
+ if ($node instanceof BpNode) {
+ if ($bp->getMetadata()->canModify()) {
+ return [$this->createEditAction($bp, $node), $this->renderAddNewNode($node)];
+ } else {
+ return '';
+ }
+ } else {
+ return $this->createSimulationAction($bp, $node);
+ }
+ }
+
+ protected function createEditAction(BpConfig $bp, BpNode $node)
+ {
+ return $this->actionIcon(
+ 'edit',
+ $this->getUrl()->with(array(
+ 'action' => 'edit',
+ 'editnode' => $node->getName()
+ )),
+ mt('businessprocess', '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()
+ )),
+ mt('businessprocess', 'Simulate a specific state')
+ );
+ }
+
+ protected function createInfoAction(BpNode $node)
+ {
+ $url = $node->getInfoUrl();
+ return $this->actionIcon(
+ 'help',
+ $url,
+ sprintf('%s: %s', mt('businessprocess', 'More information'), $url)
+ )->addAttributes(['target' => '_blank']);
+ }
+
+ protected function actionIcon($icon, $url, $title)
+ {
+ return Html::tag(
+ 'a',
+ [
+ 'href' => $url,
+ 'title' => $title,
+ 'class' => 'action-link'
+ ],
+ Html::tag('i', ['class' => 'icon icon-' . $icon])
+ );
+ }
+
+ protected function renderAddNewNode($parent)
+ {
+ return $this->actionIcon(
+ 'plus',
+ $this->getUrl()
+ ->with('action', 'add')
+ ->with('node', $parent->getName()),
+ mt('businessprocess', 'Add a new business process node')
+ );
+ }
+}
diff --git a/library/Businessprocess/ServiceNode.php b/library/Businessprocess/ServiceNode.php
new file mode 100644
index 0000000..6160bce
--- /dev/null
+++ b/library/Businessprocess/ServiceNode.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Module\Businessprocess\Web\Url;
+
+class ServiceNode extends MonitoredNode
+{
+ protected $hostname;
+
+ /** @var string Alias of the host */
+ protected $hostAlias;
+
+ protected $service;
+
+ protected $className = 'service';
+
+ protected $icon = 'service';
+
+ public function __construct($object)
+ {
+ $this->name = $object->hostname . ';' . $object->service;
+ $this->hostname = $object->hostname;
+ $this->service = $object->service;
+ if (isset($object->state)) {
+ $this->setState($object->state);
+ } else {
+ $this->setState(0)->setMissing();
+ }
+ }
+
+ public function getHostname()
+ {
+ return $this->hostname;
+ }
+
+ /**
+ * Get the host alias
+ *
+ * @return string
+ */
+ public function getHostAlias()
+ {
+ return $this->hostAlias;
+ }
+
+ /**
+ * Set the host alias
+ *
+ * @param string $hostAlias
+ *
+ * @return $this
+ */
+ public function setHostAlias($hostAlias)
+ {
+ $this->hostAlias = $hostAlias;
+
+ return $this;
+ }
+
+ public function getServiceDescription()
+ {
+ return $this->service;
+ }
+
+ public function getAlias()
+ {
+ return $this->getHostAlias() . ': ' . $this->alias;
+ }
+
+ public function getUrl()
+ {
+ $params = array(
+ 'host' => $this->getHostname(),
+ 'service' => $this->getServiceDescription()
+ );
+
+ if ($this->getBpConfig()->hasBackendName()) {
+ $params['backend'] = $this->getBpConfig()->getBackendName();
+ }
+
+ return Url::fromPath('businessprocess/service/show', $params);
+ }
+}
diff --git a/library/Businessprocess/Simulation.php b/library/Businessprocess/Simulation.php
new file mode 100644
index 0000000..1bc9d1d
--- /dev/null
+++ b/library/Businessprocess/Simulation.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Session\SessionNamespace;
+
+class Simulation
+{
+ const DEFAULT_SESSION_KEY = 'bp-simulations';
+
+ /**
+ * @var SessionNamespace
+ */
+ protected $session;
+
+ /**
+ * @var string
+ */
+ protected $sessionKey;
+
+ /**
+ * @var array
+ */
+ protected $simulations = array();
+
+ /**
+ * Simulation constructor.
+ * @param array $simulations
+ */
+ public function __construct(array $simulations = array())
+ {
+ $this->simulations = $simulations;
+ }
+
+ /**
+ * @param array $simulations
+ * @return static
+ */
+ public static function create(array $simulations = array())
+ {
+ return new static($simulations);
+ }
+
+ /**
+ * @param SessionNamespace $session
+ * @param null $sessionKey
+ * @return $this
+ */
+ public static function fromSession(SessionNamespace $session, $sessionKey = null)
+ {
+ return static::create()
+ ->setSessionKey($sessionKey)
+ ->persistToSession($session);
+ }
+
+ /**
+ * @param string $key
+ * @return $this
+ */
+ public function setSessionKey($key = null)
+ {
+ if ($key === null) {
+ $this->sessionKey = Simulation::DEFAULT_SESSION_KEY;
+ } else {
+ $this->sessionKey = $key;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param SessionNamespace $session
+ * @return $this
+ */
+ public function persistToSession(SessionNamespace $session)
+ {
+ $this->session = $session;
+ $this->simulations = $this->session->get($this->sessionKey, array());
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function simulations()
+ {
+ return $this->simulations;
+ }
+
+ /**
+ * @param $simulations
+ * @return $this
+ */
+ protected function setSimulations($simulations)
+ {
+ $this->simulations = $simulations;
+ if ($this->session !== null) {
+ $this->session->set($this->sessionKey, $simulations);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function clear()
+ {
+ $this->simulations = array();
+ if ($this->session !== null) {
+ $this->session->set($this->sessionKey, array());
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function count()
+ {
+ return count($this->simulations());
+ }
+
+ /**
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return $this->count() === 0;
+ }
+
+ /**
+ * @param $node
+ * @param $properties
+ */
+ public function set($node, $properties)
+ {
+ $simulations = $this->simulations();
+ $simulations[$node] = $properties;
+ $this->setSimulations($simulations);
+ }
+
+ /**
+ * @param $name
+ * @return bool
+ */
+ public function hasNode($name)
+ {
+ $simulations = $this->simulations();
+ return array_key_exists($name, $simulations);
+ }
+
+ /**
+ * @param $name
+ * @return mixed
+ * @throws ProgrammingError
+ */
+ public function getNode($name)
+ {
+ $simulations = $this->simulations();
+ if (! array_key_exists($name, $simulations)) {
+ throw new ProgrammingError('Trying to access invalid node %s', $name);
+ }
+ return $simulations[$name];
+ }
+
+ /**
+ * @param $node
+ * @return bool
+ */
+ public function remove($node)
+ {
+ $simulations = $this->simulations();
+ if (array_key_exists($node, $simulations)) {
+ unset($simulations[$node]);
+ $this->setSimulations($simulations);
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/library/Businessprocess/State/IcingaDbState.php b/library/Businessprocess/State/IcingaDbState.php
new file mode 100644
index 0000000..f33d4a4
--- /dev/null
+++ b/library/Businessprocess/State/IcingaDbState.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\State;
+
+use Exception;
+use Icinga\Application\Benchmark;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\IcingaDbObject;
+use Icinga\Module\Businessprocess\ServiceNode;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Sql\Connection as IcingaDbConnection;
+use ipl\Stdlib\Filter;
+
+class IcingaDbState
+{
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var IcingaDbConnection */
+ protected $backend;
+
+ public function __construct(BpConfig $config)
+ {
+ $this->config = $config;
+ $this->backend = IcingaDbObject::fetchDb();
+ }
+
+ 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(sprintf(
+ 'Retrieving states for business process %s using Icinga DB backend',
+ $config->getName()
+ ));
+
+ $hosts = $config->listInvolvedHostNames();
+ if (empty($hosts)) {
+ return $this;
+ }
+
+ $queryHost = Host::on($this->backend)->with('state');
+ IcingaDbObject::applyIcingaDbRestrictions($queryHost);
+
+ $queryHost->filter(Filter::equal('host.name', $hosts));
+
+ $hostObject = $queryHost->getModel()->getTableName();
+
+ Benchmark::measure('Retrieved states for ' . $queryHost->count() . ' hosts in ' . $config->getName());
+
+ $queryService = Service::on($this->backend)->with([
+ 'state',
+ 'host',
+ 'host.state'
+ ]);
+
+ $queryService->filter(Filter::equal('host.name', $hosts));
+
+ IcingaDbObject::applyIcingaDbRestrictions($queryService);
+
+ Benchmark::measure('Retrieved states for ' . $queryService->count() . ' services in ' . $config->getName());
+
+ $configs = $config->listInvolvedConfigs();
+
+ $serviceObject = $queryService->getModel()->getTableName();
+
+ foreach ($configs as $cfg) {
+ foreach ($queryService as $row) {
+ $this->handleDbRow($row, $cfg, $serviceObject);
+ }
+ foreach ($queryHost as $row) {
+ $this->handleDbRow($row, $cfg, $hostObject);
+ }
+ }
+
+ Benchmark::measure('Got states for business process ' . $config->getName());
+
+ return $this;
+ }
+
+ protected function handleDbRow($row, BpConfig $config, $objectName)
+ {
+ if ($objectName === 'service') {
+ $key = $row->host->name . ';' . $row->name;
+ } else {
+ $key = $row->name . ';Hoststatus';
+ }
+
+ // We fetch more states than we need, so skip unknown ones
+ if (! $config->hasNode($key)) {
+ return;
+ }
+
+ $node = $config->getNode($key);
+
+ if ($this->config->usesHardStates()) {
+ if ($row->state->hard_state !== null) {
+ $node->setState($row->state->hard_state)->setMissing(false);
+ }
+ } else {
+ if ($row->state->soft_state !== null) {
+ $node->setState($row->state->soft_state)->setMissing(false);
+ }
+ }
+
+ if ($row->state->last_state_change !== null) {
+ $node->setLastStateChange($row->state->last_state_change/1000);
+ }
+ if ($row->state->in_downtime) {
+ $node->setDowntime(true);
+ }
+ if ($row->state->is_acknowledged) {
+ $node->setAck(true);
+ }
+
+ $node->setAlias($row->display_name);
+
+ if ($node instanceof ServiceNode) {
+ $node->setHostAlias($row->host->display_name);
+ }
+ }
+}
diff --git a/library/Businessprocess/State/MonitoringState.php b/library/Businessprocess/State/MonitoringState.php
new file mode 100644
index 0000000..d317528
--- /dev/null
+++ b/library/Businessprocess/State/MonitoringState.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\State;
+
+use Exception;
+use Icinga\Application\Benchmark;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\ServiceNode;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+
+class MonitoringState
+{
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var MonitoringBackend */
+ protected $backend;
+
+ private function __construct(BpConfig $config)
+ {
+ $this->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;
+
+ 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';
+ }
+
+ $hosts = $config->listInvolvedHostNames();
+ if (empty($hosts)) {
+ return $this;
+ }
+
+ $hostFilter = Filter::expression('host_name', '=', $hosts);
+
+ $hostStatus = $backend->select()->from('hostStatus', array(
+ 'hostname' => 'host_name',
+ 'last_state_change' => $hostStateChangeColumn,
+ 'in_downtime' => 'host_in_downtime',
+ 'ack' => 'host_acknowledged',
+ 'state' => $hostStateColumn,
+ 'display_name' => 'host_display_name'
+ ))->applyFilter($hostFilter)->getQuery()->fetchAll();
+
+ Benchmark::measure('Retrieved states for ' . count($hostStatus) . ' hosts in ' . $config->getName());
+
+ // NOTE: we intentionally filter by host_name ONLY
+ // Tests with host IN ... AND service IN shows longer query times
+ // while retrieving 1635 (in 5ms) vs. 1388 (in ~430ms) services
+ $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,
+ 'display_name' => 'service_display_name',
+ 'host_display_name' => 'host_display_name'
+ ))->applyFilter($hostFilter)->getQuery()->fetchAll();
+
+ Benchmark::measure('Retrieved states for ' . count($serviceStatus) . ' services in ' . $config->getName());
+
+ $configs = $config->listInvolvedConfigs();
+ foreach ($configs as $cfg) {
+ foreach ($serviceStatus as $row) {
+ $this->handleDbRow($row, $cfg);
+ }
+ foreach ($hostStatus as $row) {
+ $this->handleDbRow($row, $cfg);
+ }
+ }
+
+ // 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);
+ }
+
+ $node->setAlias($row->display_name);
+
+ if ($node instanceof ServiceNode) {
+ $node->setHostAlias($row->host_display_name);
+ }
+ }
+}
diff --git a/library/Businessprocess/Storage/ConfigDiff.php b/library/Businessprocess/Storage/ConfigDiff.php
new file mode 100644
index 0000000..495151e
--- /dev/null
+++ b/library/Businessprocess/Storage/ConfigDiff.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Storage;
+
+use Diff;
+use Diff_Renderer_Html_Inline;
+use Diff_Renderer_Html_SideBySide;
+use Diff_Renderer_Text_Context;
+use Diff_Renderer_Text_Unified;
+use ipl\Html\ValidHtml;
+
+class ConfigDiff implements ValidHtml
+{
+ protected $a;
+
+ protected $b;
+
+ protected $diff;
+ protected $opcodes;
+
+ protected function __construct($a, $b)
+ {
+ $this->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..17fc8a5
--- /dev/null
+++ b/library/Businessprocess/Storage/LegacyConfigParser.php
@@ -0,0 +1,409 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Storage;
+
+use Icinga\Application\Benchmark;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\SystemPermissionException;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Metadata;
+
+class LegacyConfigParser
+{
+ /** @var int */
+ protected $currentLineNumber;
+
+ /** @var string */
+ protected $currentFilename;
+
+ protected $name;
+
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var array */
+ protected $missingNodes = [];
+
+ /**
+ * LegacyConfigParser constructor
+ *
+ * @param $name
+ */
+ private function __construct($name)
+ {
+ $this->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);
+
+ $config = $parser->getParsedConfig();
+ $config->setMetadata(
+ static::readMetadataFromString($name, $string)
+ );
+
+ foreach (preg_split('/\r?\n/', $string) as $line) {
+ $parser->parseLine($line);
+ }
+
+ $parser->resolveMissingNodes();
+
+ Benchmark::measure('Business process ' . $name . ' loaded');
+ return $config;
+ }
+
+ 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);
+ }
+
+ $this->resolveMissingNodes();
+
+ fclose($fh);
+ unset($this->currentLineNumber);
+ unset($this->currentFilename);
+ }
+
+ /**
+ * Resolve previously missed business process nodes
+ *
+ * @throws ConfigurationError In case a referenced process does not exist
+ */
+ protected function resolveMissingNodes()
+ {
+ foreach ($this->missingNodes as $name => $parents) {
+ foreach ($parents as $parent) {
+ /** @var BpNode $parent */
+ $parent->addChild($this->config->getNode($name));
+ }
+ }
+ }
+
+ 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;
+ }
+
+ public static function readMetadataFromString($name, &$string)
+ {
+ $metadata = new Metadata($name);
+
+ $lines = preg_split('/\r?\n/', substr($string, 0, 8092));
+ foreach ($lines as $line) {
+ static::parseHeaderLine($line, $metadata);
+ }
+
+ 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('/\r?\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*(.+)$/', trim($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 parseStateOverrides(&$line, BpConfig $bp)
+ {
+ // state_overrides <bp-node>!<child>|n-n[,n-n]!<child>|n-n[,n-n]
+ $segments = preg_split('~\s*!\s*~', substr($line, 16));
+ $node = $bp->getNode(array_shift($segments));
+ foreach ($segments as $overrideDef) {
+ list($childName, $overrides) = preg_split('~\s*\|\s*~', $overrideDef, 2);
+
+ $stateOverrides = [];
+ foreach (preg_split('~\s*,\s*~', $overrides) as $override) {
+ list($from, $to) = preg_split('~\s*-\s*~', $override, 2);
+ $stateOverrides[(int) $from] = (int) $to;
+ }
+
+ $node->setStateOverrides($stateOverrides, $childName);
+ }
+ }
+
+ 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;
+ case 'state_overrides':
+ $this->parseStateOverrides($line, $bp);
+ break;
+ case 'template':
+ // compat, ignoring for now
+ 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;
+ }
+
+ // Space found in the first 16 cols? Might be a line with extra information
+ $pos = strpos($line, ' ');
+ if ($pos !== false && $pos < 16) {
+ if ($this->parseExtraLine($line, $pos, $bp)) {
+ return;
+ }
+ }
+
+ if (strpos($line, '=') === false) {
+ $this->parseError('Got invalid line');
+ }
+
+ 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: <var> = <num> of: <var1> + <var2> [+ <varn>]*');
+ }
+ $op_name = $m[1];
+ // New feature: $minWarn = $m[2];
+ $value = $m[3];
+ }
+
+ $node = new BpNode((object) array(
+ 'name' => $name,
+ 'operator' => $op_name,
+ 'child_names' => []
+ ));
+ $node->setBpConfig($bp);
+
+ $cmps = preg_split('~\s*(?<!\\\\)\\' . $op . '\s*~', $value, -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($cmps as $val) {
+ $val = preg_replace('~(\\\\([\|\+&\!\%]))~', '$2', $val);
+ if (strpos($val, ';') !== false) {
+ if ($bp->hasNode($val)) {
+ $node->addChild($bp->getNode($val));
+ } else {
+ list($host, $service) = preg_split('~;~', $val, 2);
+ if ($service === 'Hoststatus') {
+ $node->addChild($bp->createHost($host));
+ } else {
+ $node->addChild($bp->createService($host, $service));
+ }
+ }
+ } elseif ($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
+ );
+ } else {
+ list($config, $nodeName) = preg_split('~:\s*~', substr($val, 1), 2);
+ $node->addChild($bp->createImportedNode($config, $nodeName));
+ }
+ } elseif ($bp->hasNode($val)) {
+ $node->addChild($bp->getNode($val));
+ } else {
+ $this->missingNodes[$val][] = $node;
+ }
+ }
+
+ $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..7e2e0b2
--- /dev/null
+++ b/library/Businessprocess/Storage/LegacyConfigRenderer.php
@@ -0,0 +1,255 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Storage;
+
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\ImportedNode;
+
+class LegacyConfigRenderer
+{
+ /** @var array */
+ protected $renderedNodes;
+
+ /**
+ * LecagyConfigRenderer constructor
+ *
+ * @param BpConfig $config
+ */
+ public function __construct(BpConfig $config)
+ {
+ $this->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) {
+ if ($child instanceof ImportedNode) {
+ continue;
+ }
+
+ $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::renderStateOverrides($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 . ' ', array_map(function ($val) {
+ return preg_replace('~([\|\+&\!\%])~', '\\\\$1', $val);
+ }, $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 '';
+ }
+ }
+
+ public static function renderStateOverrides(BpNode $node)
+ {
+ $stateOverrides = '';
+ foreach ($node->getStateOverrides() as $childName => $overrideRules) {
+ $overrides = [];
+ foreach ($overrideRules as $from => $to) {
+ $overrides[] = sprintf('%d-%d', $from, $to);
+ }
+
+ if (! empty($overrides)) {
+ $stateOverrides .= '!' . $childName . '|' . join(',', $overrides);
+ }
+ }
+
+ if (! $stateOverrides) {
+ return '';
+ }
+
+ return 'state_overrides ' . $node->getName() . $stateOverrides . "\n";
+ }
+
+ /**
+ * @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
new file mode 100644
index 0000000..6582ebd
--- /dev/null
+++ b/library/Businessprocess/Storage/LegacyStorage.php
@@ -0,0 +1,207 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Storage;
+
+use DirectoryIterator;
+use Icinga\Application\Hook\AuditHook;
+use Icinga\Application\Icinga;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Exception\SystemPermissionException;
+
+class LegacyStorage extends Storage
+{
+ /**
+ * All parsed configurations
+ *
+ * @var BpConfig[]
+ */
+ protected $configs = [];
+
+ /** @var string */
+ protected $configDir;
+
+ public function getConfigDir()
+ {
+ if ($this->configDir === null) {
+ $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;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function listProcesses()
+ {
+ $files = array();
+
+ foreach ($this->listAllProcessNames() as $name) {
+ $meta = $this->loadMetadata($name);
+ if (! $meta->canRead()) {
+ continue;
+ }
+
+ $files[$name] = $meta->getExtendedTitle();
+ }
+
+ natcasesort($files);
+ return $files;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function listProcessNames()
+ {
+ $files = array();
+
+ foreach ($this->listAllProcessNames() as $name) {
+ $meta = $this->loadMetadata($name);
+ if (! $meta->canRead()) {
+ continue;
+ }
+
+ $files[$name] = $name;
+ }
+
+ natcasesort($files);
+ return $files;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ 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)
+ {
+ if (! isset($this->configs[$name])) {
+ $this->configs[$name] = LegacyConfigParser::parseFile(
+ $name,
+ $this->getFilename($name)
+ );
+ }
+
+ return $this->configs[$name];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function storeProcess(BpConfig $process)
+ {
+ AuditHook::logActivity('businessprocess/store', "Business Process \"{$process->getName()}\" stored");
+ file_put_contents(
+ $this->getFilename($process->getName()),
+ LegacyConfigRenderer::renderConfig($process)
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function deleteProcess($name)
+ {
+ AuditHook::logActivity('businessprocess/delete', "Business Process \"{$name}\" deleted");
+ return @unlink($this->getFilename($name));
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function loadMetadata($name)
+ {
+ if (isset($this->configs[$name])) {
+ return $this->configs[$name]->getMetadata();
+ }
+
+ return LegacyConfigParser::readMetadataFromFileHeader(
+ $name,
+ $this->getFilename($name)
+ );
+ }
+
+ public function getSource($name)
+ {
+ return file_get_contents($this->getFilename($name));
+ }
+
+ public function getFilename($name)
+ {
+ return $this->getConfigDir() . '/' . $name . '.conf';
+ }
+
+ /**
+ * @param $name
+ * @param $string
+ *
+ * @return BpConfig
+ */
+ public function loadFromString($name, $string)
+ {
+ return LegacyConfigParser::parseString($name, $string);
+ }
+
+ /**
+ * @param $name
+ * @return bool
+ */
+ public function hasProcess($name)
+ {
+ $file = $this->getFilename($name);
+ if (! is_file($file)) {
+ return false;
+ }
+
+ return $this->loadMetadata($name)->canRead();
+ }
+}
diff --git a/library/Businessprocess/Storage/Storage.php b/library/Businessprocess/Storage/Storage.php
new file mode 100644
index 0000000..c8a07ba
--- /dev/null
+++ b/library/Businessprocess/Storage/Storage.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Storage;
+
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Metadata;
+
+abstract class Storage
+{
+ /**
+ * @var static
+ */
+ protected static $instance;
+
+ /**
+ * @var ConfigObject
+ */
+ protected $config;
+
+ /**
+ * Storage constructor.
+ * @param ConfigObject $config
+ */
+ public function __construct(ConfigObject $config)
+ {
+ $this->config = $config;
+ $this->init();
+ }
+
+ protected function init()
+ {
+ }
+
+ public static function getInstance()
+ {
+ if (static::$instance === null) {
+ static::$instance = new static(Config::module('businessprocess')->getSection('global'));
+ }
+
+ return static::$instance;
+ }
+
+ /**
+ * All processes readable by the current user
+ *
+ * The returned array has the form <process name> => <nice title>, sorted
+ * by title
+ *
+ * @return array
+ */
+ abstract public function listProcesses();
+
+ /**
+ * All process names readable by the current user
+ *
+ * The returned array has the form <process name> => <process name> 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(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 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Test;
+
+use Icinga\Application\Config;
+use Icinga\Application\ApplicationBootstrap;
+use Icinga\Application\Icinga;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Module\Businessprocess\Web\FakeRequest;
+use PHPUnit_Framework_TestCase;
+
+abstract class BaseTestCase extends PHPUnit_Framework_TestCase
+{
+ /** @var ApplicationBootstrap */
+ private static $app;
+
+ /**
+ * @inheritdoc
+ */
+ public function setUp()
+ {
+ $this->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..4141c16
--- /dev/null
+++ b/library/Businessprocess/Test/Bootstrap.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Test;
+
+use Icinga\Application\Cli;
+
+class Bootstrap
+{
+ public static function cli($basedir = null)
+ {
+ error_reporting(E_ALL | E_STRICT);
+ if ($basedir === null) {
+ $basedir = dirname(dirname(dirname(__DIR__)));
+ }
+ $testsDir = $basedir . '/test';
+ require_once 'Icinga/Application/Cli.php';
+
+ if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) {
+ $configDir = $_SERVER['ICINGAWEB_CONFIGDIR'];
+ } else {
+ $configDir = $testsDir . '/config';
+ }
+
+ Cli::start($testsDir, $configDir)
+ ->getModuleManager()
+ ->loadModule('ipl', $basedir . '/vendor/ipl')
+ ->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..94458dc
--- /dev/null
+++ b/library/Businessprocess/Web/Component/ActionBar.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use ipl\Html\BaseHtmlElement;
+
+class ActionBar extends BaseHtmlElement
+{
+ protected $contentSeparator = ' ';
+
+ /** @var string */
+ protected $tag = 'div';
+
+ protected $defaultAttributes = array('class' => 'action-bar');
+}
diff --git a/library/Businessprocess/Web/Component/BpDashboardTile.php b/library/Businessprocess/Web/Component/BpDashboardTile.php
new file mode 100644
index 0000000..17d3a0c
--- /dev/null
+++ b/library/Businessprocess/Web/Component/BpDashboardTile.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\Text;
+
+class BpDashboardTile extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $defaultAttributes = ['class' => 'dashboard-tile'];
+
+ public function __construct(BpConfig $bp, $title, $description, $icon, $url, $urlParams = null, $attributes = null)
+ {
+ if (! isset($attributes['href'])) {
+ $attributes['href'] = Url::fromPath($url, $urlParams ?: []);
+ }
+
+ $this->add(Html::tag(
+ 'div',
+ ['class' => 'bp-link', 'data-base-target' => '_main'],
+ Html::tag('a', $attributes, Html::tag('i', ['class' => 'icon icon-' . $icon]))
+ ->add(Html::tag('span', ['class' => 'header'], $title))
+ ->add($description)
+ ));
+
+ $tiles = Html::tag('div', ['class' => 'bp-root-tiles']);
+
+ foreach ($bp->getChildren() as $node) {
+ $state = strtolower($node->getStateName());
+
+ $tiles->add(Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath($url, $urlParams ?: [])->with(['node' => $node->getName()]),
+ 'class' => "badge state-{$state}",
+ 'title' => $node->getAlias()
+ ],
+ Text::create('&nbsp;')->setEscaped()
+ ));
+ }
+
+ $this->add($tiles);
+ }
+}
diff --git a/library/Businessprocess/Web/Component/Content.php b/library/Businessprocess/Web/Component/Content.php
new file mode 100644
index 0000000..6d14197
--- /dev/null
+++ b/library/Businessprocess/Web/Component/Content.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use ipl\Html\BaseHtmlElement;
+
+class Content extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $contentSeparator = "\n";
+
+ protected $defaultAttributes = array('class' => 'content');
+}
diff --git a/library/Businessprocess/Web/Component/Controls.php b/library/Businessprocess/Web/Component/Controls.php
new file mode 100644
index 0000000..259cbbb
--- /dev/null
+++ b/library/Businessprocess/Web/Component/Controls.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use ipl\Html\BaseHtmlElement;
+
+class Controls extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $contentSeparator = "\n";
+
+ protected $defaultAttributes = array('class' => 'controls');
+}
diff --git a/library/Businessprocess/Web/Component/Dashboard.php b/library/Businessprocess/Web/Component/Dashboard.php
new file mode 100644
index 0000000..58506df
--- /dev/null
+++ b/library/Businessprocess/Web/Component/Dashboard.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Businessprocess\Storage\Storage;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+
+class Dashboard extends BaseHtmlElement
+{
+ /** @var string */
+ protected $contentSeparator = "\n";
+
+ /** @var string */
+ protected $tag = 'div';
+
+ protected $defaultAttributes = array(
+ 'class' => '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(
+ Html::tag('h1', null, mt('businessprocess', 'Welcome to your Business Process Overview'))
+ );
+ $this->add(Html::tag(
+ 'p',
+ null,
+ mt(
+ 'businessprocess',
+ '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(
+ mt('businessprocess', 'Create'),
+ mt('businessprocess', 'Create a new Business Process configuration'),
+ 'plus',
+ 'businessprocess/process/create',
+ null,
+ array('class' => 'addnew')
+ )
+ )->add(
+ new DashboardAction(
+ mt('businessprocess', 'Upload'),
+ mt('businessprocess', 'Upload an existing Business Process configuration'),
+ 'upload',
+ 'businessprocess/process/upload',
+ null,
+ array('class' => 'addnew')
+ )
+ );
+ } elseif (empty($processes)) {
+ $this->add(
+ Html::tag('div')
+ ->add(Html::tag('h1', null, mt('businessprocess', 'Not available')))
+ ->add(Html::tag('p', null, mt(
+ 'businessprocess',
+ '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;
+ }
+
+ $bp = $storage->loadProcess($name);
+
+ if (Module::exists('icingadb') &&
+ (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())
+ ) {
+ IcingaDbState::apply($bp);
+ } else {
+ MonitoringState::apply($bp);
+ }
+
+ $this->add(new BpDashboardTile(
+ $bp,
+ $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..5ed1845
--- /dev/null
+++ b/library/Businessprocess/Web/Component/DashboardAction.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use Icinga\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+
+class DashboardAction extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $defaultAttributes = array('class' => 'action');
+
+ public function __construct($title, $description, $icon, $url, $urlParams = null, $attributes = null)
+ {
+ if (! isset($attributes['href'])) {
+ $attributes['href'] = Url::fromPath($url, $urlParams ?: []);
+ }
+
+ $this->add(Html::tag('a', $attributes)
+ ->add(Html::tag('i', ['class' => 'icon icon-' . $icon]))
+ ->add(Html::tag('span', ['class' => 'header'], $title))
+ ->add($description));
+ }
+}
diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php
new file mode 100644
index 0000000..6f192dc
--- /dev/null
+++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Renderer\Renderer;
+use Icinga\Module\Businessprocess\Renderer\TreeRenderer;
+use Icinga\Web\Url;
+use ipl\Html\Html;
+
+class RenderedProcessActionBar extends ActionBar
+{
+ public function __construct(BpConfig $config, Renderer $renderer, Auth $auth, Url $url)
+ {
+ $meta = $config->getMetadata();
+
+ if ($renderer instanceof TreeRenderer) {
+ $link = Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('mode', 'tile'),
+ 'title' => mt('businessprocess', 'Switch to Tile view')
+ ]
+ );
+ } else {
+ $link = Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('mode', 'tree'),
+ 'title' => mt('businessprocess', 'Switch to Tree view')
+ ]
+ );
+ }
+
+ $link->add([
+ Html::tag('i', ['class' => 'icon icon-dashboard' . ($renderer instanceof TreeRenderer ? '' : ' active')]),
+ Html::tag('i', ['class' => 'icon icon-sitemap' . ($renderer instanceof TreeRenderer ? ' active' : '')])
+ ]);
+
+ $this->add(
+ Html::tag('div', ['class' => 'view-toggle'])
+ ->add(Html::tag('span', null, mt('businessprocess', 'View')))
+ ->add($link)
+ );
+
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'data-base-target' => '_main',
+ 'href' => $url->with('showFullscreen', true),
+ 'title' => mt('businessprocess', 'Switch to fullscreen mode'),
+ 'class' => 'icon-resize-full-alt'
+ ],
+ mt('businessprocess', 'Fullscreen')
+ ));
+
+ $hasChanges = $config->hasSimulations() || $config->hasBeenChanged();
+
+ if ($renderer->isLocked()) {
+ if (! $renderer->wantsRootNodes() && $renderer->rendersImportedNode()) {
+ $span = Html::tag('span', [
+ 'class' => 'disabled',
+ 'title' => mt(
+ 'businessprocess',
+ 'Imported processes can only be changed in their original configuration'
+ )
+ ]);
+ $span->add(Html::tag('i', ['class' => 'icon icon-lock']))
+ ->add(mt('businessprocess', 'Editing Locked'));
+ $this->add($span);
+ } else {
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('unlocked', true),
+ 'title' => mt('businessprocess', 'Click to unlock editing for this process'),
+ 'class' => 'icon-lock'
+ ],
+ mt('businessprocess', 'Unlock Editing')
+ ));
+ }
+ } elseif (! $hasChanges) {
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->without('unlocked')->without('action'),
+ 'title' => mt('businessprocess', 'Click to lock editing for this process'),
+ 'class' => 'icon-lock-open'
+ ],
+ mt('businessprocess', 'Lock Editing')
+ ));
+ }
+
+ if (($hasChanges || ! $renderer->isLocked()) && $meta->canModify()) {
+ if ($renderer->wantsRootNodes()) {
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'data-base-target' => '_next',
+ 'href' => Url::fromPath('businessprocess/process/config', $this->currentProcessParams($url)),
+ 'title' => mt('businessprocess', 'Modify this process'),
+ 'class' => 'icon-wrench'
+ ],
+ mt('businessprocess', 'Config')
+ ));
+ } else {
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with([
+ 'action' => 'edit',
+ 'editnode' => $url->getParam('node')
+ ])->getAbsoluteUrl(),
+ 'title' => mt('businessprocess', 'Modify this process'),
+ 'class' => 'icon-wrench'
+ ],
+ mt('businessprocess', 'Config')
+ ));
+ }
+ }
+
+ if (($hasChanges || (! $renderer->isLocked())) && $meta->canModify()) {
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('action', 'add'),
+ 'title' => mt('businessprocess', 'Add a new business process node'),
+ 'class' => 'icon-plus button-link'
+ ],
+ mt('businessprocess', 'Add Node')
+ ));
+ }
+ }
+
+ protected function currentProcessParams(Url $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..aaa444e
--- /dev/null
+++ b/library/Businessprocess/Web/Component/Tabs.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use ipl\Html\ValidHtml;
+
+class Tabs extends WtfTabs implements ValidHtml
+{
+}
diff --git a/library/Businessprocess/Web/Component/WtfTabs.php b/library/Businessprocess/Web/Component/WtfTabs.php
new file mode 100644
index 0000000..8f2250f
--- /dev/null
+++ b/library/Businessprocess/Web/Component/WtfTabs.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use Icinga\Web\Widget\Tabs;
+
+/**
+ * Class WtfTabs
+ *
+ * TODO: Please remove this as soon as we drop support for PHP 5.3.x
+ * This works around https://bugs.php.net/bug.php?id=43200 and fixes
+ * https://github.com/Icinga/icingaweb2-module-businessprocess/issues/81
+ *
+ * @package Icinga\Module\Businessprocess\Web\Component
+ */
+class WtfTabs extends Tabs
+{
+ public function render()
+ {
+ return parent::render();
+ }
+}
diff --git a/library/Businessprocess/Web/Controller.php b/library/Businessprocess/Web/Controller.php
new file mode 100644
index 0000000..e9719e4
--- /dev/null
+++ b/library/Businessprocess/Web/Controller.php
@@ -0,0 +1,269 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Module\Businessprocess\Storage\Storage;
+use Icinga\Module\Businessprocess\Web\Component\ActionBar;
+use Icinga\Module\Businessprocess\Web\Component\Controls;
+use Icinga\Module\Businessprocess\Web\Component\Content;
+use Icinga\Module\Businessprocess\Web\Component\Tabs;
+use Icinga\Module\Businessprocess\Web\Form\FormLoader;
+use Icinga\Web\Controller as ModuleController;
+use Icinga\Web\Notification;
+use Icinga\Web\View;
+use ipl\Html\Html;
+
+class Controller extends ModuleController
+{
+ /** @var View */
+ public $view;
+
+ /** @var BpConfig */
+ protected $bp;
+
+ /** @var Tabs */
+ protected $mytabs;
+
+ /** @var Storage */
+ private $storage;
+
+ /** @var bool */
+ protected $showFullscreen;
+
+ /** @var Url */
+ private $url;
+
+ public function init()
+ {
+ $m = Icinga::app()->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->setViewScript('default');
+ }
+
+ /**
+ * @return Url
+ */
+ protected function url()
+ {
+ if ($this->url === null) {
+ $this->url = Url::fromPath(
+ $this->getRequest()->getUrl()->getPath()
+ )->setParams($this->getRequest()->getUrl()->getParams());
+ }
+
+ 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) {
+ $controls = $this->view->controls = new Controls();
+ if ($this->view->compact) {
+ $controls->getAttributes()->add('class', 'compact');
+ }
+ }
+
+ return $this->view->controls;
+ }
+
+ /**
+ * @return Content
+ */
+ protected function content()
+ {
+ if ($this->view->content === null) {
+ $content = $this->view->content = new Content();
+ if ($this->view->compact) {
+ $content->getAttributes()->add('class', 'compact');
+ }
+ }
+
+ 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(Html::tag('h1', null, $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 = LegacyStorage::getInstance();
+ }
+
+ 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 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Request;
+
+class FakeRequest extends Request
+{
+ /** @var string */
+ private static $baseUrl;
+
+ public static function setConfiguredBaseUrl($url)
+ {
+ self::$baseUrl = $url;
+ }
+
+ public function getBaseUrl($raw = false)
+ {
+ if (self::$baseUrl === null) {
+ throw new ProgrammingError('Cannot determine base URL on CLI if not configured');
+ } else {
+ return self::$baseUrl;
+ }
+ }
+}
diff --git a/library/Businessprocess/Web/Form/BpConfigBaseForm.php b/library/Businessprocess/Web/Form/BpConfigBaseForm.php
new file mode 100644
index 0000000..ddfc851
--- /dev/null
+++ b/library/Businessprocess/Web/Form/BpConfigBaseForm.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form;
+
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Module\Businessprocess\BpConfig;
+
+abstract class BpConfigBaseForm extends QuickForm
+{
+ /** @var LegacyStorage */
+ protected $storage;
+
+ /** @var BpConfig */
+ protected $config;
+
+ protected function listAvailableBackends()
+ {
+ $keys = [];
+ $moduleManager = Icinga::app()->getModuleManager();
+ if ($moduleManager->hasEnabled('monitoring')) {
+ $keys = array_keys(Config::module('monitoring', 'backends')->toArray());
+ $keys = array_combine($keys, $keys);
+ }
+
+ return $keys;
+ }
+
+ public function setStorage(LegacyStorage $storage)
+ {
+ $this->storage = $storage;
+ return $this;
+ }
+
+ public function setProcessConfig(BpConfig $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ protected function prepareMetadata(BpConfig $config)
+ {
+ $meta = $config->getMetadata();
+ $auth = Auth::getInstance();
+ $meta->set('Owner', $auth->getUser()->getUsername());
+
+ if ($auth->hasPermission('businessprocess/showall')) {
+ return true;
+ }
+
+ $prefixes = $auth->getRestrictions('businessprocess/prefix');
+ if (! empty($prefixes) && ! $meta->nameIsPrefixedWithOneOf($prefixes)) {
+ if (count($prefixes) === 1) {
+ $this->getElement('name')->addError(sprintf(
+ $this->translate('Please prefix the name with "%s"'),
+ current($prefixes)
+ ));
+ } else {
+ $this->getElement('name')->addError(sprintf(
+ $this->translate('Please prefix the name with one of "%s"'),
+ implode('", "', $prefixes)
+ ));
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/library/Businessprocess/Web/Form/CsrfToken.php b/library/Businessprocess/Web/Form/CsrfToken.php
new file mode 100644
index 0000000..9eb24ef
--- /dev/null
+++ b/library/Businessprocess/Web/Form/CsrfToken.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form;
+
+class CsrfToken
+{
+ /**
+ * Check whether the given token is valid
+ *
+ * @param string $token Token
+ *
+ * @return bool
+ */
+ public static function isValid($token)
+ {
+ if (strpos($token, '|') === false) {
+ return false;
+ }
+
+ list($seed, $token) = explode('|', $token);
+
+ if (!is_numeric($seed)) {
+ return false;
+ }
+
+ return $token === hash('sha256', self::getSessionId() . $seed);
+ }
+
+ /**
+ * Create a new token
+ *
+ * @return string
+ */
+ public static function generate()
+ {
+ $seed = mt_rand();
+ $token = hash('sha256', self::getSessionId() . $seed);
+
+ return sprintf('%s|%s', $seed, $token);
+ }
+
+ /**
+ * Get current session id
+ *
+ * TODO: we should do this through our App or Session object
+ *
+ * @return string
+ */
+ protected static function getSessionId()
+ {
+ return session_id();
+ }
+}
diff --git a/library/Businessprocess/Web/Form/Element/Checkbox.php b/library/Businessprocess/Web/Form/Element/Checkbox.php
new file mode 100644
index 0000000..7975b82
--- /dev/null
+++ b/library/Businessprocess/Web/Form/Element/Checkbox.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form\Element;
+
+class Checkbox extends \Icinga\Web\Form\Element\Checkbox
+{
+
+}
diff --git a/library/Businessprocess/Web/Form/Element/FormElement.php b/library/Businessprocess/Web/Form/Element/FormElement.php
new file mode 100644
index 0000000..7647a5e
--- /dev/null
+++ b/library/Businessprocess/Web/Form/Element/FormElement.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form\Element;
+
+use Zend_Form_Element_Xhtml;
+
+class FormElement extends Zend_Form_Element_Xhtml
+{
+}
diff --git a/library/Businessprocess/Web/Form/Element/SimpleNote.php b/library/Businessprocess/Web/Form/Element/SimpleNote.php
new file mode 100644
index 0000000..9f757f2
--- /dev/null
+++ b/library/Businessprocess/Web/Form/Element/SimpleNote.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form\Element;
+
+class SimpleNote extends FormElement
+{
+ public $helper = 'formSimpleNote';
+
+ /**
+ * Always ignore this element
+ * @codingStandardsIgnoreStart
+ *
+ * @var boolean
+ */
+ protected $_ignore = true;
+ // @codingStandardsIgnoreEnd
+
+ public function isValid($value, $context = null)
+ {
+ return true;
+ }
+}
diff --git a/library/Businessprocess/Web/Form/Element/StateOverrides.php b/library/Businessprocess/Web/Form/Element/StateOverrides.php
new file mode 100644
index 0000000..c2216c0
--- /dev/null
+++ b/library/Businessprocess/Web/Form/Element/StateOverrides.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form\Element;
+
+class StateOverrides extends FormElement
+{
+ public $helper = 'formStateOverrides';
+
+ /** @var array The overridable states */
+ protected $states;
+
+ /**
+ * Set the overridable states
+ *
+ * @param array $states
+ *
+ * @return $this
+ */
+ public function setStates(array $states)
+ {
+ $this->states = $states;
+
+ return $this;
+ }
+
+ /**
+ * Get the overridable states
+ *
+ * @return array
+ */
+ public function getStates()
+ {
+ return $this->states;
+ }
+
+ public function init()
+ {
+ $this->setIsArray(true);
+ }
+
+ public function setValue($value)
+ {
+ $cleanedValue = [];
+
+ if (! empty($value)) {
+ foreach ($value as $from => $to) {
+ if ((int) $from !== (int) $to) {
+ $cleanedValue[$from] = $to;
+ }
+ }
+ }
+
+ return parent::setValue($cleanedValue);
+ }
+}
diff --git a/library/Businessprocess/Web/Form/FormLoader.php b/library/Businessprocess/Web/Form/FormLoader.php
new file mode 100644
index 0000000..965da4b
--- /dev/null
+++ b/library/Businessprocess/Web/Form/FormLoader.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\ProgrammingError;
+
+class FormLoader
+{
+ public static function load($name, Module $module = null)
+ {
+ if ($module === null) {
+ $basedir = Icinga::app()->getApplicationDir('forms');
+ $ns = '\\Icinga\\Web\\Forms\\';
+ } else {
+ $basedir = $module->getFormDir();
+ $ns = '\\Icinga\\Module\\' . ucfirst($module->getName()) . '\\Forms\\';
+ }
+ if (preg_match('~^[a-z0-9/]+$~i', $name)) {
+ $parts = preg_split('~/~', $name);
+ $class = ucfirst(array_pop($parts)) . 'Form';
+ $file = sprintf('%s/%s/%s.php', rtrim($basedir, '/'), implode('/', $parts), $class);
+ if (file_exists($file)) {
+ require_once($file);
+ $class = $ns . $class;
+ $options = array();
+ if ($module !== null) {
+ $options['icingaModule'] = $module;
+ }
+
+ return new $class($options);
+ }
+ }
+ throw new ProgrammingError(sprintf('Cannot load %s (%s), no such form', $name, $file));
+ }
+}
diff --git a/library/Businessprocess/Web/Form/QuickBaseForm.php b/library/Businessprocess/Web/Form/QuickBaseForm.php
new file mode 100644
index 0000000..3ef7b66
--- /dev/null
+++ b/library/Businessprocess/Web/Form/QuickBaseForm.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use ipl\Html\ValidHtml;
+use Zend_Form;
+
+abstract class QuickBaseForm extends Zend_Form implements ValidHtml
+{
+ /**
+ * The Icinga module this form belongs to. Usually only set if the
+ * form is initialized through the FormLoader
+ *
+ * @var Module
+ */
+ protected $icingaModule;
+
+ protected $icingaModuleName;
+
+ private $hintCount = 0;
+
+ public function __construct($options = null)
+ {
+ $this->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('<div class="hint">' . $html . '</div>', $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
new file mode 100644
index 0000000..c39b34b
--- /dev/null
+++ b/library/Businessprocess/Web/Form/QuickForm.php
@@ -0,0 +1,502 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Notification;
+use Icinga\Web\Request;
+use Icinga\Web\Response;
+use Icinga\Web\Url;
+use Exception;
+
+/**
+ * QuickForm wants to be a base class for simple forms
+ */
+abstract class QuickForm extends QuickBaseForm
+{
+ const ID = '__FORM_NAME';
+
+ const CSRF = '__FORM_CSRF';
+
+ /**
+ * The name of this form
+ */
+ protected $formName;
+
+ /**
+ * Whether the form has been sent
+ */
+ protected $hasBeenSent;
+
+ /**
+ * Whether the form has been sent
+ */
+ protected $hasBeenSubmitted;
+
+ /**
+ * The submit caption, element - still tbd
+ */
+ // protected $submit;
+
+ /**
+ * Our request
+ */
+ protected $request;
+
+ /**
+ * @var Url
+ */
+ protected $successUrl;
+
+ protected $successMessage;
+
+ protected $submitLabel;
+
+ protected $submitButtonName;
+
+ protected $deleteButtonName;
+
+ protected $fakeSubmitButtonName;
+
+ /**
+ * Whether form elements have already been created
+ */
+ protected $didSetup = false;
+
+ protected $isApiRequest = false;
+
+ public function __construct($options = null)
+ {
+ parent::__construct($options);
+
+ $this->setMethod('post');
+ $this->getActionFromRequest()
+ ->createIdElement()
+ ->regenerateCsrfToken()
+ ->setPreferredDecorators();
+ }
+
+ protected function getActionFromRequest()
+ {
+ $this->setAction(Url::fromRequest());
+ return $this;
+ }
+
+ protected function setPreferredDecorators()
+ {
+ $this->setAttrib('class', 'autofocus icinga-controls');
+ $this->setDecorators(
+ array(
+ 'Description',
+ array('FormErrors', array('onlyCustomFormErrors' => true)),
+ 'FormElements',
+ 'Form'
+ )
+ );
+
+ return $this;
+ }
+
+ protected function addSubmitButtonIfSet()
+ {
+ 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,
+ ));
+ }
+
+ protected function addSimpleDisplayGroup($elements, $name, $options)
+ {
+ 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()
+ {
+ $this->detectName();
+ $this->addHidden(self::ID, $this->getName());
+ $this->getElement(self::ID)->setIgnore(true);
+ return $this;
+ }
+
+ public function getSentValue($name, $default = null)
+ {
+ $request = $this->getRequest();
+ if ($request->isPost() && $this->hasBeenSent()) {
+ return $request->getPost($name);
+ } else {
+ return $default;
+ }
+ }
+
+ public function getSubmitLabel()
+ {
+ if ($this->submitLabel === null) {
+ return $this->translate('Submit');
+ }
+
+ return $this->submitLabel;
+ }
+
+ public function setSubmitLabel($label)
+ {
+ $this->submitLabel = $label;
+ return $this;
+ }
+
+ public function setApiRequest($isApiRequest = true)
+ {
+ $this->isApiRequest = $isApiRequest;
+ return $this;
+ }
+
+ public function isApiRequest()
+ {
+ return $this->isApiRequest;
+ }
+
+ public function regenerateCsrfToken()
+ {
+ if (! $element = $this->getElement(self::CSRF)) {
+ $this->addHidden(self::CSRF, CsrfToken::generate());
+ $element = $this->getElement(self::CSRF);
+ }
+ $element->setIgnore(true);
+
+ return $this;
+ }
+
+ public function removeCsrfToken()
+ {
+ $this->removeElement(self::CSRF);
+ return $this;
+ }
+
+ public function setSuccessUrl($url, $params = null)
+ {
+ if (! $url instanceof Url) {
+ $url = Url::fromPath($url);
+ }
+ if ($params !== null) {
+ $url->setParams($params);
+ }
+ $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()
+ {
+ }
+
+ protected function onSetup()
+ {
+ }
+
+ public function setAction($action)
+ {
+ if ($action instanceof Url) {
+ $action = $action->getAbsoluteUrl('&');
+ }
+
+ return parent::setAction($action);
+ }
+
+ public function hasBeenSubmitted()
+ {
+ if ($this->hasBeenSubmitted === null) {
+ $req = $this->getRequest();
+ if ($req->isPost()) {
+ 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;
+ }
+ }
+
+ 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())
+ {
+ }
+
+ public function prepareElements()
+ {
+ if (! $this->didSetup) {
+ $this->beforeSetup();
+ $this->setup();
+ $this->addSubmitButtonIfSet();
+ $this->onSetup();
+ $this->didSetup = true;
+ }
+
+ return $this;
+ }
+
+ public function handleRequest(Request $request = null)
+ {
+ if ($request === null) {
+ $request = $this->getRequest();
+ } else {
+ $this->setRequest($request);
+ }
+
+ $this->prepareElements();
+
+ if ($this->hasBeenSent()) {
+ $post = $request->getPost();
+ if ($this->hasBeenSubmitted()) {
+ $this->beforeValidation($post);
+ if ($this->isValid($post)) {
+ try {
+ $this->onSuccess();
+ } catch (Exception $e) {
+ $this->addException($e);
+ $this->onFailure();
+ }
+ } else {
+ $this->onFailure();
+ }
+ } else {
+ $this->setDefaults($post);
+ }
+ } else {
+ // Well...
+ }
+
+ return $this;
+ }
+
+ public function addException(Exception $e, $elementName = null)
+ {
+ $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 {
+ $this->addError($msg);
+ }
+ }
+
+ public function onSuccess()
+ {
+ $this->redirectOnSuccess();
+ }
+
+ public function setSuccessMessage($message)
+ {
+ $this->successMessage = $message;
+ return $this;
+ }
+
+ public function getSuccessMessage($message = null)
+ {
+ if ($message !== null) {
+ return $message;
+ }
+ if ($this->successMessage === null) {
+ return t('Form has successfully been sent');
+ }
+ return $this->successMessage;
+ }
+
+ public function redirectOnSuccess($message = null)
+ {
+ 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);
+ }
+
+ public function onFailure()
+ {
+ }
+
+ public function notifySuccess($message = null)
+ {
+ if ($message === null) {
+ $message = t('Form has successfully been sent');
+ }
+ Notification::success($message);
+ return $this;
+ }
+
+ public function notifyError($message)
+ {
+ Notification::error($message);
+ return $this;
+ }
+
+ protected function 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()
+ {
+ }
+
+ public function setRequest(Request $request)
+ {
+ if ($this->request !== null) {
+ throw new ProgrammingError('Unable to set request twice');
+ }
+
+ $this->request = $request;
+ $this->prepareElements();
+ $this->onRequest();
+ return $this;
+ }
+
+ /**
+ * @return Request
+ */
+ public function getRequest()
+ {
+ if ($this->request === null) {
+ /** @var Request $request */
+ $request = Icinga::app()->getFrontController()->getRequest();
+ $this->setRequest($request);
+ }
+ return $this->request;
+ }
+
+ public function hasBeenSent()
+ {
+ if ($this->hasBeenSent === null) {
+
+ /** @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;
+ }
+ }
+
+ return $this->hasBeenSent;
+ }
+
+ protected function detectName()
+ {
+ if ($this->formName !== null) {
+ $this->setName($this->formName);
+ } else {
+ $this->setName(get_class($this));
+ }
+ }
+}
diff --git a/library/Businessprocess/Web/Form/Validator/NoDuplicateChildrenValidator.php b/library/Businessprocess/Web/Form/Validator/NoDuplicateChildrenValidator.php
new file mode 100644
index 0000000..9676de0
--- /dev/null
+++ b/library/Businessprocess/Web/Form/Validator/NoDuplicateChildrenValidator.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form\Validator;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Forms\EditNodeForm;
+use Icinga\Module\Businessprocess\Web\Form\QuickForm;
+use Zend_Validate_Abstract;
+
+class NoDuplicateChildrenValidator extends Zend_Validate_Abstract
+{
+ const CHILD_FOUND = 'childFound';
+
+ /** @var QuickForm */
+ protected $form;
+
+ /** @var BpConfig */
+ protected $bp;
+
+ /** @var BpNode */
+ protected $parent;
+
+ /** @var string */
+ protected $label;
+
+ public function __construct(QuickForm $form, BpConfig $bp, BpNode $parent = null)
+ {
+ $this->form = $form;
+ $this->bp = $bp;
+ $this->parent = $parent;
+
+ $this->_messageVariables['label'] = 'label';
+ $this->_messageTemplates = [
+ self::CHILD_FOUND => mt('businessprocess', '%label% is already defined in this process')
+ ];
+ }
+
+ public function isValid($value)
+ {
+ if ($this->parent === null) {
+ $found = $this->bp->hasRootNode($value);
+ } elseif ($this->form instanceof EditNodeForm && $this->form->getNode()->getName() === $value) {
+ $found = false;
+ } else {
+ $found = $this->parent->hasChild($value);
+ }
+
+ if (! $found) {
+ return true;
+ }
+
+ $this->label = $this->form->getElement('children')->getMultiOptions()[$value];
+ $this->_error(self::CHILD_FOUND);
+ return false;
+ }
+}
diff --git a/library/Businessprocess/Web/Url.php b/library/Businessprocess/Web/Url.php
new file mode 100644
index 0000000..3c036d4
--- /dev/null
+++ b/library/Businessprocess/Web/Url.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Url as WebUrl;
+
+/**
+ * Class Url
+ *
+ * The main purpose of this class is to get unit tests running on CLI
+ *
+ * @package Icinga\Module\Businessprocess\Web
+ */
+class Url extends WebUrl
+{
+ protected static function getRequest()
+ {
+ $app = Icinga::app();
+ if ($app->isCli()) {
+ return new FakeRequest();
+ } else {
+ return $app->getRequest();
+ }
+ }
+}