summaryrefslogtreecommitdiffstats
path: root/library/Director/Web/Form
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
commitf66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch)
treefbff2135e7013f196b891bbde54618eb050e4aaf /library/Director/Web/Form
parentInitial commit. (diff)
downloadicingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.tar.xz
icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.zip
Adding upstream version 1.10.2.upstream/1.10.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Director/Web/Form')
-rw-r--r--library/Director/Web/Form/ClickHereForm.php31
-rw-r--r--library/Director/Web/Form/CloneImportSourceForm.php72
-rw-r--r--library/Director/Web/Form/CloneSyncRuleForm.php76
-rw-r--r--library/Director/Web/Form/CsrfToken.php53
-rw-r--r--library/Director/Web/Form/DbSelectorForm.php85
-rw-r--r--library/Director/Web/Form/Decorator/ViewHelperRaw.php14
-rw-r--r--library/Director/Web/Form/DirectorForm.php58
-rw-r--r--library/Director/Web/Form/DirectorObjectForm.php1734
-rw-r--r--library/Director/Web/Form/Element/Boolean.php90
-rw-r--r--library/Director/Web/Form/Element/DataFilter.php361
-rw-r--r--library/Director/Web/Form/Element/ExtensibleSet.php89
-rw-r--r--library/Director/Web/Form/Element/FormElement.php9
-rw-r--r--library/Director/Web/Form/Element/InstanceSummary.php51
-rw-r--r--library/Director/Web/Form/Element/OptionalYesNo.php22
-rw-r--r--library/Director/Web/Form/Element/SimpleNote.php34
-rw-r--r--library/Director/Web/Form/Element/StoredPassword.php62
-rw-r--r--library/Director/Web/Form/Element/Text.php16
-rw-r--r--library/Director/Web/Form/Element/YesNo.php14
-rw-r--r--library/Director/Web/Form/Filter/QueryColumnsFromSql.php48
-rw-r--r--library/Director/Web/Form/FormLoader.php43
-rw-r--r--library/Director/Web/Form/IcingaObjectFieldLoader.php628
-rw-r--r--library/Director/Web/Form/IconHelper.php89
-rw-r--r--library/Director/Web/Form/IplElement/ExtensibleSetElement.php570
-rw-r--r--library/Director/Web/Form/QuickBaseForm.php177
-rw-r--r--library/Director/Web/Form/QuickForm.php641
-rw-r--r--library/Director/Web/Form/QuickSubForm.php36
-rw-r--r--library/Director/Web/Form/Validate/IsDataListEntry.php55
-rw-r--r--library/Director/Web/Form/Validate/NamePattern.php38
28 files changed, 5196 insertions, 0 deletions
diff --git a/library/Director/Web/Form/ClickHereForm.php b/library/Director/Web/Form/ClickHereForm.php
new file mode 100644
index 0000000..abba9d7
--- /dev/null
+++ b/library/Director/Web/Form/ClickHereForm.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\Web\InlineForm;
+
+class ClickHereForm extends InlineForm
+{
+ use TranslationHelper;
+
+ protected $hasBeenClicked = false;
+
+ protected function assemble()
+ {
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('here'),
+ 'class' => 'link-button'
+ ]);
+ }
+
+ public function hasBeenClicked()
+ {
+ return $this->hasBeenClicked;
+ }
+
+ public function onSuccess()
+ {
+ $this->hasBeenClicked = true;
+ }
+}
diff --git a/library/Director/Web/Form/CloneImportSourceForm.php b/library/Director/Web/Form/CloneImportSourceForm.php
new file mode 100644
index 0000000..0849dd4
--- /dev/null
+++ b/library/Director/Web/Form/CloneImportSourceForm.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Module\Director\Data\Exporter;
+use ipl\Html\Form;
+use ipl\Html\FormDecorator\DdDtDecorator;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\Objects\ImportSource;
+
+class CloneImportSourceForm extends Form
+{
+ use TranslationHelper;
+
+ /** @var ImportSource */
+ protected $source;
+
+ /** @var ImportSource|null */
+ protected $newSource;
+
+ public function __construct(ImportSource $source)
+ {
+ $this->setDefaultElementDecorator(new DdDtDecorator());
+ $this->source = $source;
+ }
+
+ protected function assemble()
+ {
+ $this->addElement('text', 'source_name', [
+ 'label' => $this->translate('New name'),
+ 'value' => $this->source->get('source_name'),
+ ]);
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('Clone')
+ ]);
+ }
+
+ /**
+ * @return \Icinga\Module\Director\Db
+ */
+ protected function getTargetDb()
+ {
+ return $this->source->getConnection();
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ $db = $this->getTargetDb();
+ $export = (new Exporter($db))->export($this->source);
+ $newName = $this->getElement('source_name')->getValue();
+ $export->source_name = $newName;
+ unset($export->originalId);
+ if (ImportSource::existsWithName($newName, $db)) {
+ $this->getElement('source_name')->addMessage('Name already exists');
+ }
+ $this->newSource = ImportSource::import($export, $db);
+ $this->newSource->store();
+ }
+
+ public function getSuccessUrl()
+ {
+ if ($this->newSource === null) {
+ return parent::getSuccessUrl();
+ } else {
+ return Url::fromPath('director/importsource', ['id' => $this->newSource->get('id')]);
+ }
+ }
+}
diff --git a/library/Director/Web/Form/CloneSyncRuleForm.php b/library/Director/Web/Form/CloneSyncRuleForm.php
new file mode 100644
index 0000000..f90b593
--- /dev/null
+++ b/library/Director/Web/Form/CloneSyncRuleForm.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Module\Director\Data\Exporter;
+use ipl\Html\Form;
+use ipl\Html\FormDecorator\DdDtDecorator;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\Objects\SyncRule;
+
+class CloneSyncRuleForm extends Form
+{
+ use TranslationHelper;
+
+ /** @var SyncRule */
+ protected $rule;
+
+ /** @var SyncRule|null */
+ protected $newRule;
+
+ public function __construct(SyncRule $rule)
+ {
+ $this->setDefaultElementDecorator(new DdDtDecorator());
+ $this->rule = $rule;
+ }
+
+ protected function assemble()
+ {
+ $this->addElement('text', 'rule_name', [
+ 'label' => $this->translate('New name'),
+ 'value' => $this->rule->get('rule_name'),
+ ]);
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('Clone')
+ ]);
+ }
+
+ /**
+ * @return \Icinga\Module\Director\Db
+ */
+ protected function getTargetDb()
+ {
+ return $this->rule->getConnection();
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ $db = $this->getTargetDb();
+ $exporter = new Exporter($db);
+
+ $export = $exporter->export($this->rule);
+ $newName = $this->getValue('rule_name');
+ $export->rule_name = $newName;
+ unset($export->originalId);
+
+ if (SyncRule::existsWithName($newName, $db)) {
+ $this->getElement('rule_name')->addMessage('Name already exists');
+ }
+ $this->newRule = SyncRule::import($export, $db);
+ $this->newRule->store();
+ }
+
+ public function getSuccessUrl()
+ {
+ if ($this->newRule === null) {
+ return parent::getSuccessUrl();
+ } else {
+ return Url::fromPath('director/syncrule', ['id' => $this->newRule->get('id')]);
+ }
+ }
+}
diff --git a/library/Director/Web/Form/CsrfToken.php b/library/Director/Web/Form/CsrfToken.php
new file mode 100644
index 0000000..24edf88
--- /dev/null
+++ b/library/Director/Web/Form/CsrfToken.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Icinga\Module\Director\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('|', $elementValue);
+
+ 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/Director/Web/Form/DbSelectorForm.php b/library/Director/Web/Form/DbSelectorForm.php
new file mode 100644
index 0000000..52fe5ea
--- /dev/null
+++ b/library/Director/Web/Form/DbSelectorForm.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use gipfl\IcingaWeb2\Url;
+use Icinga\Web\Response;
+use ipl\Html\Form;
+use Icinga\Web\Window;
+
+class DbSelectorForm extends Form
+{
+ protected $defaultAttributes = [
+ 'class' => 'db-selector'
+ ];
+
+ protected $allowedNames;
+
+ /** @var Window */
+ protected $window;
+
+ protected $response;
+
+ public function __construct(Response $response, Window $window, $allowedNames)
+ {
+ $this->response = $response;
+ $this->window = $window;
+ $this->allowedNames = $allowedNames;
+ }
+
+ protected function assemble()
+ {
+ $this->addElement('hidden', 'DbSelector', [
+ 'value' => 'sent'
+ ]);
+ $this->addElement('select', 'db_resource', [
+ 'options' => $this->allowedNames,
+ 'class' => 'autosubmit',
+ 'value' => $this->getSession()->get('db_resource')
+ ]);
+ }
+
+ /**
+ * A base class should handle this, based on hidden fields
+ *
+ * @return bool
+ */
+ public function hasBeenSubmitted()
+ {
+ return $this->hasBeenSent() && $this->getRequestParam('DbSelector') === 'sent';
+ }
+
+ public function onSuccess()
+ {
+ $this->getSession()->set('db_resource', $this->getElement('db_resource')->getValue());
+ $this->response->redirectAndExit(Url::fromRequest($this->getRequest()));
+ }
+
+ protected function getRequestParam($name, $default = null)
+ {
+ $request = $this->getRequest();
+ if ($request === null) {
+ return $default;
+ }
+ if ($request->getMethod() === 'POST') {
+ $params = $request->getParsedBody();
+ } elseif ($this->getMethod() === 'GET') {
+ parse_str($request->getUri()->getQuery(), $params);
+ } else {
+ $params = [];
+ }
+
+ if (array_key_exists($name, $params)) {
+ return $params[$name];
+ }
+
+ return $default;
+ }
+ /**
+ * @return \Icinga\Web\Session\SessionNamespace
+ */
+ protected function getSession()
+ {
+ return $this->window->getSessionNamespace('director');
+ }
+}
diff --git a/library/Director/Web/Form/Decorator/ViewHelperRaw.php b/library/Director/Web/Form/Decorator/ViewHelperRaw.php
new file mode 100644
index 0000000..a3aefbf
--- /dev/null
+++ b/library/Director/Web/Form/Decorator/ViewHelperRaw.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Decorator;
+
+use Zend_Form_Decorator_ViewHelper as ViewHelper;
+use Zend_Form_Element as Element;
+
+class ViewHelperRaw extends ViewHelper
+{
+ public function getValue($element)
+ {
+ return $element->getUnfilteredValue();
+ }
+}
diff --git a/library/Director/Web/Form/DirectorForm.php b/library/Director/Web/Form/DirectorForm.php
new file mode 100644
index 0000000..145be5b
--- /dev/null
+++ b/library/Director/Web/Form/DirectorForm.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Director\Db;
+
+abstract class DirectorForm extends QuickForm
+{
+ /** @var Db */
+ protected $db;
+
+ /**
+ * @param Db $db
+ * @return $this
+ */
+ public function setDb(Db $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * @return Db
+ */
+ public function getDb()
+ {
+ return $this->db;
+ }
+
+ /**
+ * @return static
+ */
+ public static function load()
+ {
+ return new static([
+ 'icingaModule' => Icinga::App()->getModuleManager()->getModule('director')
+ ]);
+ }
+
+ public function addBoolean($key, $options, $default = null)
+ {
+ if ($default === null) {
+ return $this->addElement('OptionalYesNo', $key, $options);
+ } else {
+ $this->addElement('YesNo', $key, $options);
+ return $this->getElement($key)->setValue($default);
+ }
+ }
+
+ protected function optionalBoolean($key, $label, $description)
+ {
+ return $this->addBoolean($key, array(
+ 'label' => $label,
+ 'description' => $description
+ ));
+ }
+}
diff --git a/library/Director/Web/Form/DirectorObjectForm.php b/library/Director/Web/Form/DirectorObjectForm.php
new file mode 100644
index 0000000..b70bd7b
--- /dev/null
+++ b/library/Director/Web/Form/DirectorObjectForm.php
@@ -0,0 +1,1734 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Exception;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\Hook\IcingaObjectFormHook;
+use Icinga\Module\Director\IcingaConfig\StateFilterSet;
+use Icinga\Module\Director\IcingaConfig\TypeFilterSet;
+use Icinga\Module\Director\Objects\IcingaTemplateChoice;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Util;
+use Icinga\Module\Director\Web\Form\Element\ExtensibleSet;
+use Icinga\Module\Director\Web\Form\Validate\NamePattern;
+use Zend_Form_Element as ZfElement;
+use Zend_Form_Element_Select as ZfSelect;
+
+abstract class DirectorObjectForm extends DirectorForm
+{
+ const GROUP_ORDER_OBJECT_DEFINITION = 20;
+ const GROUP_ORDER_RELATED_OBJECTS = 25;
+ const GROUP_ORDER_ASSIGN = 30;
+ const GROUP_ORDER_CHECK_EXECUTION = 40;
+ const GROUP_ORDER_CUSTOM_FIELDS = 50;
+ const GROUP_ORDER_CUSTOM_FIELD_CATEGORIES = 60;
+ const GROUP_ORDER_EVENT_FILTERS = 700;
+ const GROUP_ORDER_EXTRA_INFO = 750;
+ const GROUP_ORDER_CLUSTERING = 800;
+ const GROUP_ORDER_BUTTONS = 1000;
+
+ /** @var IcingaObject */
+ protected $object;
+
+ /** @var Branch */
+ protected $branch;
+
+ protected $objectName;
+
+ protected $className;
+
+ protected $deleteButtonName;
+
+ protected $displayGroups = [];
+
+ protected $resolvedImports;
+
+ protected $listUrl;
+
+ /** @var Auth */
+ private $auth;
+
+ private $choiceElements = [];
+
+ protected $preferredObjectType;
+
+ /** @var IcingaObjectFieldLoader */
+ protected $fieldLoader;
+
+ private $allowsExperimental;
+
+ private $presetImports;
+
+ private $earlyProperties = array(
+ // 'imports',
+ 'check_command',
+ 'check_command_id',
+ 'has_agent',
+ 'command',
+ 'command_id',
+ 'event_command',
+ 'event_command_id',
+ );
+
+ public function setPreferredObjectType($type)
+ {
+ $this->preferredObjectType = $type;
+ return $this;
+ }
+
+ public function setAuth(Auth $auth)
+ {
+ $this->auth = $auth;
+ return $this;
+ }
+
+ public function getAuth()
+ {
+ if ($this->auth === null) {
+ $this->auth = Auth::getInstance();
+ }
+ return $this->auth;
+ }
+
+ protected function eventuallyAddNameRestriction($restrictionName)
+ {
+ $restrictions = $this->getAuth()->getRestrictions($restrictionName);
+ if (! empty($restrictions)) {
+ $this->getElement('object_name')->addValidator(
+ new NamePattern($restrictions)
+ );
+ }
+
+ return $this;
+ }
+
+ public function presetImports($imports)
+ {
+ if (! empty($imports)) {
+ if (is_array($imports)) {
+ $this->presetImports = $imports;
+ } else {
+ $this->presetImports = array($imports);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return DbObject|DbObjectWithSettings|IcingaObject
+ */
+ protected function object()
+ {
+ if ($this->object === null) {
+ $values = $this->getValues();
+ /** @var DbObject|IcingaObject $class */
+ $class = $this->getObjectClassname();
+ if ($this->preferredObjectType) {
+ $values['object_type'] = $this->preferredObjectType;
+ }
+ if ($this->presetImports) {
+ $values['imports'] = $this->presetImports;
+ }
+
+ $this->object = $class::create($values, $this->db);
+ } else {
+ if (! $this->object->hasConnection()) {
+ $this->object->setConnection($this->db);
+ }
+ }
+
+ return $this->object;
+ }
+
+ protected function extractChoicesFromPost($post)
+ {
+ $imports = [];
+
+ foreach ($this->choiceElements as $other) {
+ $name = $other->getName();
+ if (array_key_exists($name, $post)) {
+ $value = $post[$name];
+ if (is_string($value)) {
+ $imports[] = $value;
+ } elseif (is_array($value)) {
+ foreach ($value as $chosen) {
+ $imports[] = $chosen;
+ }
+ }
+ }
+ }
+
+ return $imports;
+ }
+
+ protected function assertResolvedImports()
+ {
+ if ($this->resolvedImports !== null) {
+ return $this->resolvedImports;
+ }
+
+ $object = $this->object;
+
+ if (! $object instanceof IcingaObject) {
+ return $this->setResolvedImports(false);
+ }
+ if (! $object->supportsImports()) {
+ return $this->setResolvedImports(false);
+ }
+
+ if ($this->hasBeenSent()) {
+ // prefill special properties, required to resolve fields and similar
+ $post = $this->getRequest()->getPost();
+
+ $key = 'imports';
+ if ($el = $this->getElement($key)) {
+ if (array_key_exists($key, $post)) {
+ $imports = $post[$key];
+ if (! is_array($imports)) {
+ $imports = array($imports);
+ }
+ $imports = array_filter(array_values(array_merge(
+ $imports,
+ $this->extractChoicesFromPost($post)
+ )), 'strlen');
+
+ /** @var ZfElement $el */
+ $this->populate([$key => $imports]);
+ $el->setValue($imports);
+ if (! $this->tryToSetObjectPropertyFromElement($object, $el, $key)) {
+ return $this->resolvedImports = false;
+ }
+ }
+ } elseif ($this->presetImports) {
+ $imports = array_values(array_merge(
+ $this->presetImports,
+ $this->extractChoicesFromPost($post)
+ ));
+ if (! $this->eventuallySetImports($imports)) {
+ return $this->resolvedImports = false;
+ }
+ } else {
+ if (! empty($this->choiceElements)) {
+ if (! $this->eventuallySetImports($this->extractChoicesFromPost($post))) {
+ return $this->resolvedImports = false;
+ }
+ }
+ }
+
+ foreach ($this->earlyProperties as $key) {
+ if ($el = $this->getElement($key)) {
+ if (array_key_exists($key, $post)) {
+ $this->populate([$key => $post[$key]]);
+ $this->tryToSetObjectPropertyFromElement($object, $el, $key);
+ }
+ }
+ }
+ }
+
+ try {
+ $object->listAncestorIds();
+ } catch (NestingError $e) {
+ $this->addUniqueErrorMessage($e->getMessage());
+ return $this->resolvedImports = false;
+ } catch (Exception $e) {
+ $this->addException($e, 'imports');
+ return $this->resolvedImports = false;
+ }
+
+ return $this->setResolvedImports();
+ }
+
+ protected function eventuallySetImports($imports)
+ {
+ try {
+ $this->object()->set('imports', $imports);
+ return true;
+ } catch (Exception $e) {
+ $this->addException($e, 'imports');
+ return false;
+ }
+ }
+
+ protected function tryToSetObjectPropertyFromElement(
+ IcingaObject $object,
+ ZfElement $element,
+ $key
+ ) {
+ $old = null;
+ try {
+ $old = $object->get($key);
+ $object->set($key, $element->getValue());
+ $object->resolveUnresolvedRelatedProperties();
+
+ if ($key === 'imports') {
+ $object->imports()->getObjects();
+ }
+ return true;
+ } catch (Exception $e) {
+ if ($old !== null) {
+ $object->set($key, $old);
+ }
+ $this->addException($e, $key);
+ return false;
+ }
+ }
+
+ public function setResolvedImports($resolved = true)
+ {
+ return $this->resolvedImports = $resolved;
+ }
+
+ public function isObject()
+ {
+ return $this->getSentOrObjectValue('object_type') === 'object';
+ }
+
+ public function isTemplate()
+ {
+ return $this->getSentOrObjectValue('object_type') === 'template';
+ }
+
+ // TODO: move to a subform
+ protected function handleRanges(IcingaObject $object, &$values)
+ {
+ if (! $object->supportsRanges()) {
+ return;
+ }
+
+ $key = 'ranges';
+ $object = $this->object();
+
+ /* Sample:
+
+ array(
+ 'monday' => 'eins',
+ 'tuesday' => '00:00-24:00',
+ 'sunday' => 'zwei',
+ );
+
+ */
+ if (array_key_exists($key, $values)) {
+ $object->ranges()->set($values[$key]);
+ unset($values[$key]);
+ }
+
+ foreach ($object->ranges()->getRanges() as $key => $value) {
+ $this->addRange($key, $value);
+ }
+ }
+
+ protected function addToCheckExecutionDisplayGroup($elements)
+ {
+ return $this->addElementsToGroup(
+ $elements,
+ 'check_execution',
+ self::GROUP_ORDER_CHECK_EXECUTION,
+ $this->translate('Check execution')
+ );
+ }
+
+ public function addElementsToGroup($elements, $group, $order, $legend = null)
+ {
+ if (! is_array($elements)) {
+ $elements = array($elements);
+ }
+
+ // These are optional elements, they might exist or not. We still want
+ // to see exception for other ones
+ $skipLegally = array('check_period_id');
+
+ $skip = array();
+ foreach ($elements as $k => $v) {
+ if (is_string($v)) {
+ $el = $this->getElement($v);
+ if (!$el && in_array($v, $skipLegally)) {
+ $skip[] = $k;
+ continue;
+ }
+
+ $elements[$k] = $el;
+ }
+ }
+
+ foreach ($skip as $k) {
+ unset($elements[$k]);
+ }
+
+ if (! array_key_exists($group, $this->displayGroups)) {
+ $this->addDisplayGroup($elements, $group, array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => $order,
+ 'legend' => $legend ?: $group,
+ ));
+ $this->displayGroups[$group] = $this->getDisplayGroup($group);
+ } else {
+ $this->displayGroups[$group]->addElements($elements);
+ }
+
+ return $this->displayGroups[$group];
+ }
+
+ protected function handleProperties(DbObject $object, &$values)
+ {
+ if ($this->hasBeenSent()) {
+ foreach ($values as $key => $value) {
+ try {
+ if ($key === 'imports' && ! empty($this->choiceElements)) {
+ if (! is_array($value)) {
+ $value = [$value];
+ }
+ foreach ($this->choiceElements as $element) {
+ $chosen = $element->getValue();
+ if (is_string($chosen)) {
+ $value[] = $chosen;
+ } elseif (is_array($chosen)) {
+ foreach ($chosen as $choice) {
+ $value[] = $choice;
+ }
+ }
+ }
+ }
+ $object->set($key, $value);
+ if ($object instanceof IcingaObject) {
+ if ($this->resolvedImports !== false) {
+ $object->imports()->getObjects();
+ }
+ }
+ } catch (Exception $e) {
+ $this->addException($e, $key);
+ }
+ }
+ }
+ }
+
+ protected function loadInheritedProperties()
+ {
+ if ($this->assertResolvedImports()) {
+ try {
+ $this->showInheritedProperties($this->object());
+ } catch (Exception $e) {
+ $this->addException($e);
+ }
+ }
+ }
+
+ protected function showInheritedProperties(IcingaObject $object)
+ {
+ $inherited = $object->getInheritedProperties();
+ $origins = $object->getOriginsProperties();
+
+ foreach ($inherited as $k => $v) {
+ if ($v !== null && $k !== 'object_name') {
+ $el = $this->getElement($k);
+ if ($el) {
+ $this->setInheritedValue($el, $inherited->$k, $origins->$k);
+ } elseif (substr($k, -3) === '_id') {
+ $k = substr($k, 0, -3);
+ $el = $this->getElement($k);
+ if ($el) {
+ $this->setInheritedValue(
+ $el,
+ $object->getRelatedObjectName($k, $v),
+ $origins->{"${k}_id"}
+ );
+ }
+ }
+ }
+ }
+ }
+
+ protected function prepareFields($object)
+ {
+ if ($this->assertResolvedImports()) {
+ $this->fieldLoader = new IcingaObjectFieldLoader($object);
+ $this->fieldLoader->prepareElements($this);
+ }
+
+ return $this;
+ }
+
+ protected function setCustomVarValues($values)
+ {
+ if ($this->fieldLoader) {
+ $this->fieldLoader->setValues($values, 'var_');
+ }
+
+ return $this;
+ }
+
+ protected function addFields()
+ {
+ if ($this->fieldLoader) {
+ $this->fieldLoader->addFieldsToForm($this);
+ $this->onAddedFields();
+ }
+ }
+
+ protected function onAddedFields()
+ {
+ }
+
+ // TODO: remove, used in sets I guess
+ protected function fieldLoader($object)
+ {
+ if ($this->fieldLoader === null) {
+ $this->fieldLoader = new IcingaObjectFieldLoader($object);
+ }
+
+ return $this->fieldLoader;
+ }
+
+ protected function isNew()
+ {
+ return $this->object === null || ! $this->object->hasBeenLoadedFromDb();
+ }
+
+ protected function setButtons()
+ {
+ if ($this->isNew()) {
+ $this->setSubmitLabel(
+ $this->translate('Add')
+ );
+ } else {
+ $this->setSubmitLabel(
+ $this->translate('Store')
+ );
+ $this->addDeleteButton();
+ }
+ }
+
+ /**
+ * @param bool $importsFirst
+ * @return $this
+ */
+ protected function groupMainProperties($importsFirst = false)
+ {
+ if ($importsFirst) {
+ $elements = [
+ 'imports',
+ 'object_type',
+ 'object_name',
+ ];
+ } else {
+ $elements = [
+ 'object_type',
+ 'object_name',
+ 'imports',
+ ];
+ }
+ $elements = array_merge($elements, [
+ 'display_name',
+ 'host',
+ 'host_id',
+ 'address',
+ 'address6',
+ 'groups',
+ 'inherited_groups',
+ 'applied_groups',
+ 'users',
+ 'user_groups',
+ 'apply_to',
+ 'command_id', // Notification
+ 'notification_interval',
+ 'period_id',
+ 'times_begin',
+ 'times_end',
+ 'email',
+ 'pager',
+ 'enable_notifications',
+ 'disable_checks', //Dependencies
+ 'disable_notifications',
+ 'ignore_soft_states',
+ 'apply_for',
+ 'create_live',
+ 'disabled',
+ ]);
+
+ // Add template choices to the main section
+ /** @var \Zend_Form_Element $el */
+ foreach ($this->getElements() as $key => $el) {
+ if (substr($el->getName(), 0, 6) === 'choice') {
+ $elements[] = $key;
+ }
+ }
+
+ $this->addDisplayGroup($elements, 'object_definition', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_OBJECT_DEFINITION,
+ 'legend' => $this->translate('Main properties')
+ ));
+
+ return $this;
+ }
+
+ protected function setSentValue($name, $value)
+ {
+ if ($this->hasBeenSent()) {
+ $request = $this->getRequest();
+ if ($value !== null && $request->isPost() && $request->getPost($name) !== null) {
+ $request->setPost($name, $value);
+ }
+ }
+
+ $this->setElementValue($name, $value);
+ }
+
+ public function setElementValue($name, $value = null)
+ {
+ $el = $this->getElement($name);
+ if (! $el) {
+ // Not showing an error, as most object properties do not exist. Not
+ // yet, because IMO this should be checked.
+ // $this->addError(sprintf($this->translate('Form element "%s" does not exist'), $name));
+ return;
+ }
+
+ if ($value !== null) {
+ $el->setValue($value);
+ }
+ }
+
+ public function setInheritedValue(ZfElement $el, $inherited, $inheritedFrom)
+ {
+ if ($inherited === null) {
+ return;
+ }
+
+ $txtInherited = sprintf($this->translate(' (inherited from "%s")'), $inheritedFrom);
+ if ($el instanceof ZfSelect) {
+ $multi = $el->getMultiOptions();
+ if (is_bool($inherited)) {
+ $inherited = $inherited ? 'y' : 'n';
+ }
+ if (is_scalar($inherited) && array_key_exists($inherited, $multi)) {
+ $multi[null] = $multi[$inherited] . $txtInherited;
+ } else {
+ $multi[null] = $this->stringifyInheritedValue($inherited) . $txtInherited;
+ }
+ $el->setMultiOptions($multi);
+ } elseif ($el instanceof ExtensibleSet) {
+ $el->setAttrib('inherited', $inherited);
+ $el->setAttrib('inheritedFrom', $inheritedFrom);
+ } else {
+ if (is_string($inherited) || is_int($inherited)) {
+ $el->setAttrib('placeholder', $inherited . $txtInherited);
+ }
+ }
+
+ // We inherited a value, so no need to require the field
+ $el->setRequired(false);
+ }
+
+ protected function stringifyInheritedValue($value)
+ {
+ return is_scalar($value) ? $value : substr(json_encode($value), 0, 40);
+ }
+
+ public function setListUrl($url)
+ {
+ $this->listUrl = $url;
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object();
+ if ($object->hasBeenModified()) {
+ if (! $object->hasBeenLoadedFromDb()) {
+ $this->setHttpResponseCode(201);
+ }
+
+ $msg = sprintf(
+ $object->hasBeenLoadedFromDb()
+ ? $this->translate('The %s has successfully been stored')
+ : $this->translate('A new %s has successfully been created'),
+ $this->translate($this->getObjectShortClassName())
+ );
+ $this->getDbObjectStore()->store($object);
+ } else {
+ if ($this->isApiRequest()) {
+ $this->setHttpResponseCode(304);
+ }
+ $msg = $this->translate('No action taken, object has not been modified');
+ }
+
+ $this->setObjectSuccessUrl();
+ $this->beforeSuccessfulRedirect();
+ $this->redirectOnSuccess($msg);
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ $object = $this->object();
+
+ if ($object instanceof IcingaObject) {
+ $params = $object->getUrlParams();
+ $url = Url::fromPath($this->getAction());
+ if ($url->hasParam('dbResourceName')) {
+ $params['dbResourceName'] = $url->getParam('dbResourceName');
+ }
+ $this->setSuccessUrl(
+ 'director/' . strtolower($this->getObjectShortClassName()),
+ $params
+ );
+ } elseif ($object->hasProperty('id')) {
+ $this->setSuccessUrl($this->getSuccessUrl()->with('id', $object->getProperty('id')));
+ }
+ }
+
+ protected function beforeSuccessfulRedirect()
+ {
+ }
+
+ public function hasElement($name)
+ {
+ return $this->getElement($name) !== null;
+ }
+
+ public function getObject()
+ {
+ return $this->object;
+ }
+
+ public function hasObject()
+ {
+ return $this->object !== null;
+ }
+
+ public function isIcingaObject()
+ {
+ if ($this->object !== null) {
+ return $this->object instanceof IcingaObject;
+ }
+
+ /** @var DbObject $class */
+ $class = $this->getObjectClassname();
+ $instance = $class::create();
+
+ return $instance instanceof IcingaObject;
+ }
+
+ public function isMultiObjectForm()
+ {
+ return false;
+ }
+
+ public function setObject(DbObject $object)
+ {
+ $this->object = $object;
+ if ($this->db === null) {
+ /** @var Db $connection */
+ $connection = $object->getConnection();
+ $this->setDb($connection);
+ }
+
+ return $this;
+ }
+
+ protected function getObjectClassname()
+ {
+ if ($this->className === null) {
+ return 'Icinga\\Module\\Director\\Objects\\'
+ . substr(join('', array_slice(explode('\\', get_class($this)), -1)), 0, -4);
+ }
+
+ return $this->className;
+ }
+
+ protected function getObjectShortClassName()
+ {
+ if ($this->objectName === null) {
+ $className = substr(strrchr(get_class($this), '\\'), 1);
+ if (substr($className, 0, 6) === 'Icinga') {
+ return substr($className, 6, -4);
+ } else {
+ return substr($className, 0, -4);
+ }
+ }
+
+ return $this->objectName;
+ }
+
+ protected function removeFromSet(&$set, $key)
+ {
+ unset($set[$key]);
+ }
+
+ protected function moveUpInSet(&$set, $key)
+ {
+ list($set[$key - 1], $set[$key]) = array($set[$key], $set[$key - 1]);
+ }
+
+ protected function moveDownInSet(&$set, $key)
+ {
+ list($set[$key + 1], $set[$key]) = array($set[$key], $set[$key + 1]);
+ }
+
+ protected function beforeSetup()
+ {
+ if (!$this->hasBeenSent()) {
+ return;
+ }
+
+ $post = $values = $this->getRequest()->getPost();
+
+ foreach ($post as $key => $value) {
+ if (preg_match('/^(.+?)_(\d+)__(MOVE_DOWN|MOVE_UP|REMOVE)$/', $key, $m)) {
+ $values[$m[1]] = array_filter($values[$m[1]], 'strlen');
+ switch ($m[3]) {
+ case 'MOVE_UP':
+ $this->moveUpInSet($values[$m[1]], $m[2]);
+ break;
+ case 'MOVE_DOWN':
+ $this->moveDownInSet($values[$m[1]], $m[2]);
+ break;
+ case 'REMOVE':
+ $this->removeFromSet($values[$m[1]], $m[2]);
+ break;
+ }
+
+ $this->getRequest()->setPost($m[1], $values[$m[1]]);
+ }
+ }
+ }
+
+ protected function onRequest()
+ {
+ if ($this->object !== null) {
+ $this->setDefaultsFromObject($this->object);
+ }
+ $this->prepareFields($this->object());
+ IcingaObjectFormHook::callOnSetup($this);
+ if ($this->hasBeenSent()) {
+ $this->handlePost();
+ }
+ try {
+ $this->loadInheritedProperties();
+ $this->addFields();
+ $this->callOnRequestCallables();
+ } catch (Exception $e) {
+ $this->addUniqueException($e);
+
+ return;
+ }
+
+ if ($this->shouldBeDeleted()) {
+ $this->deleteObject($this->object());
+ }
+ }
+
+ protected function handlePost()
+ {
+ $object = $this->object();
+
+ $post = $this->getRequest()->getPost();
+ $this->populate($post);
+ $values = $this->getValues();
+
+ if ($object instanceof IcingaObject) {
+ $this->setCustomVarValues($post);
+ }
+
+ $this->handleProperties($object, $values);
+
+ // TODO: get rid of this
+ if ($object instanceof IcingaObject) {
+ $this->handleRanges($object, $values);
+ }
+ }
+
+ protected function setDefaultsFromObject(DbObject $object)
+ {
+ /** @var ZfElement $element */
+ foreach ($this->getElements() as $element) {
+ $key = $element->getName();
+ if ($object->hasProperty($key)) {
+ $value = $object->get($key);
+ if ($object instanceof IcingaObject) {
+ if ($object->propertyIsRelatedSet($key)) {
+ if (! count((array) $value)) {
+ continue;
+ }
+ }
+ }
+
+ if ($value !== null && $value !== []) {
+ $element->setValue($value);
+ }
+ }
+ }
+ }
+
+ protected function deleteObject($object)
+ {
+ if ($object instanceof IcingaObject && $object->hasProperty('object_name')) {
+ $msg = sprintf(
+ '%s "%s" has been removed',
+ $this->translate($this->getObjectShortClassName()),
+ $object->getObjectName()
+ );
+ } else {
+ $msg = sprintf(
+ '%s has been removed',
+ $this->translate($this->getObjectShortClassName())
+ );
+ }
+
+ if ($this->listUrl) {
+ $url = $this->listUrl;
+ } elseif ($object instanceof IcingaObject && $object->hasProperty('object_name')) {
+ $url = $object->getOnDeleteUrl();
+ } else {
+ $url = $this->getSuccessUrl()->without(
+ array('field_id', 'argument_id', 'range', 'range_type')
+ );
+ }
+
+ if ($this->getDbObjectStore()->delete($object)) {
+ $this->setSuccessUrl($url);
+ }
+ $this->redirectOnSuccess($msg);
+ }
+
+ /**
+ * @return DbObjectStore
+ */
+ protected function getDbObjectStore()
+ {
+ $store = new DbObjectStore($this->getDb(), $this->branch);
+ return $store;
+ }
+
+ protected function addDeleteButton($label = null)
+ {
+ $object = $this->object;
+
+ if ($label === null) {
+ $label = $this->translate('Delete');
+ }
+
+ $el = $this->createElement('submit', $label)
+ ->setLabel($label)
+ ->setDecorators(array('ViewHelper'));
+ //->removeDecorator('Label');
+
+ $this->deleteButtonName = $el->getName();
+
+ if ($object instanceof IcingaObject && $object->isTemplate()) {
+ if ($cnt = $object->countDirectDescendants()) {
+ $el->setAttrib('disabled', 'disabled');
+ $el->setAttrib(
+ 'title',
+ sprintf(
+ $this->translate('This template is still in use by %d other objects'),
+ $cnt
+ )
+ );
+ }
+ } elseif ($object instanceof IcingaCommand && $object->isInUse()) {
+ $el->setAttrib('disabled', 'disabled');
+ $el->setAttrib(
+ 'title',
+ sprintf(
+ $this->translate('This Command is still in use by %d other objects'),
+ $object->countDirectUses()
+ )
+ );
+ }
+
+ $this->addElement($el);
+
+ return $this;
+ }
+
+ public function hasDeleteButton()
+ {
+ return $this->deleteButtonName !== null;
+ }
+
+ public function shouldBeDeleted()
+ {
+ if (! $this->hasDeleteButton()) {
+ return false;
+ }
+
+ $name = $this->deleteButtonName;
+ return $this->getSentValue($name) === $this->getElement($name)->getLabel();
+ }
+
+ public function abortDeletion()
+ {
+ if ($this->hasDeleteButton()) {
+ $this->setSentValue($this->deleteButtonName, 'ABORTED');
+ }
+ }
+
+ public function getSentOrResolvedObjectValue($name, $default = null)
+ {
+ return $this->getSentOrObjectValue($name, $default, true);
+ }
+
+ public function getSentOrObjectValue($name, $default = null, $resolved = false)
+ {
+ // TODO: check whether getSentValue is still needed since element->getValue
+ // is in place (currently for form element default values only)
+
+ if (!$this->hasObject()) {
+ if ($this->hasBeenSent()) {
+ return $this->getSentValue($name, $default);
+ } else {
+ if ($name === 'object_type' && $this->preferredObjectType) {
+ return $this->preferredObjectType;
+ }
+ if ($name === 'imports' && $this->presetImports) {
+ return $this->presetImports;
+ }
+ if ($this->valueIsEmpty($val = $this->getValue($name))) {
+ return $default;
+ } else {
+ return $val;
+ }
+ }
+ }
+
+ if ($this->hasBeenSent()) {
+ if (!$this->valueIsEmpty($value = $this->getSentValue($name))) {
+ return $value;
+ }
+ }
+
+ $object = $this->getObject();
+
+ if ($object->hasProperty($name)) {
+ if ($resolved && $object->supportsImports()) {
+ if ($this->assertResolvedImports()) {
+ $objectProperty = $object->getResolvedProperty($name);
+ } else {
+ $objectProperty = $object->$name;
+ }
+ } else {
+ $objectProperty = $object->$name;
+ }
+ } else {
+ $objectProperty = null;
+ }
+
+ if ($objectProperty !== null) {
+ return $objectProperty;
+ }
+
+ if (($el = $this->getElement($name)) && !$this->valueIsEmpty($val = $el->getValue())) {
+ return $val;
+ }
+
+ return $default;
+ }
+
+ public function loadObject($id)
+ {
+ if ($this->branch && $this->branch->isBranch()) {
+ throw new \RuntimeException('Calling loadObject from form in a branch');
+ }
+ /** @var DbObject $class */
+ $class = $this->getObjectClassname();
+ if (is_int($id)) {
+ $this->object = $class::loadWithAutoIncId($id, $this->db);
+ if ($this->object->getKeyName() === 'id') {
+ $this->addHidden('id', $id);
+ }
+ } else {
+ $this->object = $class::load($id, $this->db);
+ }
+
+
+ return $this;
+ }
+
+ protected function addRange($key, $range)
+ {
+ $this->addElement('text', 'range_' . $key, array(
+ 'label' => 'ranges.' . $key,
+ 'value' => $range->range_value
+ ));
+ }
+
+ /**
+ * @param Db $db
+ * @return $this
+ */
+ public function setDb(Db $db)
+ {
+ if ($this->object !== null) {
+ $this->object->setConnection($db);
+ }
+
+ parent::setDb($db);
+ return $this;
+ }
+
+ public function optionallyAddFromEnum($enum)
+ {
+ return array(
+ null => $this->translate('- click to add more -')
+ ) + $enum;
+ }
+
+ protected function addObjectTypeElement()
+ {
+ if (!$this->isNew()) {
+ return $this;
+ }
+
+ if ($this->preferredObjectType) {
+ $this->addHidden('object_type', $this->preferredObjectType);
+ return $this;
+ }
+
+ $object = $this->object();
+
+ if ($object->supportsImports()) {
+ $templates = $this->enumAllowedTemplates();
+
+ if (empty($templates) && $this->getObjectShortClassName() !== 'Command') {
+ $types = array('template' => $this->translate('Template'));
+ } else {
+ $types = array(
+ 'object' => $this->translate('Object'),
+ 'template' => $this->translate('Template'),
+ );
+ }
+ } else {
+ $types = array('object' => $this->translate('Object'));
+ }
+
+ if ($this->object()->supportsApplyRules()) {
+ $types['apply'] = $this->translate('Apply rule');
+ }
+
+ $this->addElement('select', 'object_type', array(
+ 'label' => $this->translate('Object type'),
+ 'description' => $this->translate(
+ 'What kind of object this should be. Templates allow full access'
+ . ' to any property, they are your building blocks for "real" objects.'
+ . ' External objects should usually not be manually created or modified.'
+ . ' They allow you to work with objects locally defined on your Icinga nodes,'
+ . ' while not rendering and deploying them with the Director. Apply rules allow'
+ . ' to assign services, notifications and groups to other objects.'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($types),
+ 'class' => 'autosubmit'
+ ));
+
+ return $this;
+ }
+
+ protected function hasObjectType()
+ {
+ if (!$this->object()->hasProperty('object_type')) {
+ return false;
+ }
+
+ return ! $this->valueIsEmpty($this->getSentOrObjectValue('object_type'));
+ }
+
+ protected function addZoneElement($all = false)
+ {
+ if ($all || $this->isTemplate()) {
+ $zones = $this->db->enumZones();
+ } else {
+ $zones = $this->db->enumNonglobalZones();
+ }
+
+ $this->addElement('select', 'zone_id', array(
+ 'label' => $this->translate('Cluster Zone'),
+ 'description' => $this->translate(
+ 'Icinga cluster zone. Allows to manually override Directors decisions'
+ . ' of where to deploy your config to. You should consider not doing so'
+ . ' unless you gained deep understanding of how an Icinga Cluster stack'
+ . ' works'
+ ),
+ 'multiOptions' => $this->optionalEnum($zones)
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @param $type
+ * @return $this
+ */
+ protected function addChoices($type)
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $connection = $this->getDb();
+ $choiceType = 'TemplateChoice' . ucfirst($type);
+ $table = "icinga_$type";
+ $choices = IcingaObject::loadAllByType($choiceType, $connection);
+ $chosenTemplates = $this->getSentOrObjectValue('imports');
+ $db = $connection->getDbAdapter();
+ if (empty($chosenTemplates)) {
+ $importedIds = [];
+ } else {
+ $importedIds = $db->fetchCol(
+ $db->select()->from($table, 'id')
+ ->where('object_name in (?)', (array)$chosenTemplates)
+ ->where('object_type = ?', 'template')
+ );
+ }
+
+ foreach ($choices as $choice) {
+ $required = $choice->get('required_template_id');
+ if ($required === null || in_array($required, $importedIds, false)) {
+ $this->addChoiceElement($choice);
+ }
+ }
+
+ return $this;
+ }
+
+ protected function addChoiceElement(IcingaTemplateChoice $choice)
+ {
+ $imports = $this->object()->listImportNames();
+ $element = $choice->createFormElement($this, $imports);
+ $this->addElement($element);
+ $this->choiceElements[$element->getName()] = $element;
+ return $this;
+ }
+
+ /**
+ * @param bool $required
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addImportsElement($required = null)
+ {
+ if ($this->presetImports) {
+ return $this;
+ }
+
+ if (in_array($this->getObjectShortClassName(), ['TimePeriod', 'ScheduledDowntime'])) {
+ $required = false;
+ } else {
+ $required = $required !== null ? $required : !$this->isTemplate();
+ }
+ $enum = $this->enumAllowedTemplates();
+ if (empty($enum)) {
+ if ($required) {
+ if ($this->hasBeenSent()) {
+ $this->addError($this->translate('No template has been chosen'));
+ } else {
+ if ($this->hasPermission('director/admin')) {
+ $html = $this->translate('Please define a related template first');
+ } else {
+ $html = $this->translate('No related template has been provided yet');
+ }
+ $this->addHtml('<p class="warning">' . $html . '</p>');
+ }
+ }
+ return $this;
+ }
+
+ $db = $this->getDb()->getDbAdapter();
+ $object = $this->object;
+ if ($object->supportsChoices()) {
+ $choiceNames = $db->fetchCol(
+ $db->select()->from(
+ $this->object()->getTableName(),
+ 'object_name'
+ )->where('template_choice_id IS NOT NULL')
+ );
+ } else {
+ $choiceNames = [];
+ }
+
+ $type = $object->getShortTableName();
+ $this->addElement('extensibleSet', 'imports', array(
+ 'label' => $this->translate('Imports'),
+ 'description' => $this->translate(
+ 'Importable templates, add as many as you want. Please note that order'
+ . ' matters when importing properties from multiple templates: last one'
+ . ' wins'
+ ),
+ 'required' => $required,
+ 'spellcheck' => 'false',
+ 'hideOptions' => $choiceNames,
+ 'suggest' => "${type}templates",
+ // 'multiOptions' => $this->optionallyAddFromEnum($enum),
+ 'sorted' => true,
+ 'value' => $this->presetImports,
+ 'class' => 'autosubmit'
+ ));
+
+ return $this;
+ }
+
+ protected function addDisabledElement()
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addBoolean(
+ 'disabled',
+ array(
+ 'label' => $this->translate('Disabled'),
+ 'description' => $this->translate('Disabled objects will not be deployed')
+ ),
+ 'n'
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addGroupDisplayNameElement()
+ {
+ $this->addElement('text', 'display_name', array(
+ 'label' => $this->translate('Display Name'),
+ 'description' => $this->translate(
+ 'An alternative display name for this group. If you wonder how this'
+ . ' could be helpful just leave it blank'
+ )
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @param bool $force
+ *
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addCheckCommandElements($force = false)
+ {
+ if (! $force && ! $this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('text', 'check_command', array(
+ 'label' => $this->translate('Check command'),
+ 'description' => $this->translate('Check command definition'),
+ // 'multiOptions' => $this->optionalEnum($this->db->enumCheckcommands()),
+ 'class' => 'autosubmit director-suggest', // This influences fields
+ 'data-suggestion-context' => 'checkcommandnames',
+ 'value' => $this->getObject()->get('check_command')
+ ));
+ $this->getDisplayGroup('object_definition')
+ // ->addElement($this->getElement('check_command_id'))
+ ->addElement($this->getElement('check_command'));
+
+ $eventCommands = $this->db->enumEventcommands();
+
+ if (! empty($eventCommands)) {
+ $this->addElement('select', 'event_command_id', array(
+ 'label' => $this->translate('Event command'),
+ 'description' => $this->translate('Event command definition'),
+ 'multiOptions' => $this->optionalEnum($eventCommands),
+ 'class' => 'autosubmit',
+ ));
+ $this->addToCheckExecutionDisplayGroup('event_command_id');
+ }
+
+ return $this;
+ }
+
+ protected function addCheckExecutionElements($force = false)
+ {
+ if (! $force && ! $this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'text',
+ 'check_interval',
+ array(
+ 'label' => $this->translate('Check interval'),
+ 'description' => $this->translate('Your regular check interval')
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'retry_interval',
+ array(
+ 'label' => $this->translate('Retry interval'),
+ 'description' => $this->translate(
+ 'Retry interval, will be applied after a state change unless the next hard state is reached'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'max_check_attempts',
+ array(
+ 'label' => $this->translate('Max check attempts'),
+ 'description' => $this->translate(
+ 'Defines after how many check attempts a new hard state is reached'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'check_timeout',
+ array(
+ 'label' => $this->translate('Check timeout'),
+ 'description' => $this->translate(
+ "Check command timeout in seconds. Overrides the CheckCommand's timeout attribute"
+ )
+ )
+ );
+
+ $periods = $this->db->enumTimeperiods();
+
+ if (!empty($periods)) {
+ $this->addElement(
+ 'select',
+ 'check_period_id',
+ array(
+ 'label' => $this->translate('Check period'),
+ 'description' => $this->translate(
+ 'The name of a time period which determines when this'
+ . ' object should be monitored. Not limited by default.'
+ ),
+ 'multiOptions' => $this->optionalEnum($periods),
+ )
+ );
+ }
+
+ $this->optionalBoolean(
+ 'enable_active_checks',
+ $this->translate('Execute active checks'),
+ $this->translate('Whether to actively check this object')
+ );
+
+ $this->optionalBoolean(
+ 'enable_passive_checks',
+ $this->translate('Accept passive checks'),
+ $this->translate('Whether to accept passive check results for this object')
+ );
+
+ $this->optionalBoolean(
+ 'enable_notifications',
+ $this->translate('Send notifications'),
+ $this->translate('Whether to send notifications for this object')
+ );
+
+ $this->optionalBoolean(
+ 'enable_event_handler',
+ $this->translate('Enable event handler'),
+ $this->translate('Whether to enable event handlers this object')
+ );
+
+ $this->optionalBoolean(
+ 'enable_perfdata',
+ $this->translate('Process performance data'),
+ $this->translate('Whether to process performance data provided by this object')
+ );
+
+ $this->optionalBoolean(
+ 'enable_flapping',
+ $this->translate('Enable flap detection'),
+ $this->translate('Whether flap detection is enabled on this object')
+ );
+
+ $this->addElement(
+ 'text',
+ 'flapping_threshold_high',
+ array(
+ 'label' => $this->translate('Flapping threshold (high)'),
+ 'description' => $this->translate(
+ 'Flapping upper bound in percent for a service to be considered flapping'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'flapping_threshold_low',
+ array(
+ 'label' => $this->translate('Flapping threshold (low)'),
+ 'description' => $this->translate(
+ 'Flapping lower bound in percent for a service to be considered not flapping'
+ )
+ )
+ );
+
+ $this->optionalBoolean(
+ 'volatile',
+ $this->translate('Volatile'),
+ $this->translate('Whether this check is volatile.')
+ );
+
+ $elements = array(
+ 'check_interval',
+ 'retry_interval',
+ 'max_check_attempts',
+ 'check_timeout',
+ 'check_period_id',
+ 'enable_active_checks',
+ 'enable_passive_checks',
+ 'enable_notifications',
+ 'enable_event_handler',
+ 'enable_perfdata',
+ 'enable_flapping',
+ 'flapping_threshold_high',
+ 'flapping_threshold_low',
+ 'volatile'
+ );
+ $this->addToCheckExecutionDisplayGroup($elements);
+
+ return $this;
+ }
+
+ protected function enumAllowedTemplates()
+ {
+ $object = $this->object();
+ $tpl = $this->db->enumIcingaTemplates($object->getShortTableName());
+ if (empty($tpl)) {
+ return [];
+ }
+
+ $id = $object->get('id');
+
+ if (array_key_exists($id, $tpl)) {
+ unset($tpl[$id]);
+ }
+
+ return array_combine($tpl, $tpl);
+ }
+
+ protected function addExtraInfoElements()
+ {
+ $this->addElement('textarea', 'notes', array(
+ 'label' => $this->translate('Notes'),
+ 'description' => $this->translate(
+ 'Additional notes for this object'
+ ),
+ 'rows' => 2,
+ 'columns' => 60,
+ ));
+
+ $this->addElement('text', 'notes_url', array(
+ 'label' => $this->translate('Notes URL'),
+ 'description' => $this->translate(
+ 'An URL pointing to additional notes for this object'
+ ),
+ ));
+
+ $this->addElement('text', 'action_url', array(
+ 'label' => $this->translate('Action URL'),
+ 'description' => $this->translate(
+ 'An URL leading to additional actions for this object. Often used'
+ . ' with Icinga Classic, rarely with Icinga Web 2 as it provides'
+ . ' far better possibilities to integrate addons'
+ ),
+ ));
+
+ $this->addElement('text', 'icon_image', array(
+ 'label' => $this->translate('Icon image'),
+ 'description' => $this->translate(
+ 'An URL pointing to an icon for this object. Try "tux.png" for icons'
+ . ' relative to public/img/icons or "cloud" (no extension) for items'
+ . ' from the Icinga icon font'
+ ),
+ ));
+
+ $this->addElement('text', 'icon_image_alt', array(
+ 'label' => $this->translate('Icon image alt'),
+ 'description' => $this->translate(
+ 'Alternative text to be shown in case above icon is missing'
+ ),
+ ));
+
+ $elements = array(
+ 'notes',
+ 'notes_url',
+ 'action_url',
+ 'icon_image',
+ 'icon_image_alt',
+ );
+
+ $this->addDisplayGroup($elements, 'extrainfo', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_EXTRA_INFO,
+ 'legend' => $this->translate('Additional properties')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * Add an assign_filter form element
+ *
+ * Forms should use this helper method for objects using the typical
+ * assign_filter column
+ *
+ * @param array $properties Form element properties
+ *
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addAssignFilter($properties)
+ {
+ if (!$this->object || !$this->object->supportsAssignments()) {
+ return $this;
+ }
+
+ $this->addFilterElement('assign_filter', $properties);
+ $el = $this->getElement('assign_filter');
+
+ $this->addDisplayGroup(array($el), 'assign', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_ASSIGN,
+ 'legend' => $this->translate('Assign where')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * Add a dataFilter element with fitting decorators
+ *
+ * TODO: Evaluate whether parts or all of this could be moved to the element
+ * class.
+ *
+ * @param string $name Element name
+ * @param array $properties Form element properties
+ *
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addFilterElement($name, $properties)
+ {
+ $this->addElement('dataFilter', $name, $properties);
+ $el = $this->getElement($name);
+
+ $ddClass = 'full-width';
+ if (array_key_exists('required', $properties) && $properties['required']) {
+ $ddClass .= ' required';
+ }
+
+ $el->clearDecorators()
+ ->addDecorator('ViewHelper')
+ ->addDecorator('Errors')
+ ->addDecorator('Description', array('tag' => 'p', 'class' => 'description'))
+ ->addDecorator('HtmlTag', array(
+ 'tag' => 'dd',
+ 'class' => $ddClass,
+ ));
+
+ return $this;
+ }
+
+ protected function addEventFilterElements($elements = array('states','types'))
+ {
+ if (in_array('states', $elements)) {
+ $this->addElement('extensibleSet', 'states', array(
+ 'label' => $this->translate('States'),
+ 'multiOptions' => $this->optionallyAddFromEnum($this->enumStates()),
+ 'description' => $this->translate(
+ 'The host/service states you want to get notifications for'
+ ),
+ ));
+ }
+
+ if (in_array('types', $elements)) {
+ $this->addElement('extensibleSet', 'types', array(
+ 'label' => $this->translate('Transition types'),
+ 'multiOptions' => $this->optionallyAddFromEnum($this->enumTypes()),
+ 'description' => $this->translate(
+ 'The state transition types you want to get notifications for'
+ ),
+ ));
+ }
+
+ $this->addDisplayGroup($elements, 'event_filters', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_EVENT_FILTERS,
+ 'legend' => $this->translate('State and transition type filters')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @param string $permission
+ * @return bool
+ */
+ public function hasPermission($permission)
+ {
+ return Util::hasPermission($permission);
+ }
+
+ public function setBranch(Branch $branch)
+ {
+ $this->branch = $branch;
+
+ return $this;
+ }
+
+ protected function allowsExperimental()
+ {
+ // NO, it is NOT a good idea to use this. You'll break your monitoring
+ // and nobody will help you.
+ if ($this->allowsExperimental === null) {
+ $this->allowsExperimental = $this->db->settings()->get(
+ 'experimental_features'
+ ) === 'allow';
+ }
+
+ return $this->allowsExperimental;
+ }
+
+ protected function enumStates()
+ {
+ $set = new StateFilterSet();
+ return $set->enumAllowedValues();
+ }
+
+ protected function enumTypes()
+ {
+ $set = new TypeFilterSet();
+ return $set->enumAllowedValues();
+ }
+}
diff --git a/library/Director/Web/Form/Element/Boolean.php b/library/Director/Web/Form/Element/Boolean.php
new file mode 100644
index 0000000..b2402c7
--- /dev/null
+++ b/library/Director/Web/Form/Element/Boolean.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Zend_Form_Element_Select as ZfSelect;
+
+/**
+ * Input control for booleans
+ */
+class Boolean extends ZfSelect
+{
+ public $options = array(
+ null => '- please choose -',
+ 'y' => 'Yes',
+ 'n' => 'No',
+ );
+
+ public function getValue()
+ {
+ $value = $this->getUnfilteredValue();
+
+ if ($value === 'y' || $value === true) {
+ return true;
+ } elseif ($value === 'n' || $value === false) {
+ return false;
+ }
+
+ return null;
+ }
+
+ public function isValid($value, $context = null)
+ {
+ if ($value === 'y' || $value === 'n') {
+ $this->setValue($value);
+ return true;
+ }
+
+ return parent::isValid($value, $context);
+ }
+
+ /**
+ * @param string $value
+ * @param string $key
+ * @codingStandardsIgnoreStart
+ */
+ protected function _filterValue(&$value, &$key)
+ {
+ // @codingStandardsIgnoreEnd
+ if ($value === true) {
+ $value = 'y';
+ } elseif ($value === false) {
+ $value = 'n';
+ } elseif ($value === '') {
+ $value = null;
+ }
+
+ parent::_filterValue($value, $key);
+ }
+
+ public function setValue($value)
+ {
+ if ($value === true) {
+ $value = 'y';
+ } elseif ($value === false) {
+ $value = 'n';
+ } elseif ($value === '') {
+ $value = null;
+ }
+
+ return parent::setValue($value);
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ */
+ protected function _translateOption($option, $value)
+ {
+ // @codingStandardsIgnoreEnd
+ if (!isset($this->_translated[$option]) && !empty($value)) {
+ $this->options[$option] = mt('director', $value);
+ if ($this->options[$option] === $value) {
+ return false;
+ }
+ $this->_translated[$option] = true;
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/library/Director/Web/Form/Element/DataFilter.php b/library/Director/Web/Form/Element/DataFilter.php
new file mode 100644
index 0000000..adae07d
--- /dev/null
+++ b/library/Director/Web/Form/Element/DataFilter.php
@@ -0,0 +1,361 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Web\Form\IconHelper;
+use Exception;
+
+/**
+ * Input control for extensible sets
+ */
+class DataFilter extends FormElement
+{
+ /**
+ * Default form view helper to use for rendering
+ * @var string
+ */
+ public $helper = 'formDataFilter';
+
+ private $addTo;
+
+ private $removeFilter;
+
+ private $stripFilter;
+
+ /** @var FilterChain */
+ private $filter;
+
+ public function getValue()
+ {
+ $value = parent::getValue();
+ if ($value !== null && $this->isEmpty($value)) {
+ $value = null;
+ }
+
+ return $value;
+ }
+
+ protected function isEmpty(Filter $filter)
+ {
+ return $filter->isEmpty() || $this->isEmptyExpression($filter);
+ }
+
+ protected function isEmptyExpression(Filter $filter)
+ {
+ return $filter instanceof FilterExpression &&
+ $filter->getColumn() === '' &&
+ $filter->getExpression() === '""'; // -> json_encode('')
+ }
+
+ /**
+ * @inheritdoc
+ * @codingStandardsIgnoreStart
+ */
+ protected function _filterValue(&$value, &$key)
+ {
+ // @codingStandardsIgnoreEnd
+ try {
+ if ($value instanceof Filter) {
+ // OK
+ } elseif (is_string($value)) {
+ $value = Filter::fromQueryString($value);
+ } elseif (is_array($value) || is_null($value)) {
+ $value = $this->arrayToFilter($value);
+ } else {
+ throw new ProgrammingError(
+ 'Value to be filtered has to be Filter, string, array or null'
+ );
+ }
+ } catch (Exception $e) {
+ $value = null;
+ // TODO: getFile, getLine
+ // Hint: cannot addMessage at it would loop through getValue
+ $this->addErrorMessage($e->getMessage());
+ $this->_isErrorForced = true;
+ }
+ }
+
+ /**
+ * This method transforms filter form data into a filter
+ * and reacts on pressed buttons
+ *
+ * @param array|null $array
+ *
+ * @return FilterChain|null
+ */
+ protected function arrayToFilter($array)
+ {
+ if ($array === null) {
+ return null;
+ }
+
+ $this->filter = null;
+ foreach ($array as $id => $entry) {
+ $filterId = $this->idToFilterId($id);
+ $sub = $this->entryToFilter($entry);
+ $this->checkEntryForActions($filterId, $entry);
+ $parentId = $this->parentIdFor($filterId);
+
+ if ($this->filter === null) {
+ $this->filter = $sub;
+ } else {
+ $this->filter->getById($parentId)->addFilter($sub);
+ }
+ }
+
+ $this->removeFilterIfRequested()
+ ->stripFilterIfRequested()
+ ->addNewFilterIfRequested()
+ ->fixNotsWithMultipleChildren();
+
+ return $this->filter;
+ }
+
+ protected function removeFilterIfRequested()
+ {
+ if ($this->removeFilter !== null) {
+ if ($this->filter->getById($this->removeFilter)->isRootNode()) {
+ $this->filter = $this->emptyExpression();
+ } else {
+ $this->filter->removeId($this->removeFilter);
+ }
+ }
+
+ return $this;
+ }
+
+
+ protected function stripFilterIfRequested()
+ {
+ if ($this->stripFilter !== null) {
+ $strip = $this->stripFilter;
+ $subId = $strip . '-1';
+ if ($this->filter->getId() === $strip) {
+ $this->filter = $this->filter->getById($subId);
+ } else {
+ $this->filter->replaceById($strip, $this->filter->getById($subId));
+ }
+ }
+
+ return $this;
+ }
+
+ protected function addNewFilterIfRequested()
+ {
+ if ($this->addTo !== null) {
+ $parent = $this->filter->getById($this->addTo);
+
+ if ($parent instanceof FilterChain) {
+ if ($parent->isEmpty()) {
+ $parent->addFilter($this->emptyExpression());
+ } else {
+ $parent->addFilter($this->emptyExpression());
+ }
+ } elseif ($parent instanceof FilterExpression) {
+ $replacement = Filter::matchAll(clone($parent));
+ if ($parent->isRootNode()) {
+ $this->filter = $replacement;
+ } else {
+ $this->filter->replaceById($parent->getId(), $replacement);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ protected function fixNotsWithMultipleChildren()
+ {
+ $this->filter = $this->fixNotsWithMultipleChildrenForFilter($this->filter);
+ return $this;
+ }
+
+ protected function fixNotsWithMultipleChildrenForFilter(Filter $filter)
+ {
+ if ($filter instanceof FilterChain) {
+ if ($filter->getOperatorName() === 'NOT') {
+ if ($filter->count() > 1) {
+ $filter = $this->notToNotAnd($filter);
+ }
+ }
+ /** @var Filter $sub */
+ foreach ($filter->filters() as $sub) {
+ $filter->replaceById(
+ $sub->getId(),
+ $this->fixNotsWithMultipleChildrenForFilter($sub)
+ );
+ }
+ }
+
+ return $filter;
+ }
+
+ protected function notToNotAnd(FilterChain $not)
+ {
+ $and = Filter::matchAll();
+ foreach ($not->filters() as $sub) {
+ $and->addFilter(clone($sub));
+ }
+
+ return Filter::not($and);
+ }
+
+ protected function emptyExpression()
+ {
+ return Filter::expression('', '=', '');
+ }
+
+ protected function parentIdFor($id)
+ {
+ if (false === ($pos = strrpos($id, '-'))) {
+ return '0';
+ } else {
+ return substr($id, 0, $pos);
+ }
+ }
+
+ protected function idToFilterId($id)
+ {
+ if (! preg_match('/^id_(new_)?(\d+(?:-\d+)*)$/', $id, $m)) {
+ die('nono' . $id);
+ }
+
+ return $m[2];
+ }
+
+ protected function checkEntryForActions($filterId, $entry)
+ {
+ switch ($this->entryAction($entry)) {
+ case 'cancel':
+ $this->removeFilter = $filterId;
+ break;
+
+ case 'minus':
+ $this->stripFilter = $filterId;
+ break;
+
+ case 'plus':
+ case 'angle-double-right':
+ $this->addTo = $filterId;
+ break;
+ }
+ }
+
+ /**
+ * Transforms a single submitted form component from an array
+ * into a Filter object
+ *
+ * @param array $entry The array as submitted through the form
+ *
+ * @return Filter
+ */
+ protected function entryToFilter($entry)
+ {
+ if (array_key_exists('operator', $entry)) {
+ return Filter::chain($entry['operator']);
+ } else {
+ return $this->entryToFilterExpression($entry);
+ }
+ }
+
+ protected function entryToFilterExpression($entry)
+ {
+ if ($entry['sign'] === 'true') {
+ return Filter::expression(
+ $entry['column'],
+ '=',
+ json_encode(true)
+ );
+ } elseif ($entry['sign'] === 'false') {
+ return Filter::expression(
+ $entry['column'],
+ '=',
+ json_encode(false)
+ );
+ } elseif ($entry['sign'] === 'in') {
+ if (array_key_exists('value', $entry)) {
+ if (is_array($entry['value'])) {
+ $value = array_filter($entry['value'], 'strlen');
+ } elseif (empty($entry['value'])) {
+ $value = array();
+ } else {
+ $value = array($entry['value']);
+ }
+ } else {
+ $value = array();
+ }
+ return Filter::expression(
+ $entry['column'],
+ '=',
+ json_encode($value)
+ );
+ } elseif ($entry['sign'] === 'contains') {
+ $value = array_key_exists('value', $entry) ? $entry['value'] : null;
+
+ return Filter::expression(
+ json_encode($value),
+ '=',
+ $entry['column']
+ );
+ } else {
+ $value = array_key_exists('value', $entry) ? $entry['value'] : null;
+
+ return Filter::expression(
+ $entry['column'],
+ $entry['sign'],
+ json_encode($value)
+ );
+ }
+ }
+
+ protected function entryAction($entry)
+ {
+ if (array_key_exists('action', $entry)) {
+ return IconHelper::instance()->characterIconName($entry['action']);
+ }
+
+ return null;
+ }
+
+ protected function hasIncompleteExpressions(Filter $filter)
+ {
+ if ($filter instanceof FilterChain) {
+ foreach ($filter->filters() as $sub) {
+ if ($this->hasIncompleteExpressions($sub)) {
+ return true;
+ }
+ }
+
+ return false;
+ } else {
+ /** @var FilterExpression $filter */
+ if ($filter->isRootNode() && $this->isEmptyExpression($filter)) {
+ return false;
+ }
+
+ return $filter->getColumn() === '';
+ }
+ }
+
+ public function isValid($value, $context = null)
+ {
+ if (! $value instanceof Filter) {
+ // TODO: try, return false on E
+ $filter = $this->arrayToFilter($value);
+ $this->setValue($filter);
+ } else {
+ $filter = $value;
+ }
+
+ if ($this->hasIncompleteExpressions($filter)) {
+ $this->addError('The configured filter is incomplete');
+ return false;
+ }
+
+ return parent::isValid($value);
+ }
+}
diff --git a/library/Director/Web/Form/Element/ExtensibleSet.php b/library/Director/Web/Form/Element/ExtensibleSet.php
new file mode 100644
index 0000000..f3c968f
--- /dev/null
+++ b/library/Director/Web/Form/Element/ExtensibleSet.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use InvalidArgumentException;
+
+/**
+ * Input control for extensible sets
+ */
+class ExtensibleSet extends FormElement
+{
+ /**
+ * Default form view helper to use for rendering
+ * @var string
+ */
+ public $helper = 'formIplExtensibleSet';
+
+ // private $multiOptions;
+
+ public function getValue()
+ {
+ $value = parent::getValue();
+ if (is_string($value) || is_numeric($value)) {
+ $value = [$value];
+ } elseif ($value === null) {
+ return $value;
+ }
+ if (! is_array($value)) {
+ throw new InvalidArgumentException(sprintf(
+ 'ExtensibleSet expects to work with Arrays, got %s',
+ var_export($value, 1)
+ ));
+ }
+ $value = array_filter($value, 'strlen');
+
+ if (empty($value)) {
+ return null;
+ }
+
+ return $value;
+ }
+
+ /**
+ * We do not want one message per entry
+ *
+ * @codingStandardsIgnoreStart
+ */
+ protected function _getErrorMessages()
+ {
+ return $this->_errorMessages;
+ // @codingStandardsIgnoreEnd
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ */
+ protected function _filterValue(&$value, &$key)
+ {
+ // @codingStandardsIgnoreEnd
+ if (is_array($value)) {
+ $value = array_filter($value, 'strlen');
+ } elseif (is_string($value) && !strlen($value)) {
+ $value = null;
+ }
+
+ parent::_filterValue($value, $key);
+ }
+
+ public function isValid($value, $context = null)
+ {
+ if ($value === null) {
+ $value = [];
+ }
+
+ $value = array_filter($value, 'strlen');
+ $this->setValue($value);
+ if ($this->isRequired() && empty($value)) {
+ // TODO: translate
+ $this->addError('You are required to choose at least one element');
+ return false;
+ }
+
+ if ($this->hasErrors()) {
+ return false;
+ }
+
+ return parent::isValid($value, $context);
+ }
+}
diff --git a/library/Director/Web/Form/Element/FormElement.php b/library/Director/Web/Form/Element/FormElement.php
new file mode 100644
index 0000000..c327859
--- /dev/null
+++ b/library/Director/Web/Form/Element/FormElement.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Zend_Form_Element_Xhtml;
+
+class FormElement extends Zend_Form_Element_Xhtml
+{
+}
diff --git a/library/Director/Web/Form/Element/InstanceSummary.php b/library/Director/Web/Form/Element/InstanceSummary.php
new file mode 100644
index 0000000..722ad26
--- /dev/null
+++ b/library/Director/Web/Form/Element/InstanceSummary.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Html;
+
+/**
+ * Used by the
+ */
+class InstanceSummary extends FormElement
+{
+ public $helper = 'formSimpleNote';
+
+ /**
+ * Always ignore this element
+ * @codingStandardsIgnoreStart
+ *
+ * @var boolean
+ */
+ protected $_ignore = true;
+ // @codingStandardsIgnoreEnd
+
+ private $instances;
+
+ /** @var array will be set via options */
+ protected $linkParams;
+
+ public function setValue($value)
+ {
+ $this->instances = $value;
+ return $this;
+ }
+
+ public function getValue()
+ {
+ return Html::tag('span', [
+ Html::tag('italic', 'empty'),
+ ' ',
+ Link::create('Manage Instances', 'director/data/dictionary', $this->linkParams, [
+ 'data-base-target' => '_next',
+ 'class' => 'icon-forward'
+ ])
+ ]);
+ }
+
+ public function isValid($value, $context = null)
+ {
+ return true;
+ }
+}
diff --git a/library/Director/Web/Form/Element/OptionalYesNo.php b/library/Director/Web/Form/Element/OptionalYesNo.php
new file mode 100644
index 0000000..7ef6d7f
--- /dev/null
+++ b/library/Director/Web/Form/Element/OptionalYesNo.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+/**
+ * Input control for booleans, gives y/n
+ */
+class OptionalYesNo extends Boolean
+{
+ public function getValue()
+ {
+ $value = $this->getUnfilteredValue();
+
+ if ($value === 'y' || $value === true) {
+ return 'y';
+ } elseif ($value === 'n' || $value === false) {
+ return 'n';
+ }
+
+ return null;
+ }
+}
diff --git a/library/Director/Web/Form/Element/SimpleNote.php b/library/Director/Web/Form/Element/SimpleNote.php
new file mode 100644
index 0000000..3097e11
--- /dev/null
+++ b/library/Director/Web/Form/Element/SimpleNote.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Icinga\Module\Director\PlainObjectRenderer;
+use ipl\Html\ValidHtml;
+
+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;
+ }
+
+ public function setValue($value)
+ {
+ if (is_object($value) && ! $value instanceof ValidHtml) {
+ $value = 'Unexpected object: ' . PlainObjectRenderer::render($value);
+ }
+
+ return parent::setValue($value);
+ }
+}
diff --git a/library/Director/Web/Form/Element/StoredPassword.php b/library/Director/Web/Form/Element/StoredPassword.php
new file mode 100644
index 0000000..fa0545b
--- /dev/null
+++ b/library/Director/Web/Form/Element/StoredPassword.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Zend_Form_Element_Text as ZfText;
+
+/**
+ * StoredPassword
+ *
+ * This is a special form field and it might look a little bit weird at first
+ * sight. It's main use-case are stored cleartext passwords a user should be
+ * allowed to change.
+ *
+ * While this might sound simple, it's quite tricky if you try to fulfill the
+ * following requirements:
+ *
+ * - the current password should not be rendered to the HTML page (unless the
+ * user decides to change it)
+ * - it must be possible to visually distinct whether a password has been set
+ * - it should be impossible to "see" the length of the stored password
+ * - a changed password must be persisted
+ * - forms might be subject to multiple submissions in case other fields fail.
+ * If the user changed the password during the first submission attempt, the
+ * new string should not be lost.
+ * - all this must happen within the bounds of ZF1 form elements and related
+ * view helpers. This means that there is no related context available - and
+ * we do not know whether the form has been submitted and whether the current
+ * values have been populated from DB
+ *
+ * @package Icinga\Module\Director\Web\Form\Element
+ */
+class StoredPassword extends ZfText
+{
+ const UNCHANGED = '__UNCHANGED_VALUE__';
+
+ public $helper = 'formStoredPassword';
+
+ public function setValue($value)
+ {
+ if (\is_array($value) && isset($value['_value'], $value['_sent'])
+ && $value['_sent'] === 'y'
+ ) {
+ $value = $sentValue = $value['_value'];
+ if ($sentValue !== self::UNCHANGED) {
+ $this->setAttrib('sentValue', $sentValue);
+ }
+ } else {
+ $sentValue = null;
+ }
+
+ if ($value === self::UNCHANGED) {
+ return $this;
+ } else {
+ // Workaround for issue with modified DataTypes. This is Director-specific
+ if (\is_array($value)) {
+ $value = \json_encode($value);
+ }
+
+ return parent::setValue((string) $value);
+ }
+ }
+}
diff --git a/library/Director/Web/Form/Element/Text.php b/library/Director/Web/Form/Element/Text.php
new file mode 100644
index 0000000..eeb36f1
--- /dev/null
+++ b/library/Director/Web/Form/Element/Text.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Zend_Form_Element_Text as ZfText;
+
+class Text extends ZfText
+{
+ public function setValue($value)
+ {
+ if (\is_array($value)) {
+ $value = \json_encode($value);
+ }
+ return parent::setValue((string) $value);
+ }
+}
diff --git a/library/Director/Web/Form/Element/YesNo.php b/library/Director/Web/Form/Element/YesNo.php
new file mode 100644
index 0000000..3e8aaa7
--- /dev/null
+++ b/library/Director/Web/Form/Element/YesNo.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+/**
+ * Input control for booleans, gives y/n
+ */
+class YesNo extends OptionalYesNo
+{
+ public $options = array(
+ 'y' => 'Yes',
+ 'n' => 'No',
+ );
+}
diff --git a/library/Director/Web/Form/Filter/QueryColumnsFromSql.php b/library/Director/Web/Form/Filter/QueryColumnsFromSql.php
new file mode 100644
index 0000000..6f6d475
--- /dev/null
+++ b/library/Director/Web/Form/Filter/QueryColumnsFromSql.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Filter;
+
+use Exception;
+use Icinga\Data\Db\DbConnection;
+use Icinga\Module\Director\Forms\ImportSourceForm;
+use Zend_Filter_Interface;
+
+class QueryColumnsFromSql implements Zend_Filter_Interface
+{
+ /** @var ImportSourceForm */
+ private $form;
+
+ public function __construct(ImportSourceForm $form)
+ {
+ $this->form = $form;
+ }
+
+ public function filter($value)
+ {
+ $form = $this->form;
+ if (empty($value) || $form->hasChangedSetting('query')) {
+ try {
+ return implode(
+ ', ',
+ $this->getQueryColumns($form->getSentOrObjectSetting('query'))
+ );
+ } catch (Exception $e) {
+ $this->form->addUniqueException($e);
+ return '';
+ }
+ } else {
+ return $value;
+ }
+ }
+
+ protected function getQueryColumns($query)
+ {
+ $resourceName = $this->form->getSentOrObjectSetting('resource');
+ if (! $resourceName) {
+ return [];
+ }
+ $db = DbConnection::fromResourceName($resourceName)->getDbAdapter();
+
+ return array_keys((array) current($db->fetchAll($query)));
+ }
+}
diff --git a/library/Director/Web/Form/FormLoader.php b/library/Director/Web/Form/FormLoader.php
new file mode 100644
index 0000000..ea82857
--- /dev/null
+++ b/library/Director/Web/Form/FormLoader.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\ProgrammingError;
+use RuntimeException;
+
+class FormLoader
+{
+ public static function load($name, Module $module = null)
+ {
+ if ($module === null) {
+ try {
+ $basedir = Icinga::app()->getApplicationDir('forms');
+ } catch (ProgrammingError $e) {
+ throw new RuntimeException($e->getMessage(), 0, $e);
+ }
+ $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 RuntimeException(sprintf('Cannot load %s (%s), no such form', $name, $file));
+ }
+}
diff --git a/library/Director/Web/Form/IcingaObjectFieldLoader.php b/library/Director/Web/Form/IcingaObjectFieldLoader.php
new file mode 100644
index 0000000..c900edf
--- /dev/null
+++ b/library/Director/Web/Form/IcingaObjectFieldLoader.php
@@ -0,0 +1,628 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Hook\HostFieldHook;
+use Icinga\Module\Director\Hook\ServiceFieldHook;
+use Icinga\Module\Director\Objects\DirectorDatafieldCategory;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\ObjectApplyMatches;
+use Icinga\Web\Hook;
+use stdClass;
+use Zend_Db_Select as ZfSelect;
+use Zend_Form_Element as ZfElement;
+
+class IcingaObjectFieldLoader
+{
+ protected $form;
+
+ /** @var IcingaObject */
+ protected $object;
+
+ /** @var \Icinga\Module\Director\Db */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var DirectorDatafield[] */
+ protected $fields;
+
+ protected $elements;
+
+ protected $forceNull = array();
+
+ /** @var array Map element names to variable names 'elName' => 'varName' */
+ protected $nameMap = array();
+
+ public function __construct(IcingaObject $object)
+ {
+ $this->object = $object;
+ $this->connection = $object->getConnection();
+ $this->db = $this->connection->getDbAdapter();
+ }
+
+ public function addFieldsToForm(DirectorObjectForm $form)
+ {
+ if ($this->fields || $this->object->supportsFields()) {
+ $this->attachFieldsToForm($form);
+ }
+
+ return $this;
+ }
+
+ public function loadFieldsForMultipleObjects($objects)
+ {
+ $fields = array();
+ foreach ($objects as $object) {
+ foreach ($this->prepareObjectFields($object) as $varname => $field) {
+ $varname = $field->get('varname');
+ if (array_key_exists($varname, $fields)) {
+ if ($field->get('datatype') !== $fields[$varname]->datatype) {
+ unset($fields[$varname]);
+ }
+
+ continue;
+ }
+
+ $fields[$varname] = $field;
+ }
+ }
+
+ $this->fields = $fields;
+
+ return $this;
+ }
+
+ /**
+ * Set a list of values
+ *
+ * Works in a fail-safe way, when a field does not exist the value will be
+ * silently ignored
+ *
+ * @param array $values key/value pairs with variable names and their value
+ * @param string $prefix An optional prefix that would be stripped from keys
+ *
+ * @return IcingaObjectFieldLoader
+ *
+ * @throws IcingaException
+ */
+ public function setValues($values, $prefix = null)
+ {
+ if (! $this->object->supportsCustomVars()) {
+ return $this;
+ }
+
+ if ($prefix === null) {
+ $len = null;
+ } else {
+ $len = strlen($prefix);
+ }
+ $vars = $this->object->vars();
+
+ foreach ($values as $key => $value) {
+ if ($len !== null) {
+ if (substr($key, 0, $len) === $prefix) {
+ $key = substr($key, $len);
+ } else {
+ continue;
+ }
+ }
+
+ $varName = $this->getElementVarName($prefix . $key);
+ if ($varName === null) {
+ // throw new IcingaException(
+ // 'Cannot set variable value for "%s", got no such element',
+ // $key
+ // );
+
+ // Silently ignore additional fields. One might have switched
+ // template or command
+ continue;
+ }
+
+ $el = $this->getElement($varName);
+ if ($el === null) {
+ // throw new IcingaException('No such element %s', $key);
+ // Same here.
+ continue;
+ }
+
+ $el->setValue($value);
+ $value = $el->getValue();
+ if ($value === '' || $value === array()) {
+ $value = null;
+ }
+
+ $vars->set($varName, $value);
+ }
+
+ // Hint: this does currently not happen, as removeFilteredFields did not
+ // take place yet. This has been added to be on the safe side when
+ // cleaning things up one future day
+ foreach ($this->forceNull as $key) {
+ $vars->set($key, null);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the fields for our object
+ *
+ * @return DirectorDatafield[]
+ */
+ public function getFields()
+ {
+ if ($this->fields === null) {
+ $this->fields = $this->prepareObjectFields($this->object);
+ }
+
+ return $this->fields;
+ }
+
+ /**
+ * Get the form elements for our fields
+ *
+ * @param DirectorObjectForm $form Optional
+ *
+ * @return ZfElement[]
+ */
+ public function getElements(DirectorObjectForm $form = null)
+ {
+ if ($this->elements === null) {
+ $this->elements = $this->createElements($form);
+ $this->setValuesFromObject($this->object);
+ }
+
+ return $this->elements;
+ }
+
+ /**
+ * Prepare the form elements for our fields
+ *
+ * @param DirectorObjectForm $form Optional
+ *
+ * @return self
+ */
+ public function prepareElements(DirectorObjectForm $form = null)
+ {
+ if ($this->object->supportsFields()) {
+ $this->getElements($form);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Attach our form fields to the given form
+ *
+ * This will also create a 'Custom properties' display group
+ *
+ * @param DirectorObjectForm $form
+ */
+ protected function attachFieldsToForm(DirectorObjectForm $form)
+ {
+ if ($this->fields === null) {
+ return;
+ }
+ $elements = $this->removeFilteredFields($this->getElements($form));
+
+ foreach ($elements as $element) {
+ $form->addElement($element);
+ }
+
+ $this->attachGroupElements($elements, $form);
+ }
+
+ /**
+ * @param ZfElement[] $elements
+ * @param DirectorObjectForm $form
+ */
+ protected function attachGroupElements(array $elements, DirectorObjectForm $form)
+ {
+ $categories = [];
+ $categoriesFetchedById = [];
+ foreach ($this->fields as $key => $field) {
+ if ($id = $field->get('category_id')) {
+ if (isset($categoriesFetchedById[$id])) {
+ $category = $categoriesFetchedById[$id];
+ } else {
+ $category = DirectorDatafieldCategory::loadWithAutoIncId($id, $form->getDb());
+ $categoriesFetchedById[$id] = $category;
+ }
+ } elseif ($field->hasCategory()) {
+ $category = $field->getCategory();
+ } else {
+ continue;
+ }
+ $categories[$key] = $category;
+ }
+ $prioIdx = \array_flip(\array_keys($categories));
+
+ foreach ($elements as $key => $element) {
+ if (isset($categories[$key])) {
+ $category = $categories[$key];
+ $form->addElementsToGroup(
+ [$element],
+ 'custom_fields:' . $category->get('category_name'),
+ DirectorObjectForm::GROUP_ORDER_CUSTOM_FIELD_CATEGORIES + $prioIdx[$key],
+ $category->get('category_name')
+ );
+ } else {
+ $form->addElementsToGroup(
+ [$element],
+ 'custom_fields',
+ DirectorObjectForm::GROUP_ORDER_CUSTOM_FIELDS,
+ $form->translate('Custom properties')
+ );
+ }
+ }
+ }
+
+ /**
+ * @param ZfElement[] $elements
+ * @return ZfElement[]
+ */
+ protected function removeFilteredFields(array $elements)
+ {
+ $filters = array();
+ foreach ($this->fields as $key => $field) {
+ if ($filter = $field->var_filter) {
+ $filters[$key] = Filter::fromQueryString($filter);
+ }
+ }
+
+ $kill = array();
+ $columns = array();
+ $object = $this->object;
+ if ($object instanceof IcingaHost) {
+ $prefix = 'host.vars.';
+ } elseif ($object instanceof IcingaService) {
+ $prefix = 'service.vars.';
+ } else {
+ return $elements;
+ }
+
+ $object->invalidateResolveCache();
+ $vars = $object::fromPlainObject(
+ $object->toPlainObject(true),
+ $object->getConnection()
+ )->getVars();
+
+ $prefixedVars = (object) array();
+ foreach ($vars as $k => $v) {
+ $prefixedVars->{$prefix . $k} = $v;
+ }
+
+ foreach ($filters as $key => $filter) {
+ ObjectApplyMatches::fixFilterColumns($filter);
+ /** @var $filter FilterChain|FilterExpression */
+ foreach ($filter->listFilteredColumns() as $column) {
+ $column = substr($column, strlen($prefix));
+ $columns[$column] = $column;
+ }
+ if (! $filter->matches($prefixedVars)) {
+ $kill[] = $key;
+ }
+ }
+
+ $vars = $object->vars();
+ foreach ($kill as $key) {
+ unset($elements[$key]);
+ $this->forceNull[$key] = $key;
+ // Hint: this should happen later on, currently execution order is
+ // a little bit weird
+ $vars->set($key, null);
+ }
+
+ foreach ($columns as $col) {
+ if (array_key_exists($col, $elements)) {
+ $el = $elements[$col];
+ $existingClass = $el->getAttrib('class');
+ if ($existingClass !== null && strlen($existingClass)) {
+ $el->setAttrib('class', $existingClass . ' autosubmit');
+ } else {
+ $el->setAttrib('class', 'autosubmit');
+ }
+ }
+ }
+
+ return $elements;
+ }
+
+ protected function getElementVarName($name)
+ {
+ if (array_key_exists($name, $this->nameMap)) {
+ return $this->nameMap[$name];
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the form element for a specific field by it's variable name
+ *
+ * @param string $name
+ * @return null|ZfElement
+ */
+ protected function getElement($name)
+ {
+ $elements = $this->getElements();
+ if (array_key_exists($name, $elements)) {
+ return $this->elements[$name];
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the form elements based on the given form
+ *
+ * @param DirectorObjectForm $form
+ *
+ * @return ZfElement[]
+ */
+ protected function createElements(DirectorObjectForm $form)
+ {
+ $elements = array();
+
+ foreach ($this->getFields() as $name => $field) {
+ $el = $field->getFormElement($form);
+ $elName = $el->getName();
+ if (array_key_exists($elName, $this->nameMap)) {
+ $form->addErrorMessage(sprintf(
+ 'Form element name collision, "%s" resolves to "%s", but this is also used for "%s"',
+ $name,
+ $elName,
+ $this->nameMap[$elName]
+ ));
+ }
+ $this->nameMap[$elName] = $name;
+ $elements[$name] = $el;
+ }
+
+ return $elements;
+ }
+
+ /**
+ * @param IcingaObject $object
+ */
+ protected function setValuesFromObject(IcingaObject $object)
+ {
+ foreach ($object->getVars() as $k => $v) {
+ if ($v !== null && $el = $this->getElement($k)) {
+ $el->setValue($v);
+ }
+ }
+ }
+
+ protected function mergeFields($listOfFields)
+ {
+ // TODO: Merge field for different object, mostly sets
+ }
+
+ /**
+ * Create the fields for our object
+ *
+ * @param IcingaObject $object
+ * @return DirectorDatafield[]
+ */
+ protected function prepareObjectFields($object)
+ {
+ $fields = $this->loadResolvedFieldsForObject($object);
+ if ($object->hasRelation('check_command')) {
+ try {
+ /** @var IcingaCommand $command */
+ $command = $object->getResolvedRelated('check_command');
+ } catch (Exception $e) {
+ // Ignore failures
+ $command = null;
+ }
+
+ if ($command) {
+ $cmdLoader = new static($command);
+ $cmdFields = $cmdLoader->getFields();
+ foreach ($cmdFields as $varname => $field) {
+ if (! array_key_exists($varname, $fields)) {
+ $fields[$varname] = $field;
+ }
+ }
+ }
+
+ // TODO -> filters!
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Create the fields for our object
+ *
+ * Follows the inheritance logic, resolves all fields and keeps the most
+ * specific ones. Returns a list of fields indexed by variable name
+ *
+ * @param IcingaObject $object
+ *
+ * @return DirectorDatafield[]
+ */
+ protected function loadResolvedFieldsForObject(IcingaObject $object)
+ {
+ $result = $this->loadDataFieldsForObject(
+ $object
+ );
+
+ $fields = array();
+ foreach ($result as $objectId => $varFields) {
+ foreach ($varFields as $var => $field) {
+ $fields[$var] = $field;
+ }
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @param IcingaObject[] $objectList List of objects
+ *
+ * @return array
+ */
+ protected function getIdsForObjectList($objectList)
+ {
+ $ids = [];
+ foreach ($objectList as $object) {
+ if ($object->hasBeenLoadedFromDb()) {
+ $ids[] = $object->get('id');
+ }
+ }
+
+ return $ids;
+ }
+
+ public function fetchFieldDetailsForObject(IcingaObject $object)
+ {
+ $ids = $object->listAncestorIds();
+ if ($id = $object->getProperty('id')) {
+ $ids[] = $id;
+ }
+ return $this->fetchFieldDetailsForIds($ids);
+ }
+
+ /***
+ * @param $objectIds
+ *
+ * @return \stdClass[]
+ */
+ protected function fetchFieldDetailsForIds($objectIds)
+ {
+ if (empty($objectIds)) {
+ return [];
+ }
+
+ $query = $this->prepareSelectForIds($objectIds);
+ return $this->db->fetchAll($query);
+ }
+
+ /**
+ * @param array $ids
+ *
+ * @return ZfSelect
+ */
+ protected function prepareSelectForIds(array $ids)
+ {
+ $object = $this->object;
+
+ $idColumn = 'f.' . $object->getShortTableName() . '_id';
+
+ $query = $this->db->select()->from(
+ array('df' => 'director_datafield'),
+ array(
+ 'object_id' => $idColumn,
+ 'icinga_type' => "('" . $object->getShortTableName() . "')",
+ 'var_filter' => 'f.var_filter',
+ 'is_required' => 'f.is_required',
+ 'id' => 'df.id',
+ 'category_id' => 'df.category_id',
+ 'varname' => 'df.varname',
+ 'caption' => 'df.caption',
+ 'description' => 'df.description',
+ 'datatype' => 'df.datatype',
+ 'format' => 'df.format',
+ )
+ )->join(
+ array('f' => $object->getTableName() . '_field'),
+ 'df.id = f.datafield_id',
+ array()
+ )->where($idColumn . ' IN (?)', $ids)
+ ->order('CASE WHEN var_filter IS NULL THEN 0 ELSE 1 END ASC')
+ ->order('df.caption ASC');
+
+ return $query;
+ }
+
+ /**
+ * Fetches fields for a given object
+ *
+ * Gives a list indexed by object id, with each entry being a list of that
+ * objects DirectorDatafield instances indexed by variable name
+ *
+ * @param IcingaObject $object
+ *
+ * @return array
+ */
+ public function loadDataFieldsForObject(IcingaObject $object)
+ {
+ $res = $this->fetchFieldDetailsForObject($object);
+
+ $result = [];
+ foreach ($res as $r) {
+ $id = $r->object_id;
+ unset($r->object_id);
+ if (! array_key_exists($id, $result)) {
+ $result[$id] = new stdClass;
+ }
+
+ $result[$id]->{$r->varname} = DirectorDatafield::fromDbRow(
+ $r,
+ $this->connection
+ );
+ }
+
+ foreach ($this->loadHookedDataFieldForObject($object) as $id => $fields) {
+ if (array_key_exists($id, $result)) {
+ foreach ($fields as $varName => $field) {
+ $result[$id]->$varName = $field;
+ }
+ } else {
+ $result[$id] = $fields;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @return array
+ */
+ protected function loadHookedDataFieldForObject(IcingaObject $object)
+ {
+ $fields = [];
+ if ($object instanceof IcingaHost || $object instanceof IcingaService) {
+ $fields = $this->addHookedFields($object);
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @return mixed
+ */
+ protected function addHookedFields(IcingaObject $object)
+ {
+ $fields = [];
+ /** @var HostFieldHook|ServiceFieldHook $hook */
+ $type = ucfirst($object->getShortTableName());
+ foreach (Hook::all("Director\\${type}Field") as $hook) {
+ if ($hook->wants($object)) {
+ $id = $object->get('id');
+ $spec = $hook->getFieldSpec($object);
+ if (!array_key_exists($id, $fields)) {
+ $fields[$id] = new stdClass();
+ }
+ $fields[$id]->{$spec->getVarName()} = $spec->toDataField($object);
+ }
+ }
+ return $fields;
+ }
+}
diff --git a/library/Director/Web/Form/IconHelper.php b/library/Director/Web/Form/IconHelper.php
new file mode 100644
index 0000000..3add09b
--- /dev/null
+++ b/library/Director/Web/Form/IconHelper.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Icon helper class
+ *
+ * Should help to reduce redundant icon-lookup code. Currently with hardcoded
+ * icons only, could easily provide support for all of them as follows:
+ *
+ * $confFile = Icinga::app()
+ * ->getApplicationDir('fonts/fontello-ifont/config.json');
+ *
+ * $font = json_decode(file_get_contents($confFile));
+ * // 'icon-' is to be found in $font->css_prefix_text
+ * foreach ($font->glyphs as $icon) {
+ * // $icon->css (= 'help') -> 0x . dechex($icon->code)
+ * }
+ */
+class IconHelper
+{
+ private $icons = array(
+ 'minus' => 'e806',
+ 'trash' => 'e846',
+ 'plus' => 'e805',
+ 'cancel' => 'e804',
+ 'help' => 'e85b',
+ 'angle-double-right' => 'e87b',
+ 'up-big' => 'e825',
+ 'down-big' => 'e828',
+ 'down-open' => 'e821',
+ );
+
+ private $mappedUtf8Icons;
+
+ private $reversedUtf8Icons;
+
+ private static $instance;
+
+ public function __construct()
+ {
+ $this->prepareIconMappings();
+ }
+
+ public static function instance()
+ {
+ if (self::$instance === null) {
+ self::$instance = new static;
+ }
+
+ return self::$instance;
+ }
+
+ public function characterIconName($character)
+ {
+ if (array_key_exists($character, $this->reversedUtf8Icons)) {
+ return $this->reversedUtf8Icons[$character];
+ } else {
+ throw new ProgrammingError('There is no mapping for the given character');
+ }
+ }
+
+ protected function hexToCharacter($hex)
+ {
+ return json_decode('"\u' . $hex . '"');
+ }
+
+ public function iconCharacter($name)
+ {
+ if (array_key_exists($name, $this->mappedUtf8Icons)) {
+ return $this->mappedUtf8Icons[$name];
+ } else {
+ return $this->mappedUtf8Icons['help'];
+ }
+ }
+
+ protected function prepareIconMappings()
+ {
+ $this->mappedUtf8Icons = array();
+ $this->reversedUtf8Icons = array();
+ foreach ($this->icons as $name => $hex) {
+ $character = $this->hexToCharacter($hex);
+ $this->mappedUtf8Icons[$name] = $character;
+ $this->reversedUtf8Icons[$character] = $name;
+ }
+ }
+}
diff --git a/library/Director/Web/Form/IplElement/ExtensibleSetElement.php b/library/Director/Web/Form/IplElement/ExtensibleSetElement.php
new file mode 100644
index 0000000..a4dbb20
--- /dev/null
+++ b/library/Director/Web/Form/IplElement/ExtensibleSetElement.php
@@ -0,0 +1,570 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\IplElement;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\IcingaConfig\ExtensibleSet as Set;
+use Icinga\Module\Director\Web\Form\IconHelper;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use gipfl\Translation\TranslationHelper;
+
+class ExtensibleSetElement extends BaseHtmlElement
+{
+ use TranslationHelper;
+
+ protected $tag = 'ul';
+
+ /** @var Set */
+ protected $set;
+
+ private $id;
+
+ private $name;
+
+ private $value;
+
+ private $description;
+
+ private $multiOptions;
+
+ private $validOptions;
+
+ private $chosenOptionCount = 0;
+
+ private $suggestionContext;
+
+ private $sorted = false;
+
+ private $disabled = false;
+
+ private $remainingAttribs;
+
+ private $hideOptions = [];
+
+ private $inherited;
+
+ private $inheritedFrom;
+
+ protected $defaultAttributes = [
+ 'class' => 'extensible-set'
+ ];
+
+ protected function __construct($name)
+ {
+ $this->name = $this->id = $name;
+ }
+
+ public function hideOptions($options)
+ {
+ $this->hideOptions = array_merge($this->hideOptions, $options);
+ return $this;
+ }
+
+ private function setMultiOptions($options)
+ {
+ $this->multiOptions = $options;
+ $this->validOptions = $this->flattenOptions($options);
+ }
+
+ protected function isValidOption($option)
+ {
+ if ($this->validOptions === null) {
+ if ($this->suggestionContext === null) {
+ return true;
+ } else {
+ // TODO: ask suggestionContext, if any
+ return true;
+ }
+ } else {
+ return in_array($option, $this->validOptions);
+ }
+ }
+
+ private function disable($disable = true)
+ {
+ $this->disabled = (bool) $disable;
+ }
+
+ private function isDisabled()
+ {
+ return $this->disabled;
+ }
+
+ private function isSorted()
+ {
+ return $this->sorted;
+ }
+
+ public function setValue($value)
+ {
+ if ($value instanceof Set) {
+ $value = $value->toPlainObject();
+ }
+
+ if (is_array($value)) {
+ $value = array_filter($value, 'strlen');
+ }
+
+ if (null !== $value && ! is_array($value)) {
+ throw new ProgrammingError(
+ 'Got unexpected value, no array: %s',
+ var_export($value, 1)
+ );
+ }
+
+ $this->value = $value;
+ return $this;
+ }
+
+ protected function extractZfInfo(&$attribs = null)
+ {
+ if ($attribs === null) {
+ return;
+ }
+
+ foreach (['id', 'name', 'descriptions'] as $key) {
+ if (array_key_exists($key, $attribs)) {
+ $this->$key = $attribs[$key];
+ unset($attribs[$key]);
+ }
+ }
+ if (array_key_exists('disable', $attribs)) {
+ $this->disable($attribs['disable']);
+ unset($attribs['disable']);
+ }
+ if (array_key_exists('value', $attribs)) {
+ $this->setValue($attribs['value']);
+ unset($attribs['value']);
+ }
+ if (array_key_exists('inherited', $attribs)) {
+ $this->inherited = $attribs['inherited'];
+ unset($attribs['inherited']);
+ }
+ if (array_key_exists('inheritedFrom', $attribs)) {
+ $this->inheritedFrom = $attribs['inheritedFrom'];
+ unset($attribs['inheritedFrom']);
+ }
+
+ if (array_key_exists('multiOptions', $attribs)) {
+ $this->setMultiOptions($attribs['multiOptions']);
+ unset($attribs['multiOptions']);
+ }
+
+ if (array_key_exists('hideOptions', $attribs)) {
+ $this->hideOptions($attribs['hideOptions']);
+ unset($attribs['hideOptions']);
+ }
+
+ if (array_key_exists('sorted', $attribs)) {
+ $this->sorted = (bool) $attribs['sorted'];
+ unset($attribs['sorted']);
+ }
+
+ if (array_key_exists('description', $attribs)) {
+ $this->description = $attribs['description'];
+ unset($attribs['description']);
+ }
+
+ if (array_key_exists('suggest', $attribs)) {
+ $this->suggestionContext = $attribs['suggest'];
+ unset($attribs['suggest']);
+ }
+
+ if (! empty($attribs)) {
+ $this->remainingAttribs = $attribs;
+ }
+ }
+
+ /**
+ * Generates an 'extensible set' element.
+ *
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string|array $name If a string, the element name. If an
+ * array, all other parameters are ignored, and the array elements
+ * are used in place of added parameters.
+ *
+ * @param mixed $value The element value.
+ *
+ * @param array $attribs Attributes for the element tag.
+ *
+ * @return string The element XHTML.
+ */
+ public static function fromZfDingens($name, $value = null, $attribs = null)
+ {
+ $el = new static($name);
+ $el->extractZfInfo($attribs);
+ $el->setValue($value);
+ return $el->render();
+ }
+
+ protected function assemble()
+ {
+ $this->addChosenOptions();
+ $this->addAddMore();
+
+ if ($this->isSorted()) {
+ $this->getAttributes()->add('class', 'sortable');
+ }
+ if (null !== $this->description) {
+ $this->addDescription($this->description);
+ }
+ }
+
+ private function eventuallyAddAutosuggestion(BaseHtmlElement $element)
+ {
+ if ($this->suggestionContext !== null) {
+ $attrs = $element->getAttributes();
+ $attrs->add('class', 'director-suggest');
+ $attrs->set([
+ 'data-suggestion-context' => $this->suggestionContext,
+ ]);
+ }
+
+ return $element;
+ }
+
+ private function hasAvailableMultiOptions()
+ {
+ return count($this->multiOptions) > 1 || strlen(key($this->multiOptions));
+ }
+
+ private function addAddMore()
+ {
+ $cnt = $this->chosenOptionCount;
+
+ if ($this->multiOptions) {
+ if (! $this->hasAvailableMultiOptions()) {
+ return;
+ }
+ $field = Html::tag('select', ['class' => 'autosubmit']);
+ $more = $this->inherited === null
+ ? $this->translate('- add more -')
+ : $this->getInheritedInfo();
+ $field->add(Html::tag('option', [
+ 'value' => '',
+ 'tabindex' => '-1'
+ ], $more));
+
+ foreach ($this->multiOptions as $key => $label) {
+ if ($key === null) {
+ $key = '';
+ }
+ if (is_array($label)) {
+ $optGroup = Html::tag('optgroup', ['label' => $key]);
+ foreach ($label as $grpKey => $grpLabel) {
+ $optGroup->add(
+ Html::tag('option', ['value' => $grpKey], $grpLabel)
+ );
+ }
+ $field->add($optGroup);
+ } else {
+ $option = Html::tag('option', ['value' => $key], $label);
+ $field->add($option);
+ }
+ }
+ } else {
+ $field = Html::tag('input', [
+ 'type' => 'text',
+ 'placeholder' => $this->inherited === null
+ ? $this->translate('Add a new one...')
+ : $this->getInheritedInfo(),
+ ]);
+ }
+ $field->addAttributes([
+ 'id' => $this->id . $this->suffix($cnt),
+ 'name' => $this->name . '[]',
+ ]);
+ $this->eventuallyAddAutosuggestion(
+ $this->addRemainingAttributes(
+ $this->eventuallyDisable($field)
+ )
+ );
+ if ($cnt !== 0) { // TODO: was === 0?!
+ $field->getAttributes()->add('class', 'extend-set');
+ }
+
+ if ($this->suggestionContext === null) {
+ $this->add(Html::tag('li', null, [
+ $this->createAddNewButton(),
+ $field
+ ]));
+ } else {
+ $this->add(Html::tag('li', null, [
+ $this->newInlineButtons(
+ $this->renderDropDownButton()
+ ),
+ $field
+ ]));
+ }
+ }
+
+ private function getInheritedInfo()
+ {
+ if ($this->inheritedFrom === null) {
+ return \sprintf(
+ $this->translate('%s (inherited)'),
+ $this->stringifyInheritedValue()
+ );
+ } else {
+ return \sprintf(
+ $this->translate('%s (inherited from %s)'),
+ $this->stringifyInheritedValue(),
+ $this->inheritedFrom
+ );
+ }
+ }
+
+ private function stringifyInheritedValue()
+ {
+ if (\is_array($this->inherited)) {
+ return \implode(', ', $this->inherited);
+ } else {
+ return \sprintf(
+ $this->translate('%s (not an Array!)'),
+ \var_export($this->inherited, 1)
+ );
+ }
+ }
+
+ private function createAddNewButton()
+ {
+ return $this->newInlineButtons(
+ $this->eventuallyDisable($this->renderAddButton())
+ );
+ }
+
+ private function addChosenOptions()
+ {
+ if (null === $this->value) {
+ return;
+ }
+ $total = count($this->value);
+
+ foreach ($this->value as $val) {
+ if (in_array($val, $this->hideOptions)) {
+ continue;
+ }
+
+ if ($this->multiOptions !== null) {
+ if ($this->isValidOption($val)) {
+ $this->multiOptions = $this->removeOption(
+ $this->multiOptions,
+ $val
+ );
+ // TODO:
+ // $this->removeOption($val);
+ }
+ }
+
+ $text = Html::tag('input', [
+ 'type' => 'text',
+ 'name' => $this->name . '[]',
+ 'id' => $this->id . $this->suffix($this->chosenOptionCount),
+ 'value' => $val
+ ]);
+ $text->getAttributes()->set([
+ 'autocomplete' => 'off',
+ 'autocorrect' => 'off',
+ 'autocapitalize' => 'off',
+ 'spellcheck' => 'false',
+ ]);
+
+ $this->addRemainingAttributes($this->eventuallyDisable($text));
+ $this->add(Html::tag('li', null, [
+ $this->getOptionButtons($this->chosenOptionCount, $total),
+ $text
+ ]));
+ $this->chosenOptionCount++;
+ }
+ }
+
+ private function addRemainingAttributes(BaseHtmlElement $element)
+ {
+ if ($this->remainingAttribs !== null) {
+ $element->getAttributes()->add($this->remainingAttribs);
+ }
+
+ return $element;
+ }
+
+ private function eventuallyDisable(BaseHtmlElement $element)
+ {
+ if ($this->isDisabled()) {
+ $this->disableElement($element);
+ }
+
+ return $element;
+ }
+
+ private function disableElement(BaseHtmlElement $element)
+ {
+ $element->getAttributes()->set('disabled', 'disabled');
+ return $element;
+ }
+
+ private function disableIf(BaseHtmlElement $element, $condition)
+ {
+ if ($condition) {
+ $this->disableElement($element);
+ }
+
+ return $element;
+ }
+
+ private function getOptionButtons($cnt, $total)
+ {
+ if ($this->isDisabled()) {
+ return [];
+ }
+ $first = $cnt === 0;
+ $last = $cnt === $total - 1;
+ $name = $this->name;
+ $buttons = $this->newInlineButtons();
+ if ($this->isSorted()) {
+ $buttons->add([
+ $this->disableIf($this->renderDownButton($name, $cnt), $last),
+ $this->disableIf($this->renderUpButton($name, $cnt), $first)
+ ]);
+ }
+
+ $buttons->add($this->renderDeleteButton($name, $cnt));
+
+ return $buttons;
+ }
+
+ protected function newInlineButtons($content = null)
+ {
+ return Html::tag('span', ['class' => 'inline-buttons'], $content);
+ }
+
+ protected function addDescription($description)
+ {
+ $this->add(
+ Html::tag('p', ['class' => 'description'], $description)
+ );
+ }
+
+ private function flattenOptions($options)
+ {
+ $flat = array();
+
+ foreach ($options as $key => $option) {
+ if (is_array($option)) {
+ foreach ($option as $k => $o) {
+ $flat[] = $k;
+ }
+ } else {
+ $flat[] = $key;
+ }
+ }
+
+ return $flat;
+ }
+
+ private function removeOption($options, $option)
+ {
+ $unset = array();
+ foreach ($options as $key => & $value) {
+ if (is_array($value)) {
+ $value = $this->removeOption($value, $option);
+ if (empty($value)) {
+ $unset[] = $key;
+ }
+ } elseif ($key === $option) {
+ $unset[] = $key;
+ }
+ }
+
+ foreach ($unset as $key) {
+ unset($options[$key]);
+ }
+
+ return $options;
+ }
+
+ private function suffix($cnt)
+ {
+ if ($cnt === 0) {
+ return '';
+ } else {
+ return '_' . $cnt;
+ }
+ }
+
+ private function renderDropDownButton()
+ {
+ return $this->createRelatedAction(
+ 'drop-down',
+ $this->name,
+ $this->translate('Show available options'),
+ 'down-open'
+ );
+ }
+
+ private function renderAddButton()
+ {
+ return $this->createRelatedAction(
+ 'add',
+ // This would interfere with how PHP resolves _POST arrays. So we
+ // use a fake name for now, that way the button will be ignored and
+ // behave similar to an auto-submission
+ 'X_' . $this->name,
+ $this->translate('Add a new entry'),
+ 'plus'
+ );
+ }
+
+ private function renderDeleteButton($name, $cnt)
+ {
+ return $this->createRelatedAction(
+ 'remove',
+ $name . '_' . $cnt,
+ $this->translate('Remove this entry'),
+ 'cancel'
+ );
+ }
+
+ private function renderUpButton($name, $cnt)
+ {
+ return $this->createRelatedAction(
+ 'move-up',
+ $name . '_' . $cnt,
+ $this->translate('Move up'),
+ 'up-big'
+ );
+ }
+
+ private function renderDownButton($name, $cnt)
+ {
+ return $this->createRelatedAction(
+ 'move-down',
+ $name . '_' . $cnt,
+ $this->translate('Move down'),
+ 'down-big'
+ );
+ }
+
+ protected function makeActionName($name, $action)
+ {
+ return $name . '__' . str_replace('-', '_', strtoupper($action));
+ }
+
+ protected function createRelatedAction(
+ $action,
+ $name,
+ $title,
+ $icon
+ ) {
+ $input = Html::tag('input', [
+ 'type' => 'submit',
+ 'class' => ['related-action', 'action-' . $action],
+ 'name' => $this->makeActionName($name, $action),
+ 'value' => IconHelper::instance()->iconCharacter($icon),
+ 'title' => $title
+ ]);
+
+ return $input;
+ }
+}
diff --git a/library/Director/Web/Form/QuickBaseForm.php b/library/Director/Web/Form/QuickBaseForm.php
new file mode 100644
index 0000000..8d25ffb
--- /dev/null
+++ b/library/Director/Web/Form/QuickBaseForm.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use ipl\Html\Html;
+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->addPrefixPathsForDirector();
+ if ($this->icingaModule && $this->icingaModuleName !== 'director') {
+ $this->addPrefixPathsForModule($this->icingaModule);
+ }
+ }
+
+ protected function addPrefixPathsForDirector()
+ {
+ $module = Icinga::app()
+ ->getModuleManager()
+ ->loadModule('director')
+ ->getModule('director');
+
+ $this->addPrefixPathsForModule($module);
+ }
+
+ public function addPrefixPathsForModule(Module $module)
+ {
+ $basedir = sprintf(
+ '%s/%s/Web/Form',
+ $module->getLibDir(),
+ ucfirst($module->getName())
+ );
+
+ $this->addPrefixPath(
+ __NAMESPACE__ . '\\Element\\',
+ $basedir . '/Element',
+ 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 = [])
+ {
+ return $this->addHtml(
+ Html::tag('div', ['class' => 'hint'], $html),
+ $options
+ );
+ }
+
+ public function addHtml($html, $options = [])
+ {
+ if ($html instanceof ValidHtml) {
+ $html = $html->render();
+ }
+
+ 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 ($value === null) {
+ return true;
+ }
+
+ 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/Director/Web/Form/QuickForm.php b/library/Director/Web/Form/QuickForm.php
new file mode 100644
index 0000000..91c8f00
--- /dev/null
+++ b/library/Director/Web/Form/QuickForm.php
@@ -0,0 +1,641 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Notification;
+use Icinga\Web\Request;
+use Icinga\Web\Response;
+use Icinga\Web\Url;
+use InvalidArgumentException;
+use Exception;
+use RuntimeException;
+
+/**
+ * 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;
+
+ 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;
+
+ protected $successCallbacks = [];
+
+ protected $calledSuccessCallbacks = false;
+
+ protected $onRequestCallbacks = [];
+
+ protected $calledOnRequestCallbacks = 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()
+ {
+ $current = $this->getAttrib('class');
+ $current .= ' director-form';
+ if ($current) {
+ $this->setAttrib('class', "$current autofocus");
+ } else {
+ $this->setAttrib('class', 'autofocus');
+ }
+ $this->setDecorators(
+ array(
+ 'Description',
+ array('FormErrors', array('onlyCustomFormErrors' => true)),
+ 'FormElements',
+ 'Form'
+ )
+ );
+
+ return $this;
+ }
+
+ protected function addSubmitButton($label, $options = [])
+ {
+ $el = $this->createElement('submit', $label, $options)
+ ->setLabel($label)
+ ->setDecorators(array('ViewHelper'));
+ $this->submitButtonName = $el->getName();
+ $this->setSubmitLabel($label);
+ $this->addElement($el);
+ }
+
+ protected function addStandaloneSubmitButton($label, $options = [])
+ {
+ $this->addSubmitButton($label, $options);
+ $this->addDisplayGroup([$this->submitButtonName], 'buttons', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'p')),
+ ),
+ 'order' => 1000,
+ ));
+ }
+
+ protected function addSubmitButtonIfSet()
+ {
+ if (false === ($label = $this->getSubmitLabel())) {
+ return;
+ }
+
+ if ($this->submitButtonName && $el = $this->getElement($this->submitButtonName)) {
+ return;
+ }
+
+ $this->addSubmitButton($label);
+
+ $fakeEl = $this->createElement('submit', '_FAKE_SUBMIT', array(
+ 'role' => 'none',
+ 'tabindex' => '-1',
+ ))
+ ->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,
+ )
+ );
+
+ $this->addButtonDisplayGroup();
+ }
+
+ protected function addButtonDisplayGroup()
+ {
+ $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()
+ {
+ if ($this->isApiRequest()) {
+ return $this;
+ }
+ $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()
+ {
+ if ($this->isApiRequest === null) {
+ if ($this->request === null) {
+ throw new RuntimeException(
+ 'Early access to isApiRequest(). This is not possible, sorry'
+ );
+ }
+
+ return $this->getRequest()->isApiRequest();
+ } else {
+ return $this->isApiRequest;
+ }
+ }
+
+ public function regenerateCsrfToken()
+ {
+ if ($this->isApiRequest()) {
+ return $this;
+ }
+ 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->isApiRequest()) {
+ return $this->hasBeenSubmitted = true;
+ }
+ 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->onSetup();
+ $this->didSetup = true;
+ }
+
+ return $this;
+ }
+
+ public function handleRequest(Request $request = null)
+ {
+ if ($request === null) {
+ $request = $this->getRequest();
+ } else {
+ $this->setRequest($request);
+ }
+
+ $this->prepareElements();
+ $this->addSubmitButtonIfSet();
+
+ if ($this->hasBeenSent()) {
+ $post = $request->getPost();
+ if ($this->hasBeenSubmitted()) {
+ $this->beforeValidation($post);
+ if ($this->isValid($post)) {
+ try {
+ $this->onSuccess();
+ $this->callOnSuccessCallables();
+ } catch (Exception $e) {
+ $this->addException($e);
+ $this->onFailure();
+ }
+ } else {
+ $this->onFailure();
+ }
+ } else {
+ $this->setDefaults($post);
+ }
+ }
+
+ return $this;
+ }
+
+ public function addException(Exception $e, $elementName = null)
+ {
+ $msg = $this->getErrorMessageForException($e);
+ if ($el = $this->getElement($elementName)) {
+ $el->addError($msg);
+ } else {
+ $this->addError($msg);
+ }
+ }
+
+ public function addUniqueErrorMessage($msg)
+ {
+ if (! in_array($msg, $this->getErrorMessages())) {
+ $this->addErrorMessage($msg);
+ }
+
+ return $this;
+ }
+
+ public function addUniqueException(Exception $e)
+ {
+ $msg = $this->getErrorMessageForException($e);
+
+ if (! in_array($msg, $this->getErrorMessages())) {
+ $this->addErrorMessage($msg);
+ }
+
+ return $this;
+ }
+
+ protected function getErrorMessageForException(Exception $e)
+ {
+ $file = preg_split('/[\/\\\]/', $e->getFile(), -1, PREG_SPLIT_NO_EMPTY);
+ $file = array_pop($file);
+ return sprintf(
+ '%s (%s:%d)',
+ $e->getMessage(),
+ $file,
+ $e->getLine()
+ );
+ }
+
+ public function onSuccess()
+ {
+ $this->redirectOnSuccess();
+ }
+
+ /**
+ * @param callable $callable
+ * @return $this
+ */
+ public function callOnRequest($callable)
+ {
+ if (! is_callable($callable)) {
+ throw new InvalidArgumentException(
+ 'callOnRequest() expects a callable'
+ );
+ }
+ $this->onRequestCallbacks[] = $callable;
+
+ return $this;
+ }
+
+ protected function callOnRequestCallables()
+ {
+ if (! $this->calledOnRequestCallbacks) {
+ $this->calledOnRequestCallbacks = true;
+ foreach ($this->onRequestCallbacks as $callable) {
+ $callable($this);
+ }
+ }
+ }
+
+ /**
+ * @param callable $callable
+ * @return $this
+ */
+ public function callOnSuccess($callable)
+ {
+ if (! is_callable($callable)) {
+ throw new InvalidArgumentException(
+ 'callOnSuccess() expects a callable'
+ );
+ }
+ $this->successCallbacks[] = $callable;
+
+ return $this;
+ }
+
+ protected function callOnSuccessCallables()
+ {
+ if (! $this->calledSuccessCallbacks) {
+ $this->calledSuccessCallbacks = true;
+ foreach ($this->successCallbacks as $callable) {
+ $callable($this);
+ }
+ }
+ }
+
+ 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);
+ $this->callOnSuccessCallables();
+ return;
+ }
+
+ $url = $this->getSuccessUrl();
+ $this->callOnSuccessCallables();
+ $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()
+ {
+ $this->callOnRequestCallables();
+ }
+
+ public function setRequest(Request $request)
+ {
+ if ($this->request !== null) {
+ throw new RuntimeException('Unable to set request twice');
+ }
+
+ $this->request = $request;
+ $this->prepareElements();
+ $this->onRequest();
+ $this->callOnRequestCallables();
+
+ 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->isApiRequest()) {
+ $this->hasBeenSent = true;
+ } elseif ($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/Director/Web/Form/QuickSubForm.php b/library/Director/Web/Form/QuickSubForm.php
new file mode 100644
index 0000000..2487d35
--- /dev/null
+++ b/library/Director/Web/Form/QuickSubForm.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+abstract class QuickSubForm extends QuickBaseForm
+{
+ /**
+ * Whether or not form elements are members of an array
+ * @codingStandardsIgnoreStart
+ * @var bool
+ */
+ protected $_isArray = true;
+ // @codingStandardsIgnoreEnd
+
+ /**
+ * Load the default decorators
+ *
+ * @return $this
+ */
+ public function loadDefaultDecorators()
+ {
+ if ($this->loadDefaultDecoratorsIsDisabled()) {
+ return $this;
+ }
+
+ $decorators = $this->getDecorators();
+ if (empty($decorators)) {
+ $this->addDecorator('FormElements')
+ ->addDecorator('HtmlTag', array('tag' => 'dl'))
+ ->addDecorator('Fieldset')
+ ->addDecorator('DtDdWrapper');
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Director/Web/Form/Validate/IsDataListEntry.php b/library/Director/Web/Form/Validate/IsDataListEntry.php
new file mode 100644
index 0000000..5762d2e
--- /dev/null
+++ b/library/Director/Web/Form/Validate/IsDataListEntry.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Validate;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDatalistEntry;
+use Zend_Validate_Abstract;
+
+class IsDataListEntry extends Zend_Validate_Abstract
+{
+ const INVALID = 'intInvalid';
+
+ /** @var Db */
+ private $db;
+
+ /** @var int */
+ private $dataListId;
+
+ public function __construct($dataListId, Db $db)
+ {
+ $this->db = $db;
+ $this->dataListId = (int) $dataListId;
+ }
+
+ public function isValid($value)
+ {
+ if (is_array($value)) {
+ foreach ($value as $name) {
+ if (! $this->isListEntry($name)) {
+ $this->_error(self::INVALID, $value);
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ if ($this->isListEntry($value)) {
+ return true;
+ } else {
+ $this->_error(self::INVALID, $value);
+
+ return false;
+ }
+ }
+
+ protected function isListEntry($name)
+ {
+ return DirectorDatalistEntry::exists([
+ 'list_id' => $this->dataListId,
+ 'entry_name' => $name,
+ ], $this->db);
+ }
+}
diff --git a/library/Director/Web/Form/Validate/NamePattern.php b/library/Director/Web/Form/Validate/NamePattern.php
new file mode 100644
index 0000000..fac44d9
--- /dev/null
+++ b/library/Director/Web/Form/Validate/NamePattern.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Validate;
+
+use Icinga\Module\Director\Restriction\MatchingFilter;
+use Zend_Validate_Abstract;
+
+class NamePattern extends Zend_Validate_Abstract
+{
+ const INVALID = 'intInvalid';
+
+ private $filter;
+
+ public function __construct($pattern)
+ {
+ if (! is_array($pattern)) {
+ $pattern = [$pattern];
+ }
+
+ $this->filter = MatchingFilter::forPatterns($pattern, 'value');
+
+ $this->_messageTemplates[self::INVALID] = sprintf(
+ 'Does not match %s',
+ (string) $this->filter
+ );
+ }
+
+ public function isValid($value)
+ {
+ if ($this->filter->matches((object) ['value' => $value])) {
+ return true;
+ } else {
+ $this->_error(self::INVALID, $value);
+
+ return false;
+ }
+ }
+}