summaryrefslogtreecommitdiffstats
path: root/library/Director/Data
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/Data
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/Data')
-rw-r--r--library/Director/Data/AssignFilterHelper.php160
-rw-r--r--library/Director/Data/DataArrayHelper.php48
-rw-r--r--library/Director/Data/Db/DbConnection.php51
-rw-r--r--library/Director/Data/Db/DbDataFormatter.php26
-rw-r--r--library/Director/Data/Db/DbObject.php1487
-rw-r--r--library/Director/Data/Db/DbObjectStore.php169
-rw-r--r--library/Director/Data/Db/DbObjectTypeRegistry.php75
-rw-r--r--library/Director/Data/Db/DbObjectWithSettings.php168
-rw-r--r--library/Director/Data/Db/IcingaObjectFilterRenderer.php133
-rw-r--r--library/Director/Data/Db/IcingaObjectQuery.php255
-rw-r--r--library/Director/Data/Db/ServiceSetQueryBuilder.php158
-rw-r--r--library/Director/Data/Exporter.php303
-rw-r--r--library/Director/Data/FieldReferenceLoader.php51
-rw-r--r--library/Director/Data/HostServiceLoader.php170
-rw-r--r--library/Director/Data/ImportExportDeniedProperties.php52
-rw-r--r--library/Director/Data/InvalidDataException.php26
-rw-r--r--library/Director/Data/Json.php69
-rw-r--r--library/Director/Data/PropertiesFilter.php25
-rw-r--r--library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php20
-rw-r--r--library/Director/Data/PropertiesFilter/CustomVariablesFilter.php13
-rw-r--r--library/Director/Data/PropertyMangler.php60
-rw-r--r--library/Director/Data/RecursiveUtf8Validator.php59
-rw-r--r--library/Director/Data/Serializable.php10
-rw-r--r--library/Director/Data/SerializableValue.php90
-rw-r--r--library/Director/Data/ValueFilter.php10
-rw-r--r--library/Director/Data/ValueFilter/FilterBoolean.php19
-rw-r--r--library/Director/Data/ValueFilter/FilterInt.php21
27 files changed, 3728 insertions, 0 deletions
diff --git a/library/Director/Data/AssignFilterHelper.php b/library/Director/Data/AssignFilterHelper.php
new file mode 100644
index 0000000..b0253cf
--- /dev/null
+++ b/library/Director/Data/AssignFilterHelper.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Filter\FilterNot;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Exception\NotImplementedError;
+
+/**
+ * Class ApplyFilterMatches
+ *
+ * A wrapper for Icinga Filter to evaluate filters against Director's objects
+ */
+class AssignFilterHelper
+{
+ /** @var Filter */
+ protected $filter;
+
+ public function __construct(Filter $filter)
+ {
+ $this->filter = $filter;
+ }
+
+ /**
+ * @param object $object
+ *
+ * @return bool
+ * @throws NotImplementedError
+ */
+ public function matches($object)
+ {
+ return $this->matchesPart($this->filter, $object);
+ }
+
+ /**
+ * @param Filter $filter
+ * @param object $object
+ *
+ * @return bool
+ */
+ public static function matchesFilter(Filter $filter, $object)
+ {
+ $helper = new static($filter);
+ return $helper->matches($object);
+ }
+
+ /**
+ * @param Filter $filter
+ * @param object $object
+ *
+ * @return bool
+ * @throws NotImplementedError
+ */
+ protected function matchesPart(Filter $filter, $object)
+ {
+ if ($filter->isChain()) {
+ return $this->matchesChain($filter, $object);
+ } elseif ($filter->isExpression()) {
+ /** @var FilterExpression $filter */
+ return $this->matchesExpression($filter, $object);
+ } else {
+ return $filter->matches($object);
+ }
+ }
+
+ /**
+ * @param Filter $filter
+ * @param object $object
+ *
+ * @return bool
+ * @throws NotImplementedError
+ */
+ protected function matchesChain(Filter $filter, $object)
+ {
+ if ($filter instanceof FilterAnd) {
+ foreach ($filter->filters() as $f) {
+ if (! $this->matchesPart($f, $object)) {
+ return false;
+ }
+ }
+
+ return true;
+ } elseif ($filter instanceof FilterOr) {
+ foreach ($filter->filters() as $f) {
+ if ($this->matchesPart($f, $object)) {
+ return true;
+ }
+ }
+
+ return false;
+ } elseif ($filter instanceof FilterNot) {
+ foreach ($filter->filters() as $f) {
+ if ($this->matchesPart($f, $object)) {
+ return false;
+ }
+ }
+
+ return true;
+ } else {
+ $class = \get_class($filter);
+ $parts = \preg_split('/\\\/', $class);
+
+ throw new NotImplementedError(
+ 'Matching for Filter of type "%s" is not implemented',
+ \end($parts)
+ );
+ }
+ }
+
+ /**
+ * @param FilterExpression $filter
+ * @param object $object
+ *
+ * @return bool
+ */
+ protected function matchesExpression(FilterExpression $filter, $object)
+ {
+ $column = $filter->getColumn();
+ $sign = $filter->getSign();
+ $expression = $filter->getExpression();
+
+ if ($sign === '=') {
+ if ($expression === true) {
+ return property_exists($object, $column) && ! empty($object->{$column});
+ } elseif ($expression === false) {
+ return ! property_exists($object, $column) || empty($object->{$column});
+ } elseif (is_string($expression) && strpos($expression, '*') !== false) {
+ if (! property_exists($object, $column) || empty($object->{$column})) {
+ return false;
+ }
+ $value = $object->{$column};
+
+ $parts = array();
+ foreach (preg_split('~\*~', $expression) as $part) {
+ $parts[] = preg_quote($part);
+ }
+ // match() is case insensitive
+ $pattern = '/^' . implode('.*', $parts) . '$/i';
+
+ if (is_array($value)) {
+ foreach ($value as $candidate) {
+ if (preg_match($pattern, $candidate)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ return (bool) preg_match($pattern, $value);
+ }
+ }
+
+ // fallback to default behavior
+ return $filter->matches($object);
+ }
+}
diff --git a/library/Director/Data/DataArrayHelper.php b/library/Director/Data/DataArrayHelper.php
new file mode 100644
index 0000000..442eb0f
--- /dev/null
+++ b/library/Director/Data/DataArrayHelper.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use InvalidArgumentException;
+use function array_diff;
+use function array_key_exists;
+use function implode;
+use function is_array;
+use function is_object;
+
+class DataArrayHelper
+{
+ public static function wantArray($value)
+ {
+ if (is_object($value)) {
+ return (array) $value;
+ }
+ if (! is_array($value)) {
+ throw new InvalidDataException('Object', $value);
+ }
+
+ return $value;
+ }
+
+ public static function failOnUnknownProperties(array $values, array $knownProperties)
+ {
+ $unknownProperties = array_diff($knownProperties, array_keys($values));
+
+ if (! empty($unknownProperties)) {
+ throw new InvalidArgumentException('Unexpected properties: ' . implode(', ', $unknownProperties));
+ }
+ }
+
+ public static function requireProperties(array $value, array $properties)
+ {
+ $missing = [];
+ foreach ($properties as $property) {
+ if (! array_key_exists($property, $value)) {
+ $missing[] = $property;
+ }
+ }
+
+ if (! empty($missing)) {
+ throw new InvalidArgumentException('Missing properties: ' . implode(', ', $missing));
+ }
+ }
+}
diff --git a/library/Director/Data/Db/DbConnection.php b/library/Director/Data/Db/DbConnection.php
new file mode 100644
index 0000000..146b0e8
--- /dev/null
+++ b/library/Director/Data/Db/DbConnection.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Data\Db\DbConnection as IcingaDbConnection;
+use Icinga\Module\Director\Db\DbUtil;
+use RuntimeException;
+use Zend_Db_Expr;
+
+class DbConnection extends IcingaDbConnection
+{
+ public function isMysql()
+ {
+ return $this->getDbType() === 'mysql';
+ }
+
+ public function isPgsql()
+ {
+ return $this->getDbType() === 'pgsql';
+ }
+
+ /**
+ * @deprecated
+ * @param ?string $binary
+ * @return Zend_Db_Expr|Zend_Db_Expr[]|null
+ */
+ public function quoteBinary($binary)
+ {
+ return DbUtil::quoteBinaryLegacy($binary, $this->getDbAdapter());
+ }
+
+ public function binaryDbResult($value)
+ {
+ if (is_resource($value)) {
+ return stream_get_contents($value);
+ }
+
+ return $value;
+ }
+
+ public function hasPgExtension($name)
+ {
+ $db = $this->db();
+ $query = $db->select()->from(
+ array('e' => 'pg_extension'),
+ array('cnt' => 'COUNT(*)')
+ )->where('extname = ?', $name);
+
+ return (int) $db->fetchOne($query) === 1;
+ }
+}
diff --git a/library/Director/Data/Db/DbDataFormatter.php b/library/Director/Data/Db/DbDataFormatter.php
new file mode 100644
index 0000000..d6e4eeb
--- /dev/null
+++ b/library/Director/Data/Db/DbDataFormatter.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use InvalidArgumentException;
+
+class DbDataFormatter
+{
+ public static function normalizeBoolean($value)
+ {
+ if ($value === 'y' || $value === '1' || $value === true || $value === 1) {
+ return 'y';
+ }
+ if ($value === 'n' || $value === '0' || $value === false || $value === 0) {
+ return 'n';
+ }
+ if ($value === '' || $value === null) {
+ return null;
+ }
+
+ throw new InvalidArgumentException(sprintf(
+ 'Got invalid boolean: %s',
+ var_export($value, 1)
+ ));
+ }
+}
diff --git a/library/Director/Data/Db/DbObject.php b/library/Director/Data/Db/DbObject.php
new file mode 100644
index 0000000..6ecae8b
--- /dev/null
+++ b/library/Director/Data/Db/DbObject.php
@@ -0,0 +1,1487 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Data\InvalidDataException;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\UuidLookup;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use InvalidArgumentException;
+use LogicException;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+use RuntimeException;
+use Zend_Db_Adapter_Abstract;
+use Zend_Db_Exception;
+
+/**
+ * Base class for ...
+ */
+abstract class DbObject
+{
+ /** @var DbConnection $connection */
+ protected $connection;
+
+ /** @var string Table name. MUST be set when extending this class */
+ protected $table;
+
+ /** @var Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /**
+ * Default columns. MUST be set when extending this class. Each table
+ * column MUST be defined with a default value. Default value may be null.
+ *
+ * @var array
+ */
+ protected $defaultProperties;
+
+ /**
+ * Properties as loaded from db
+ */
+ protected $loadedProperties;
+
+ /**
+ * Whether at least one property has been modified
+ */
+ protected $hasBeenModified = false;
+
+ /**
+ * Whether this object has been loaded from db
+ */
+ protected $loadedFromDb = false;
+
+ /**
+ * Object properties
+ */
+ protected $properties = array();
+
+ /**
+ * Property names that have been modified since object creation
+ */
+ protected $modifiedProperties = array();
+
+ /**
+ * Unique key name, could be primary
+ */
+ protected $keyName;
+
+ /**
+ * Set this to an eventual autoincrementing column. May equal $keyName
+ */
+ protected $autoincKeyName;
+
+ /** @var string optional uuid column */
+ protected $uuidColumn;
+
+ /** @var bool forbid updates to autoinc values */
+ protected $protectAutoinc = true;
+
+ protected $binaryProperties = [];
+
+ /**
+ * Filled with object instances when prefetchAll is used
+ */
+ protected static $prefetched = array();
+
+ /**
+ * object_name => id map for prefetched objects
+ */
+ protected static $prefetchedNames = array();
+
+ protected static $prefetchStats = array();
+
+ /** @var ?DbObjectStore */
+ protected static $dbObjectStore;
+
+ /**
+ * Constructor is not accessible and should not be overridden
+ */
+ protected function __construct()
+ {
+ if ($this->table === null
+ || $this->keyName === null
+ || $this->defaultProperties === null
+ ) {
+ throw new LogicException("Someone extending this class didn't RTFM");
+ }
+
+ $this->properties = $this->defaultProperties;
+ $this->beforeInit();
+ }
+
+ public function getTableName()
+ {
+ return $this->table;
+ }
+
+ /************************************************************************\
+ * When extending this class one might want to override any of the *
+ * following hooks. Try to use them whenever possible, especially *
+ * instead of overriding other essential methods like store(). *
+ \************************************************************************/
+
+ /**
+ * One can override this to allow for cross checks and more before storing
+ * the object. Please note that the method is public and allows to check
+ * object consistence at any time.
+ *
+ * @return boolean Whether this object is valid
+ */
+ public function validate()
+ {
+ return true;
+ }
+
+ /**
+ * This is going to be executed before any initialization method takes *
+ * (load from DB, populate from Array...) takes place
+ *
+ * @return void
+ */
+ protected function beforeInit()
+ {
+ }
+
+ /**
+ * Will be executed every time an object has successfully been loaded from
+ * Database
+ *
+ * @return void
+ */
+ protected function onLoadFromDb()
+ {
+ }
+
+ /**
+ * Will be executed before an Object is going to be stored. In case you
+ * want to prevent the store() operation from taking place, please throw
+ * an Exception.
+ *
+ * @return void
+ */
+ protected function beforeStore()
+ {
+ }
+
+ /**
+ * Wird ausgeführt, nachdem ein Objekt erfolgreich gespeichert worden ist
+ *
+ * @return void
+ */
+ protected function onStore()
+ {
+ }
+
+ /**
+ * Wird ausgeführt, nachdem ein Objekt erfolgreich der Datenbank hinzu-
+ * gefügt worden ist
+ *
+ * @return void
+ */
+ protected function onInsert()
+ {
+ }
+
+ /**
+ * Wird ausgeführt, nachdem bestehendes Objekt erfolgreich der Datenbank
+ * geändert worden ist
+ *
+ * @return void
+ */
+ protected function onUpdate()
+ {
+ }
+
+ /**
+ * Wird ausgeführt, bevor ein Objekt gelöscht wird. Die Operation wird
+ * aber auf jeden Fall durchgeführt, außer man wirft eine Exception
+ *
+ * @return void
+ */
+ protected function beforeDelete()
+ {
+ }
+
+ /**
+ * Wird ausgeführt, nachdem bestehendes Objekt erfolgreich aud der
+ * Datenbank gelöscht worden ist
+ *
+ * @return void
+ */
+ protected function onDelete()
+ {
+ }
+
+ /**
+ * Set database connection
+ *
+ * @param DbConnection $connection Database connection
+ *
+ * @return self
+ */
+ public function setConnection(DbConnection $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+
+ return $this;
+ }
+
+ public static function setDbObjectStore(DbObjectStore $store)
+ {
+ self::$dbObjectStore = $store;
+ }
+
+ /**
+ * Getter
+ *
+ * @param string $property Property
+ *
+ * @return mixed
+ */
+ public function get($property)
+ {
+ $func = 'get' . ucfirst($property);
+ if (substr($func, -2) === '[]') {
+ $func = substr($func, 0, -2);
+ }
+ // TODO: id check avoids collision with getId. Rethink this.
+ if ($property !== 'id' && method_exists($this, $func)) {
+ return $this->$func();
+ }
+
+ $this->assertPropertyExists($property);
+ return $this->properties[$property];
+ }
+
+ public function getProperty($key)
+ {
+ $this->assertPropertyExists($key);
+ return $this->properties[$key];
+ }
+
+ protected function assertPropertyExists($key)
+ {
+ if (! array_key_exists($key, $this->properties)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Trying to get invalid property "%s"',
+ $key
+ ));
+ }
+
+ return $this;
+ }
+
+ public function hasProperty($key)
+ {
+ if (array_key_exists($key, $this->properties)) {
+ return true;
+ } elseif ($key === 'id') {
+ // There is getId, would give false positive
+ return false;
+ }
+
+ return $this->hasGetterForProperty($key);
+ }
+
+ protected function hasGetterForProperty($key)
+ {
+ $func = 'get' . ucfirst($key);
+ if (\substr($func, -2) === '[]') {
+ $func = substr($func, 0, -2);
+ }
+
+ return \method_exists($this, $func);
+ }
+
+ protected function hasSetterForProperty($key)
+ {
+ $func = 'set' . ucfirst($key);
+ if (\substr($func, -2) === '[]') {
+ $func = substr($func, 0, -2);
+ }
+
+ return \method_exists($this, $func);
+ }
+
+ /**
+ * Generic setter
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return self
+ */
+ public function set($key, $value)
+ {
+ $key = (string) $key;
+ if ($value === '') {
+ $value = null;
+ }
+
+ if (is_resource($value)) {
+ $value = stream_get_contents($value);
+ }
+ $func = 'validate' . ucfirst($key);
+ if (method_exists($this, $func) && $this->$func($value) !== true) {
+ throw new InvalidArgumentException(sprintf(
+ 'Got invalid value "%s" for "%s"',
+ $value,
+ $key
+ ));
+ }
+ $func = 'munge' . ucfirst($key);
+ if (method_exists($this, $func)) {
+ $value = $this->$func($value);
+ }
+
+ $func = 'set' . ucfirst($key);
+ if (substr($func, -2) === '[]') {
+ $func = substr($func, 0, -2);
+ }
+
+ if (method_exists($this, $func)) {
+ return $this->$func($value);
+ }
+
+ if (! $this->hasProperty($key)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Trying to set invalid key "%s"',
+ $key
+ ));
+ }
+
+ if ((is_numeric($value) || is_string($value))
+ && (string) $value === (string) $this->get($key)
+ ) {
+ return $this;
+ }
+
+ if ($key === $this->getAutoincKeyName() && $this->hasBeenLoadedFromDb()) {
+ throw new InvalidArgumentException('Changing autoincremental key is not allowed');
+ }
+
+ return $this->reallySet($key, $value);
+ }
+
+ protected function reallySet($key, $value)
+ {
+ if ($value === $this->properties[$key]) {
+ return $this;
+ }
+ if ($key === 'id' || substr($key, -3) === '_id') {
+ if ((int) $value === (int) $this->properties[$key]) {
+ return $this;
+ }
+ }
+
+ if ($this->hasBeenLoadedFromDb()) {
+ if ($value === $this->loadedProperties[$key]) {
+ unset($this->modifiedProperties[$key]);
+ if (empty($this->modifiedProperties)) {
+ $this->hasBeenModified = false;
+ }
+ } else {
+ $this->hasBeenModified = true;
+ $this->modifiedProperties[$key] = true;
+ }
+ } else {
+ $this->hasBeenModified = true;
+ $this->modifiedProperties[$key] = true;
+ }
+
+ $this->properties[$key] = $value;
+ return $this;
+ }
+
+ /**
+ * Magic getter
+ *
+ * @param mixed $key
+ *
+ * @return mixed
+ */
+ public function __get($key)
+ {
+ return $this->get($key);
+ }
+
+ /**
+ * Magic setter
+ *
+ * @param string $key Key
+ * @param mixed $val Value
+ *
+ * @return void
+ */
+ public function __set($key, $val)
+ {
+ $this->set($key, $val);
+ }
+
+ /**
+ * Magic isset check
+ *
+ * @param string $key
+ * @return boolean
+ */
+ public function __isset($key)
+ {
+ return array_key_exists($key, $this->properties);
+ }
+
+ /**
+ * Magic unsetter
+ *
+ * @param string $key
+ * @return void
+ */
+ public function __unset($key)
+ {
+ if (! array_key_exists($key, $this->properties)) {
+ throw new InvalidArgumentException('Trying to unset invalid key');
+ }
+ $this->properties[$key] = $this->defaultProperties[$key];
+ }
+
+ /**
+ * Runs set() for every key/value pair of the given Array
+ *
+ * @param array $props Array of properties
+ * @return self
+ */
+ public function setProperties($props)
+ {
+ if (! is_array($props)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Array required, got %s',
+ gettype($props)
+ ));
+ }
+ foreach ($props as $key => $value) {
+ $this->set($key, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * Return an array with all object properties
+ *
+ * @return array
+ */
+ public function getProperties()
+ {
+ //return $this->properties;
+ $res = array();
+ foreach ($this->listProperties() as $key) {
+ $res[$key] = $this->get($key);
+ }
+
+ return $res;
+ }
+
+ protected function getPropertiesForDb()
+ {
+ return $this->properties;
+ }
+
+ public function listProperties()
+ {
+ return array_keys($this->properties);
+ }
+
+ public function getDefaultProperties()
+ {
+ return $this->defaultProperties;
+ }
+
+ /**
+ * Return all properties that changed since object creation
+ *
+ * @return array
+ */
+ public function getModifiedProperties()
+ {
+ $props = array();
+ foreach (array_keys($this->modifiedProperties) as $key) {
+ if ($key === $this->autoincKeyName) {
+ if ($this->protectAutoinc) {
+ continue;
+ } elseif ($this->properties[$key] === null) {
+ continue;
+ }
+ }
+
+ $props[$key] = $this->properties[$key];
+ }
+ return $props;
+ }
+
+ /**
+ * List all properties that changed since object creation
+ *
+ * @return array
+ */
+ public function listModifiedProperties()
+ {
+ return array_keys($this->modifiedProperties);
+ }
+
+ /**
+ * Whether this object has been modified
+ *
+ * @return bool
+ */
+ public function hasBeenModified()
+ {
+ return $this->hasBeenModified;
+ }
+
+ /**
+ * Whether the given property has been modified
+ *
+ * @param string $key Property name
+ * @return boolean
+ */
+ protected function hasModifiedProperty($key)
+ {
+ return array_key_exists($key, $this->modifiedProperties);
+ }
+
+ /**
+ * Unique key name
+ *
+ * @return string
+ */
+ public function getKeyName()
+ {
+ return $this->keyName;
+ }
+
+ /**
+ * Autoinc key name
+ *
+ * @return string
+ */
+ public function getAutoincKeyName()
+ {
+ return $this->autoincKeyName;
+ }
+
+ /**
+ * @return ?string
+ */
+ public function getUuidColumn()
+ {
+ return $this->uuidColumn;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasUuidColumn()
+ {
+ return $this->uuidColumn !== null;
+ }
+
+ /**
+ * @return \Ramsey\Uuid\UuidInterface
+ */
+ public function getUniqueId()
+ {
+ if ($this->hasUuidColumn()) {
+ $binaryValue = $this->properties[$this->uuidColumn];
+ if (is_resource($binaryValue)) {
+ throw new RuntimeException('Properties contain binary UUID, probably a programming error');
+ }
+ if ($binaryValue === null) {
+ $uuid = Uuid::uuid4();
+ $this->reallySet($this->uuidColumn, $uuid->getBytes());
+ return $uuid;
+ }
+
+ return Uuid::fromBytes($binaryValue);
+ }
+
+ throw new InvalidArgumentException(sprintf('%s has no UUID column', $this->getTableName()));
+ }
+
+ public function getKeyParams()
+ {
+ $params = array();
+ $key = $this->getKeyName();
+ if (is_array($key)) {
+ foreach ($key as $k) {
+ $params[$k] = $this->get($k);
+ }
+ } else {
+ $params[$key] = $this->get($this->keyName);
+ }
+
+ return $params;
+ }
+
+ /**
+ * Return the unique identifier
+ *
+ * // TODO: may conflict with ->id
+ *
+ * @throws InvalidArgumentException When key can not be calculated
+ *
+ * @return string|array
+ */
+ public function getId()
+ {
+ if (is_array($this->keyName)) {
+ $id = array();
+ foreach ($this->keyName as $key) {
+ if (isset($this->properties[$key])) {
+ $id[$key] = $this->properties[$key];
+ }
+ }
+
+ if (empty($id)) {
+ throw new InvalidArgumentException('Could not evaluate id for multi-column object!');
+ }
+
+ return $id;
+ } else {
+ if (isset($this->properties[$this->keyName])) {
+ return $this->properties[$this->keyName];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the autoinc value if set
+ *
+ * @return int
+ */
+ public function getAutoincId()
+ {
+ if (isset($this->properties[$this->autoincKeyName])) {
+ return (int) $this->properties[$this->autoincKeyName];
+ }
+ return null;
+ }
+
+ protected function forgetAutoincId()
+ {
+ if (isset($this->properties[$this->autoincKeyName])) {
+ $this->properties[$this->autoincKeyName] = null;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Liefert das benutzte Datenbank-Handle
+ *
+ * @return Zend_Db_Adapter_Abstract
+ */
+ public function getDb()
+ {
+ return $this->db;
+ }
+
+ public function hasConnection()
+ {
+ return $this->connection !== null;
+ }
+
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * Lädt einen Datensatz aus der Datenbank und setzt die entsprechenden
+ * Eigenschaften dieses Objekts
+ *
+ * @throws NotFoundError
+ * @return self
+ */
+ protected function loadFromDb()
+ {
+ $select = $this->db->select()->from($this->table)->where($this->createWhere());
+ $properties = $this->db->fetchRow($select);
+
+ if (empty($properties)) {
+ if (is_array($this->getKeyName())) {
+ throw new NotFoundError(
+ 'Failed to load %s for %s',
+ $this->table,
+ $this->createWhere()
+ );
+ } else {
+ throw new NotFoundError(
+ 'Failed to load %s "%s"',
+ $this->table,
+ $this->getLogId()
+ );
+ }
+ }
+
+ return $this->setDbProperties($properties);
+ }
+
+ /**
+ * @param object|array $row
+ * @param Db $db
+ * @return self
+ */
+ public static function fromDbRow($row, Db $db)
+ {
+ $self = (new static())->setConnection($db);
+ if (is_object($row)) {
+ return $self->setDbProperties((array) $row);
+ }
+
+ if (is_array($row)) {
+ return $self->setDbProperties($row);
+ }
+
+ throw new InvalidDataException('array or object', $row);
+ }
+
+ protected function setDbProperties($properties)
+ {
+ foreach ($properties as $key => $val) {
+ if (! array_key_exists($key, $this->properties)) {
+ throw new LogicException(sprintf(
+ 'Trying to set invalid %s key "%s". DB schema change?',
+ $this->table,
+ $key
+ ));
+ }
+ if ($val === null) {
+ $this->properties[$key] = null;
+ } elseif (is_resource($val)) {
+ $this->properties[$key] = stream_get_contents($val);
+ } else {
+ $this->properties[$key] = (string) $val;
+ }
+ }
+
+ $this->setBeingLoadedFromDb();
+ $this->onLoadFromDb();
+ return $this;
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ $this->loadedFromDb = true;
+ $this->loadedProperties = $this->properties;
+ $this->hasBeenModified = false;
+ $this->modifiedProperties = [];
+ }
+
+ public function setLoadedProperty($key, $value)
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ $this->set($key, $value);
+ $this->loadedProperties[$key] = $this->get($key);
+ } else {
+ throw new RuntimeException('Cannot set loaded property for new object');
+ }
+ }
+
+ public function getOriginalProperties()
+ {
+ return $this->loadedProperties;
+ }
+
+ public function getOriginalProperty($key)
+ {
+ $this->assertPropertyExists($key);
+ if ($this->hasBeenLoadedFromDb()) {
+ return $this->loadedProperties[$key];
+ }
+
+ return null;
+ }
+
+ public function resetProperty($key)
+ {
+ $this->set($key, $this->getOriginalProperty($key));
+ if ($this->listModifiedProperties() === [$key]) {
+ $this->hasBeenModified = false;
+ }
+
+ return $this;
+ }
+
+ public function hasBeenLoadedFromDb()
+ {
+ return $this->loadedFromDb;
+ }
+
+ /**
+ * Ändert den entsprechenden Datensatz in der Datenbank
+ *
+ * @return int Anzahl der geänderten Zeilen
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function updateDb()
+ {
+ $properties = $this->getModifiedProperties();
+ if (empty($properties)) {
+ // Fake true, we might have manually set this to "modified"
+ return true;
+ }
+ $this->quoteBinaryProperties($properties);
+
+ // TODO: Remember changed data for audit and log
+ return $this->db->update(
+ $this->table,
+ $properties,
+ $this->createWhere()
+ );
+ }
+
+ /**
+ * Fügt der Datenbank-Tabelle einen entsprechenden Datensatz hinzu
+ *
+ * @return int Anzahl der betroffenen Zeilen
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function insertIntoDb()
+ {
+ $properties = $this->getPropertiesForDb();
+ if ($this->autoincKeyName !== null) {
+ if ($this->protectAutoinc || $properties[$this->autoincKeyName] === null) {
+ unset($properties[$this->autoincKeyName]);
+ }
+ }
+ if ($column = $this->getUuidColumn()) {
+ $properties[$column] = $this->getUniqueId()->getBytes();
+ }
+ $this->quoteBinaryProperties($properties);
+
+ return $this->db->insert($this->table, $properties);
+ }
+
+ protected function quoteBinaryProperties(&$properties)
+ {
+ foreach ($properties as $key => $value) {
+ if ($this->isBinaryColumn($key)) {
+ $properties[$key] = $this->getConnection()->quoteBinary($value);
+ }
+ }
+ }
+
+ protected function isBinaryColumn($column)
+ {
+ return in_array($column, $this->binaryProperties) || $this->getUuidColumn() === $column;
+ }
+
+ /**
+ * Store object to database
+ *
+ * @param DbConnection $db
+ * @return bool Whether storing succeeded. Always true, throws otherwise
+ * @throws DuplicateKeyException
+ */
+ public function store(DbConnection $db = null)
+ {
+ if ($db !== null) {
+ $this->setConnection($db);
+ }
+
+ if ($this->validate() !== true) {
+ throw new InvalidArgumentException(sprintf(
+ '%s[%s] validation failed',
+ $this->table,
+ $this->getLogId()
+ ));
+ }
+
+ if ($this->hasBeenLoadedFromDb() && ! $this->hasBeenModified()) {
+ return true;
+ }
+
+ $this->beforeStore();
+ $table = $this->table;
+ $id = $this->getId();
+
+ try {
+ if ($this->hasBeenLoadedFromDb()) {
+ if ($this->updateDb() !== false) {
+ $this->onUpdate();
+ } else {
+ throw new RuntimeException(sprintf(
+ 'FAILED storing %s "%s"',
+ $table,
+ $this->getLogId()
+ ));
+ }
+ } else {
+ if ($id && $this->existsInDb()) {
+ $logId = '"' . $this->getLogId() . '"';
+
+ if ($autoId = $this->getAutoincId()) {
+ $logId .= sprintf(', %s=%s', $this->autoincKeyName, $autoId);
+ }
+ throw new DuplicateKeyException(
+ 'Trying to recreate %s (%s)',
+ $table,
+ $logId
+ );
+ }
+
+ if ($this->insertIntoDb()) {
+ if ($this->autoincKeyName && $this->getProperty($this->autoincKeyName) === null) {
+ if ($this->connection->isPgsql()) {
+ $this->properties[$this->autoincKeyName] = $this->db->lastInsertId(
+ $table,
+ $this->autoincKeyName
+ );
+ } else {
+ $this->properties[$this->autoincKeyName] = $this->db->lastInsertId();
+ }
+ }
+ // $this->log(sprintf('New %s "%s" has been stored', $table, $id));
+ $this->onInsert();
+ } else {
+ throw new RuntimeException(sprintf(
+ 'FAILED to store new %s "%s"',
+ $table,
+ $this->getLogId()
+ ));
+ }
+ }
+ } catch (Zend_Db_Exception $e) {
+ throw new RuntimeException(sprintf(
+ 'Storing %s[%s] failed: %s {%s}',
+ $this->table,
+ $this->getLogId(),
+ $e->getMessage(),
+ var_export($this->getProperties(), 1) // TODO: Remove properties
+ ));
+ }
+
+ // Hint: order is differs from setBeingLoadedFromDb() as of the onStore hook
+ $this->modifiedProperties = [];
+ $this->hasBeenModified = false;
+ $this->loadedProperties = $this->properties;
+ $this->onStore();
+ $this->loadedFromDb = true;
+
+ return true;
+ }
+
+ /**
+ * Delete item from DB
+ *
+ * @return int Affected rows
+ */
+ protected function deleteFromDb()
+ {
+ return $this->db->delete(
+ $this->table,
+ $this->createWhere()
+ );
+ }
+
+ /**
+ * @param string $key
+ * @return self
+ * @throws InvalidArgumentException
+ */
+ protected function setKey($key)
+ {
+ $keyname = $this->getKeyName();
+ if (is_array($keyname)) {
+ if (! is_array($key)) {
+ throw new InvalidArgumentException(sprintf(
+ '%s has a multicolumn key, array required',
+ $this->table
+ ));
+ }
+ foreach ($keyname as $k) {
+ if (! array_key_exists($k, $key)) {
+ // We allow for null in multicolumn keys:
+ $key[$k] = null;
+ }
+ $this->set($k, $key[$k]);
+ }
+ } else {
+ $this->set($keyname, $key);
+ }
+ return $this;
+ }
+
+ protected function existsInDb()
+ {
+ $result = $this->db->fetchRow(
+ $this->db->select()->from($this->table)->where($this->createWhere())
+ );
+ return $result !== false;
+ }
+
+ public function createWhere()
+ {
+ if ($this->hasUuidColumn() && $this->properties[$this->uuidColumn] !== null) {
+ return $this->db->quoteInto(
+ sprintf('%s = ?', $this->getUuidColumn()),
+ $this->connection->quoteBinary($this->getUniqueId()->getBytes())
+ );
+ }
+ if ($id = $this->getAutoincId()) {
+ if ($originalId = $this->getOriginalProperty($this->autoincKeyName)) {
+ return $this->db->quoteInto(
+ sprintf('%s = ?', $this->autoincKeyName),
+ $originalId
+ );
+ }
+ return $this->db->quoteInto(
+ sprintf('%s = ?', $this->autoincKeyName),
+ $id
+ );
+ }
+
+ $key = $this->getKeyName();
+
+ if (is_array($key) && ! empty($key)) {
+ $where = array();
+ foreach ($key as $k) {
+ if ($this->hasBeenLoadedFromDb()) {
+ if ($this->loadedProperties[$k] === null) {
+ $where[] = sprintf('%s IS NULL', $k);
+ } else {
+ $where[] = $this->createQuotedWhere($k, $this->loadedProperties[$k]);
+ }
+ } else {
+ if ($this->properties[$k] === null) {
+ $where[] = sprintf('%s IS NULL', $k);
+ } else {
+ $where[] = $this->createQuotedWhere($k, $this->properties[$k]);
+ }
+ }
+ }
+
+ return implode(' AND ', $where);
+ } else {
+ if ($this->hasBeenLoadedFromDb()) {
+ return $this->createQuotedWhere($key, $this->loadedProperties[$key]);
+ } else {
+ return $this->createQuotedWhere($key, $this->properties[$key]);
+ }
+ }
+ }
+
+ protected function createQuotedWhere($column, $value)
+ {
+ return $this->db->quoteInto(
+ sprintf('%s = ?', $column),
+ $this->eventuallyQuoteBinary($value, $column)
+ );
+ }
+
+ protected function eventuallyQuoteBinary($value, $column)
+ {
+ if ($this->isBinaryColumn($column)) {
+ return $this->connection->quoteBinary($value);
+ } else {
+ return $value;
+ }
+ }
+
+ protected function getLogId()
+ {
+ $id = $this->getId();
+ if (is_array($id)) {
+ $logId = json_encode($id);
+ } else {
+ $logId = $id;
+ }
+
+ if ($logId === null && $this->autoincKeyName) {
+ $logId = $this->getAutoincId();
+ }
+
+ return $logId;
+ }
+
+ public function delete()
+ {
+ $table = $this->table;
+
+ if (! $this->hasBeenLoadedFromDb()) {
+ throw new LogicException(sprintf(
+ 'Cannot delete %s "%s", it has not been loaded from Db',
+ $table,
+ $this->getLogId()
+ ));
+ }
+
+ if (! $this->existsInDb()) {
+ throw new InvalidArgumentException(sprintf(
+ 'Cannot delete %s "%s", it does not exist',
+ $table,
+ $this->getLogId()
+ ));
+ }
+ $this->beforeDelete();
+ if (! $this->deleteFromDb()) {
+ throw new RuntimeException(sprintf(
+ 'Deleting %s (%s) FAILED',
+ $table,
+ $this->getLogId()
+ ));
+ }
+ // $this->log(sprintf('%s "%s" has been DELETED', $table, this->getLogId()));
+ $this->onDelete();
+ $this->loadedFromDb = false;
+ return true;
+ }
+
+ public function __clone()
+ {
+ $this->onClone();
+ $this->forgetAutoincId();
+ $this->loadedFromDb = false;
+ $this->hasBeenModified = true;
+ }
+
+ protected function onClone()
+ {
+ }
+
+ /**
+ * @param array $properties
+ * @param DbConnection|null $connection
+ *
+ * @return static
+ */
+ public static function create($properties = array(), DbConnection $connection = null)
+ {
+ $obj = new static();
+ if ($connection !== null) {
+ $obj->setConnection($connection);
+ }
+ $obj->setProperties($properties);
+ return $obj;
+ }
+
+ protected static function classWasPrefetched()
+ {
+ $class = get_called_class();
+ return array_key_exists($class, self::$prefetched);
+ }
+
+ /**
+ * @param $key
+ * @return static|bool
+ */
+ protected static function getPrefetched($key)
+ {
+ $class = get_called_class();
+ if (static::hasPrefetched($key)) {
+ if (is_string($key)
+ && array_key_exists($class, self::$prefetchedNames)
+ && array_key_exists($key, self::$prefetchedNames[$class])
+ ) {
+ return self::$prefetched[$class][
+ self::$prefetchedNames[$class][$key]
+ ];
+ } else {
+ return self::$prefetched[$class][$key];
+ }
+ } else {
+ return false;
+ }
+ }
+
+ protected static function hasPrefetched($key)
+ {
+ $class = get_called_class();
+ if (! array_key_exists($class, self::$prefetchStats)) {
+ self::$prefetchStats[$class] = (object) array(
+ 'miss' => 0,
+ 'hits' => 0,
+ 'hitNames' => 0,
+ 'combinedMiss' => 0
+ );
+ }
+
+ if (is_array($key)) {
+ self::$prefetchStats[$class]->combinedMiss++;
+ return false;
+ }
+
+ if (array_key_exists($class, self::$prefetched)) {
+ if (is_string($key)
+ && array_key_exists($class, self::$prefetchedNames)
+ && array_key_exists($key, self::$prefetchedNames[$class])
+ ) {
+ self::$prefetchStats[$class]->hitNames++;
+ return true;
+ } elseif (array_key_exists($key, self::$prefetched[$class])) {
+ self::$prefetchStats[$class]->hits++;
+ return true;
+ } else {
+ self::$prefetchStats[$class]->miss++;
+ return false;
+ }
+ } else {
+ self::$prefetchStats[$class]->miss++;
+ return false;
+ }
+ }
+
+ public static function getPrefetchStats()
+ {
+ return self::$prefetchStats;
+ }
+
+ /**
+ * @param $id
+ * @param DbConnection $connection
+ * @return static
+ * @throws NotFoundError
+ */
+ public static function loadWithAutoIncId($id, DbConnection $connection)
+ {
+ /* Need to cast to int, otherwise the id will be matched against
+ * object_name, which may wreak havoc if an object has a
+ * object_name matching some id. Note that DbObject::set() and
+ * DbObject::setDbProperties() will convert any property to
+ * string, including ids.
+ */
+ $id = (int) $id;
+
+ if ($prefetched = static::getPrefetched($id)) {
+ return $prefetched;
+ }
+
+ $obj = new static;
+ if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) {
+ $table = $obj->getTableName();
+ assert($connection instanceof Db);
+ $uuid = UuidLookup::requireUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch());
+
+ return self::$dbObjectStore->load($table, $uuid);
+ }
+
+ $obj->setConnection($connection)
+ ->set($obj->autoincKeyName, $id)
+ ->loadFromDb();
+
+ return $obj;
+ }
+
+ /**
+ * @param $id
+ * @param DbConnection $connection
+ * @return static
+ * @throws NotFoundError
+ */
+ public static function load($id, DbConnection $connection)
+ {
+ if ($prefetched = static::getPrefetched($id)) {
+ return $prefetched;
+ }
+ /** @var DbObject $obj */
+ $obj = new static;
+
+ if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) {
+ $table = $obj->getTableName();
+ assert($connection instanceof Db);
+ $uuid = UuidLookup::requireUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch());
+
+ return self::$dbObjectStore->load($table, $uuid);
+ }
+
+ $obj->setConnection($connection)->setKey($id)->loadFromDb();
+
+ return $obj;
+ }
+
+ /**
+ * @param DbConnection $connection
+ * @param \Zend_Db_Select $query
+ * @param string|null $keyColumn
+ *
+ * @return static[]
+ */
+ public static function loadAll(DbConnection $connection, $query = null, $keyColumn = null)
+ {
+ $objects = array();
+ $db = $connection->getDbAdapter();
+
+ if ($query === null) {
+ $dummy = new static;
+ $select = $db->select()->from($dummy->table);
+ } else {
+ $select = $query;
+ }
+ $rows = $db->fetchAll($select);
+
+ foreach ($rows as $row) {
+ /** @var DbObject $obj */
+ $obj = new static;
+ $obj->setConnection($connection)->setDbProperties($row);
+ if ($keyColumn === null) {
+ $objects[] = $obj;
+ } else {
+ $objects[$row->$keyColumn] = $obj;
+ }
+ }
+
+ return $objects;
+ }
+
+ /**
+ * @param DbConnection $connection
+ * @param bool $force
+ *
+ * @return static[]
+ */
+ public static function prefetchAll(DbConnection $connection, $force = false)
+ {
+ $dummy = static::create();
+ $class = get_class($dummy);
+ $autoInc = $dummy->getAutoincKeyName();
+ $keyName = $dummy->getKeyName();
+
+ if ($force || ! array_key_exists($class, self::$prefetched)) {
+ self::$prefetched[$class] = static::loadAll($connection, null, $autoInc);
+ if (! is_array($keyName) && $keyName !== $autoInc) {
+ foreach (self::$prefetched[$class] as $k => $v) {
+ self::$prefetchedNames[$class][$v->$keyName] = $k;
+ }
+ }
+ }
+
+ return self::$prefetched[$class];
+ }
+
+ public static function clearPrefetchCache()
+ {
+ $class = get_called_class();
+ if (! array_key_exists($class, self::$prefetched)) {
+ return;
+ }
+
+ unset(self::$prefetched[$class]);
+ unset(self::$prefetchedNames[$class]);
+ unset(self::$prefetchStats[$class]);
+ }
+
+ public static function clearAllPrefetchCaches()
+ {
+ self::$prefetched = array();
+ self::$prefetchedNames = array();
+ self::$prefetchStats = array();
+ }
+
+ /**
+ * @param $id
+ * @param DbConnection $connection
+ * @return bool
+ */
+ public static function exists($id, DbConnection $connection)
+ {
+ if (static::getPrefetched($id)) {
+ return true;
+ } elseif (static::classWasPrefetched()) {
+ return false;
+ }
+
+ /** @var DbObject $obj */
+ $obj = new static;
+ if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) {
+ $table = $obj->getTableName();
+ assert($connection instanceof Db);
+ $uuid = UuidLookup::findUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch());
+ if ($uuid) {
+ return self::$dbObjectStore->exists($table, $uuid);
+ }
+
+ return false;
+ }
+
+ $obj->setConnection($connection)->setKey($id);
+ return $obj->existsInDb();
+ }
+
+ public static function uniqueIdExists(UuidInterface $uuid, DbConnection $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $obj = new static;
+ $column = $obj->getUuidColumn();
+ $query = $db->select()
+ ->from($obj->getTableName(), $column)
+ ->where("$column = ?", $connection->quoteBinary($uuid->getBytes()));
+
+ $result = $db->fetchRow($query);
+
+ return $result !== false;
+ }
+
+ public static function requireWithUniqueId(UuidInterface $uuid, DbConnection $connection)
+ {
+ if ($object = static::loadWithUniqueId($uuid, $connection)) {
+ return $object;
+ }
+
+ throw new NotFoundError(sprintf(
+ 'No %s with UUID=%s has been found',
+ (new static)->getTableName(),
+ $uuid->toString()
+ ));
+ }
+
+ public static function loadWithUniqueId(UuidInterface $uuid, DbConnection $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $obj = new static;
+
+ if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) {
+ $table = $obj->getTableName();
+ assert($connection instanceof Db);
+ return self::$dbObjectStore->load($table, $uuid);
+ }
+
+ $query = $db->select()
+ ->from($obj->getTableName())
+ ->where($obj->getUuidColumn() . ' = ?', $connection->quoteBinary($uuid->getBytes()));
+
+ $result = $db->fetchRow($query);
+
+ if ($result) {
+ return $obj->setConnection($connection)->setDbProperties($result);
+ }
+
+ return null;
+ }
+
+ public function setUniqueId(UuidInterface $uuid)
+ {
+ if ($column = $this->getUuidColumn()) {
+ $binary = $uuid->getBytes();
+ $current = $this->get($column);
+ if ($current === null) {
+ $this->set($column, $binary);
+ } else {
+ if ($current !== $binary) {
+ throw new RuntimeException(sprintf(
+ 'Changing the UUID (from %s to %s) is not allowed',
+ Uuid::fromBytes($current)->toString(),
+ Uuid::fromBytes($binary)->toString()
+ ));
+ }
+ }
+ }
+ }
+
+ public function __destruct()
+ {
+ unset($this->db);
+ unset($this->connection);
+ }
+}
diff --git a/library/Director/Data/Db/DbObjectStore.php b/library/Director/Data/Db/DbObjectStore.php
new file mode 100644
index 0000000..bc69b5a
--- /dev/null
+++ b/library/Director/Data/Db/DbObjectStore.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchActivity;
+use Icinga\Module\Director\Db\Branch\BranchedObject;
+use Icinga\Module\Director\Db\Branch\MergeErrorDeleteMissingObject;
+use Icinga\Module\Director\Db\Branch\MergeErrorModificationForMissingObject;
+use Icinga\Module\Director\Db\Branch\MergeErrorRecreateOnMerge;
+use Icinga\Module\Director\Db\DbUtil;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Ramsey\Uuid\UuidInterface;
+
+/**
+ * Loader for Icinga/DbObjects
+ *
+ * Is aware of branches and prefetching. I would prefer to see a StoreInterface,
+ * with one of the above wrapping the other. But for now, this helps to clean things
+ * up
+ */
+class DbObjectStore
+{
+ /** @var Db */
+ protected $connection;
+
+ /** @var ?Branch */
+ protected $branch;
+
+ public function __construct(Db $connection, Branch $branch = null)
+ {
+ $this->connection = $connection;
+ $this->branch = $branch;
+ }
+
+ /**
+ * @param $tableName
+ * @param UuidInterface $uuid
+ * @return DbObject|null
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function load($tableName, UuidInterface $uuid)
+ {
+ $branchedObject = BranchedObject::load($this->connection, $tableName, $uuid, $this->branch);
+ $object = $branchedObject->getBranchedDbObject($this->connection);
+ if ($object === null) {
+ return null;
+ }
+
+ $object->setBeingLoadedFromDb();
+
+ return $object;
+ }
+
+ /**
+ * @param string $tableName
+ * @param string $arrayIdx
+ * @return DbObject[]|IcingaObject[]
+ * @throws MergeErrorRecreateOnMerge
+ * @throws MergeErrorDeleteMissingObject
+ * @throws MergeErrorModificationForMissingObject
+ */
+ public function loadAll($tableName, $arrayIdx = 'uuid')
+ {
+ $db = $this->connection->getDbAdapter();
+ $class = DbObjectTypeRegistry::classByType($tableName);
+ $query = $db->select()->from($tableName)->order($arrayIdx);
+ $result = [];
+ foreach ($db->fetchAll($query) as $row) {
+ $row->uuid = DbUtil::binaryResult($row->uuid);
+ $result[$row->uuid] = $class::create((array) $row, $this->connection);
+ $result[$row->uuid]->setBeingLoadedFromDb();
+ }
+ if ($this->branch && $this->branch->isBranch()) {
+ $query = $db->select()
+ ->from(BranchActivity::DB_TABLE)
+ ->where('branch_uuid = ?', $this->connection->quoteBinary($this->branch->getUuid()->getBytes()))
+ ->order('timestamp_ns ASC');
+ $rows = $db->fetchAll($query);
+ foreach ($rows as $row) {
+ $activity = BranchActivity::fromDbRow($row);
+ if ($activity->getObjectTable() !== $tableName) {
+ continue;
+ }
+ $uuid = $activity->getObjectUuid();
+ $binaryUuid = $uuid->getBytes();
+
+ $exists = isset($result[$binaryUuid]);
+ if ($activity->isActionCreate()) {
+ if ($exists) {
+ throw new MergeErrorRecreateOnMerge($activity);
+ } else {
+ $new = $activity->createDbObject($this->connection);
+ $new->setBeingLoadedFromDb();
+ $result[$binaryUuid] = $new;
+ }
+ } elseif ($activity->isActionDelete()) {
+ if ($exists) {
+ unset($result[$binaryUuid]);
+ } else {
+ throw new MergeErrorDeleteMissingObject($activity);
+ }
+ } else {
+ if ($exists) {
+ $activity->applyToDbObject($result[$binaryUuid])->setBeingLoadedFromDb();
+ } else {
+ throw new MergeErrorModificationForMissingObject($activity);
+ }
+ }
+ }
+ }
+
+ if ($arrayIdx === 'uuid') {
+ return $result;
+ }
+
+ $indexedResult = [];
+ foreach ($result as $object) {
+ $indexedResult[$object->get($arrayIdx)] = $object;
+ }
+
+ return $indexedResult;
+ }
+
+ public function exists($tableName, UuidInterface $uuid)
+ {
+ return BranchedObject::exists($this->connection, $tableName, $uuid, $this->branch->getUuid());
+ }
+
+ public function store(DbObject $object)
+ {
+ if ($this->branch && $this->branch->isBranch()) {
+ $activity = BranchActivity::forDbObject($object, $this->branch);
+ $this->connection->runFailSafeTransaction(function () use ($activity) {
+ $activity->store($this->connection);
+ BranchedObject::withActivity($activity, $this->connection)->store($this->connection);
+ });
+
+ return true;
+ } else {
+ return $object->store($this->connection);
+ }
+ }
+
+ public function delete(DbObject $object)
+ {
+ if ($this->branch && $this->branch->isBranch()) {
+ $activity = BranchActivity::deleteObject($object, $this->branch);
+ $this->connection->runFailSafeTransaction(function () use ($activity) {
+ $activity->store($this->connection);
+ BranchedObject::load(
+ $this->connection,
+ $activity->getObjectTable(),
+ $activity->getObjectUuid(),
+ $this->branch
+ )->delete($this->connection);
+ });
+ return true;
+ }
+
+ return $object->delete();
+ }
+
+ public function getBranch()
+ {
+ return $this->branch;
+ }
+}
diff --git a/library/Director/Data/Db/DbObjectTypeRegistry.php b/library/Director/Data/Db/DbObjectTypeRegistry.php
new file mode 100644
index 0000000..0c226d6
--- /dev/null
+++ b/library/Director/Data/Db/DbObjectTypeRegistry.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class DbObjectTypeRegistry
+{
+ /**
+ * @param $type
+ * @return string|DbObject Fake typehint for IDE
+ */
+ public static function classByType($type)
+ {
+ // allow for icinga_host and host
+ $type = lcfirst(preg_replace('/^icinga_/', '', $type));
+
+ // Hint: Sync/Import are not IcingaObjects, this should be reconsidered:
+ if (strpos($type, 'import') === 0 || strpos($type, 'sync') === 0) {
+ $prefix = '';
+ } elseif (strpos($type, 'data') === false) {
+ $prefix = 'Icinga';
+ } else {
+ $prefix = 'Director';
+ }
+
+ // TODO: Provide a more sophisticated solution
+ if ($type === 'hostgroup') {
+ $type = 'hostGroup';
+ } elseif ($type === 'usergroup') {
+ $type = 'userGroup';
+ } elseif ($type === 'timeperiod') {
+ $type = 'timePeriod';
+ } elseif ($type === 'servicegroup') {
+ $type = 'serviceGroup';
+ } elseif ($type === 'service_set' || $type === 'serviceset') {
+ $type = 'serviceSet';
+ } elseif ($type === 'apiuser') {
+ $type = 'apiUser';
+ } elseif ($type === 'host_template_choice') {
+ $type = 'templateChoiceHost';
+ } elseif ($type === 'service_template_choice') {
+ $type = 'TemplateChoiceService';
+ } elseif ($type === 'scheduled_downtime' || $type === 'scheduled-downtime') {
+ $type = 'ScheduledDowntime';
+ }
+
+ return 'Icinga\\Module\\Director\\Objects\\' . $prefix . ucfirst($type);
+ }
+
+ public static function tableNameByType($type)
+ {
+ $class = static::classByType($type);
+ $dummy = $class::create([]);
+
+ return $dummy->getTableName();
+ }
+
+ public static function shortTypeForObject(DbObject $object)
+ {
+ if ($object instanceof IcingaObject) {
+ return $object->getShortTableName();
+ }
+
+ return $object->getTableName();
+ }
+
+ public static function newObject($type, $properties = [], Db $db = null)
+ {
+ /** @var DbObject $class fake hint for the IDE, it's a string */
+ $class = self::classByType($type);
+ return $class::create($properties, $db);
+ }
+}
diff --git a/library/Director/Data/Db/DbObjectWithSettings.php b/library/Director/Data/Db/DbObjectWithSettings.php
new file mode 100644
index 0000000..4f6b139
--- /dev/null
+++ b/library/Director/Data/Db/DbObjectWithSettings.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Module\Director\Db;
+
+abstract class DbObjectWithSettings extends DbObject
+{
+ /** @var Db $connection */
+ protected $connection;
+
+ protected $settingsTable = 'your_table_name';
+
+ protected $settingsRemoteId = 'column_pointing_to_main_table_id';
+
+ protected $settings = [];
+
+ public function set($key, $value)
+ {
+ if ($this->hasProperty($key)) {
+ return parent::set($key, $value);
+ } elseif ($this->hasSetterForProperty($key)) { // Hint: hasProperty checks only for Getters
+ return parent::set($key, $value);
+ }
+
+ if (! \array_key_exists($key, $this->settings) || $value !== $this->settings[$key]) {
+ $this->hasBeenModified = true;
+ }
+
+ $this->settings[$key] = $value;
+ return $this;
+ }
+
+ public function get($key)
+ {
+ if ($this->hasProperty($key)) {
+ return parent::get($key);
+ }
+
+ if (array_key_exists($key, $this->settings)) {
+ return $this->settings[$key];
+ }
+
+ return parent::get($key);
+ }
+
+ public function setSettings($settings)
+ {
+ $settings = (array) $settings;
+ ksort($settings);
+ if ($settings !== $this->settings) {
+ $this->settings = $settings;
+ $this->hasBeenModified = true;
+ }
+
+ return $this;
+ }
+
+ public function getSettings()
+ {
+ // Sort them, important only for new objects
+ ksort($this->settings);
+ return $this->settings;
+ }
+
+ public function getSetting($name, $default = null)
+ {
+ if (array_key_exists($name, $this->settings)) {
+ return $this->settings[$name];
+ }
+
+ return $default;
+ }
+
+ public function getStoredSetting($name, $default = null)
+ {
+ $stored = $this->fetchSettingsFromDb();
+ if (array_key_exists($name, $stored)) {
+ return $stored[$name];
+ }
+
+ return $default;
+ }
+
+ public function __unset($key)
+ {
+ if ($this->hasProperty($key)) {
+ parent::__unset($key);
+ }
+
+ if (array_key_exists($key, $this->settings)) {
+ unset($this->settings[$key]);
+ $this->hasBeenModified = true;
+ }
+ }
+
+ protected function onStore()
+ {
+ $old = $this->fetchSettingsFromDb();
+ $oldKeys = array_keys($old);
+ $newKeys = array_keys($this->settings);
+ $add = [];
+ $mod = [];
+ $del = [];
+ $id = $this->get('id');
+
+ foreach ($this->settings as $key => $val) {
+ if (array_key_exists($key, $old)) {
+ if ($old[$key] !== $this->settings[$key]) {
+ $mod[$key] = $this->settings[$key];
+ }
+ } else {
+ $add[$key] = $this->settings[$key];
+ }
+ }
+
+ foreach (array_diff($oldKeys, $newKeys) as $key) {
+ $del[] = $key;
+ }
+
+ $where = sprintf($this->settingsRemoteId . ' = %d AND setting_name = ?', $id);
+ $db = $this->getDb();
+ foreach ($mod as $key => $val) {
+ $db->update(
+ $this->settingsTable,
+ ['setting_value' => $val],
+ $db->quoteInto($where, $key)
+ );
+ }
+
+ foreach ($add as $key => $val) {
+ $db->insert(
+ $this->settingsTable,
+ [
+ $this->settingsRemoteId => $id,
+ 'setting_name' => $key,
+ 'setting_value' => $val
+ ]
+ );
+ }
+
+ if (! empty($del)) {
+ $where = sprintf($this->settingsRemoteId . ' = %d AND setting_name IN (?)', $id);
+ $db->delete($this->settingsTable, $db->quoteInto($where, $del));
+ }
+ }
+
+ protected function fetchSettingsFromDb()
+ {
+ $db = $this->getDb();
+ $id = $this->get('id');
+ if (! $id) {
+ return [];
+ }
+
+ return $db->fetchPairs(
+ $db->select()
+ ->from($this->settingsTable, ['setting_name', 'setting_value'])
+ ->where($this->settingsRemoteId . ' = ?', $id)
+ ->order('setting_name')
+ );
+ }
+
+ protected function onLoadFromDb()
+ {
+ $this->settings = $this->fetchSettingsFromDb();
+ }
+}
diff --git a/library/Director/Data/Db/IcingaObjectFilterRenderer.php b/library/Director/Data/Db/IcingaObjectFilterRenderer.php
new file mode 100644
index 0000000..de2ec79
--- /dev/null
+++ b/library/Director/Data/Db/IcingaObjectFilterRenderer.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterException;
+use Icinga\Data\Filter\FilterExpression;
+
+class IcingaObjectFilterRenderer
+{
+ /** @var Filter */
+ protected $filter;
+
+ /** @var IcingaObjectQuery */
+ protected $query;
+
+ protected $columnMap = [
+ 'host.name' => 'host.object_name',
+ 'service.name' => 'service.object_name',
+ ];
+
+ public function __construct(Filter $filter, IcingaObjectQuery $query)
+ {
+ $this->filter = clone($filter);
+ $this->fixFilterColumns($this->filter);
+ $this->query = $query;
+ }
+
+ /**
+ * @param Filter $filter
+ * @param IcingaObjectQuery $query
+ *
+ * @return IcingaObjectQuery
+ */
+ public static function apply(Filter $filter, IcingaObjectQuery $query)
+ {
+ $self = new static($filter, $query);
+ return $self->applyFilterToQuery();
+ }
+
+ /**
+ * @return IcingaObjectQuery
+ */
+ protected function applyFilterToQuery()
+ {
+ $this->query->escapedWhere($this->renderFilter($this->filter));
+ return $this->query;
+ }
+
+ /**
+ * @param Filter $filter
+ * @return string
+ */
+ protected function renderFilter(Filter $filter)
+ {
+ if ($filter->isChain()) {
+ /** @var FilterChain $filter */
+ return $this->renderFilterChain($filter);
+ } else {
+ /** @var FilterExpression $filter */
+ return $this->renderFilterExpression($filter);
+ }
+ }
+
+ /**
+ * @param FilterChain $filter
+ *
+ * @throws FilterException
+ *
+ * @return string
+ */
+ protected function renderFilterChain(FilterChain $filter)
+ {
+ $parts = array();
+ foreach ($filter->filters() as $sub) {
+ $parts[] = $this->renderFilter($sub);
+ }
+
+ $op = $filter->getOperatorName();
+ if ($op === 'NOT') {
+ if (count($parts) !== 1) {
+ throw new FilterException(
+ 'NOT should have exactly one child, got %s',
+ count($parts)
+ );
+ }
+
+ return $op . ' ' . $parts[0];
+ } else {
+ if ($filter->isRootNode()) {
+ return implode(' ' . $op . ' ', $parts);
+ } else {
+ return '(' . implode(' ' . $op . ' ', $parts) . ')';
+ }
+ }
+ }
+
+ protected function fixFilterColumns(Filter $filter)
+ {
+ if ($filter->isExpression()) {
+ /** @var FilterExpression $filter */
+ $col = $filter->getColumn();
+ if (array_key_exists($col, $this->columnMap)) {
+ $filter->setColumn($this->columnMap[$col]);
+ }
+ if (strpos($col, 'vars.') === false) {
+ $filter->setExpression(json_decode($filter->getExpression()));
+ }
+ } else {
+ /** @var FilterChain $filter */
+ foreach ($filter->filters() as $sub) {
+ $this->fixFilterColumns($sub);
+ }
+ }
+ }
+
+ /**
+ * @param FilterExpression $filter
+ *
+ * @return string
+ */
+ protected function renderFilterExpression(FilterExpression $filter)
+ {
+ $query = $this->query;
+ $column = $query->getAliasForRequiredFilterColumn($filter->getColumn());
+ return $query->whereToSql(
+ $column,
+ $filter->getSign(),
+ $filter->getExpression()
+ );
+ }
+}
diff --git a/library/Director/Data/Db/IcingaObjectQuery.php b/library/Director/Data/Db/IcingaObjectQuery.php
new file mode 100644
index 0000000..4556ba7
--- /dev/null
+++ b/library/Director/Data/Db/IcingaObjectQuery.php
@@ -0,0 +1,255 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Data\Db\DbQuery;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Module\Director\Db;
+use Zend_Db_Expr as ZfDbExpr;
+use Zend_Db_Select as ZfDbSelect;
+
+class IcingaObjectQuery
+{
+ const BASE_ALIAS = 'o';
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var string */
+ protected $type;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var ZfDbSelect */
+ protected $query;
+
+ /** @var bool */
+ protected $resolved;
+
+ /** @var array joined tables, alias => table */
+ protected $requiredTables;
+
+ /** @var array maps table aliases, alias => table*/
+ protected $aliases;
+
+ /** @var DbQuery */
+ protected $dummyQuery;
+
+ /** @var array varname => alias */
+ protected $joinedVars = array();
+
+ protected $customVarTable;
+
+ protected $baseQuery;
+
+ /**
+ * IcingaObjectQuery constructor.
+ *
+ * @param string $type
+ * @param Db $connection
+ * @param bool $resolved
+ */
+ public function __construct($type, Db $connection, $resolved = true)
+ {
+ $this->type = $type;
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ $this->resolved = $resolved;
+ $baseTable = 'icinga_' . $type;
+ $this->baseQuery = $this->db->select()
+ ->from(
+ array(self::BASE_ALIAS => $baseTable),
+ array('name' => 'object_name')
+ )->order(self::BASE_ALIAS . '.object_name');
+ }
+
+ public function joinVar($name)
+ {
+ if (! $this->hasJoinedVar($name)) {
+ $type = $this->type;
+ $alias = $this->safeVarAlias($name);
+ $varAlias = "v_$alias";
+ // TODO: optionally $varRelation = sprintf('icinga_%s_resolved_var', $type);
+ $varRelation = sprintf('icinga_%s_var', $type);
+ $idCol = sprintf('%s.%s_id', $alias, $type);
+
+ $joinOn = sprintf('%s = %s.id', $idCol, self::BASE_ALIAS);
+ $joinVarOn = $this->db->quoteInto(
+ sprintf('%s.checksum = %s.checksum AND %s.varname = ?', $alias, $varAlias, $alias),
+ $name
+ );
+
+ $this->baseQuery->join(
+ array($alias => $varRelation),
+ $joinOn,
+ array()
+ )->join(
+ array($varAlias => 'icinga_var'),
+ $joinVarOn,
+ array($alias => $varAlias . '.varvalue')
+ );
+
+ $this->joinedVars[$name] = $varAlias . '.varvalue';
+ }
+
+ return $this;
+ }
+
+ // Debug only
+ public function getSql()
+ {
+ return (string) $this->baseQuery;
+ }
+
+ public function listNames()
+ {
+ return $this->db->fetchCol(
+ $this->baseQuery
+ );
+ }
+
+ protected function hasJoinedVar($name)
+ {
+ return array_key_exists($name, $this->joinedVars);
+ }
+
+ public function getJoinedVarAlias($name)
+ {
+ return $this->joinedVars[$name];
+ }
+
+ // TODO: recheck this
+ protected function safeVarAlias($name)
+ {
+ $alias = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $name);
+ $cnt = 1;
+ $checkAlias = $alias;
+ while (in_array($checkAlias, $this->joinedVars)) {
+ $cnt++;
+ $checkAlias = $alias . '_' . $cnt;
+ }
+
+ return $checkAlias;
+ }
+
+ public function escapedWhere($where)
+ {
+ $this->baseQuery->where(new ZfDbExpr($where));
+ }
+
+ /**
+ * @param $column
+ * @return string
+ * @throws NotFoundError
+ * @throws NotImplementedError
+ */
+ public function getAliasForRequiredFilterColumn($column)
+ {
+ list($key, $sub) = $this->splitFilterKey($column);
+ if ($sub === null) {
+ return $key;
+ } else {
+ $objectType = $key;
+ }
+
+ if ($objectType === $this->type) {
+ list($key, $sub) = $this->splitFilterKey($sub);
+ if ($sub === null) {
+ return $key;
+ }
+
+ if ($key === 'vars') {
+ return $this->joinVar($sub)->getJoinedVarAlias($sub);
+ } else {
+ throw new NotFoundError('Not yet, my type: %s - %s', $objectType, $key);
+ }
+ } else {
+ throw new NotImplementedError('Not yet: %s - %s', $objectType, $sub);
+ }
+ }
+
+ protected function splitFilterKey($key)
+ {
+ $dot = strpos($key, '.');
+ if ($dot === false) {
+ return [$key, null];
+ } else {
+ return [substr($key, 0, $dot), substr($key, $dot + 1)];
+ }
+ }
+
+ protected function requireTable($name)
+ {
+ if ($alias = $this->getTableAliasFromQuery($name)) {
+ return $alias;
+ }
+
+ $this->joinTable($name);
+ }
+
+ protected function joinTable($name)
+ {
+ if (!array_key_exists($name, $this->requiredTables)) {
+ $alias = $this->makeAlias($name);
+ }
+
+ return $this->tableAliases($name);
+ }
+
+ protected function hasAlias($name)
+ {
+ return array_key_exists($name, $this->aliases);
+ }
+
+ protected function makeAlias($name)
+ {
+ if (substr($name, 0, 7) === 'icinga_') {
+ $shortName = substr($name, 7);
+ } else {
+ $shortName = $name;
+ }
+
+ $parts = preg_split('/_/', $shortName, -1);
+ $alias = '';
+ foreach ($parts as $part) {
+ $alias .= $part[0];
+ if (! $this->hasAlias($alias)) {
+ return $alias;
+ }
+ }
+
+ $cnt = 1;
+ do {
+ $cnt++;
+ if (! $this->hasAlias($alias . $cnt)) {
+ return $alias . $cnt;
+ }
+ } while (! $this->hasAlias($alias));
+
+ return $alias;
+ }
+
+ protected function getTableAliasFromQuery($table)
+ {
+ $tables = $this->query->getPart('from');
+ $key = array_search($table, $tables);
+ if ($key === null || $key === false) {
+ return false;
+ }
+ /*
+ 'joinType' => $type,
+ 'schema' => $schema,
+ 'tableName' => $tableName,
+ 'joinCondition' => $cond
+ */
+ return $key;
+ }
+
+ public function whereToSql($col, $sign, $expression)
+ {
+ return $this->connection->renderFilter(Filter::expression($col, $sign, $expression));
+ }
+}
diff --git a/library/Director/Data/Db/ServiceSetQueryBuilder.php b/library/Director/Data/Db/ServiceSetQueryBuilder.php
new file mode 100644
index 0000000..7841d1e
--- /dev/null
+++ b/library/Director/Data/Db/ServiceSetQueryBuilder.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\BranchSupport;
+use Icinga\Module\Director\Db\DbSelectParenthesis;
+use Icinga\Module\Director\Db\DbUtil;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Web\Table\TableWithBranchSupport;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+
+class ServiceSetQueryBuilder
+{
+ use TableWithBranchSupport;
+
+ const TABLE = BranchSupport::TABLE_ICINGA_SERVICE;
+ const BRANCHED_TABLE = BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE;
+ const SET_TABLE = BranchSupport::TABLE_ICINGA_SERVICE_SET;
+ const BRANCHED_SET_TABLE = BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE_SET;
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /**
+ * @param ?UuidInterface $uuid
+ */
+ public function __construct(Db $connection, $uuid = null)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ if ($uuid) {
+ $this->setBranchUuid($uuid);
+ }
+ }
+
+ /**
+ * @return \Zend_Db_Select
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function selectServicesForSet(IcingaServiceSet $set)
+ {
+ $db = $this->connection->getDbAdapter();
+ if ($this->branchUuid) {
+ $right = $this->selectRightBranchedServices($set)->columns($this->getRightBranchedColumns());
+ $left = $this->selectLeftBranchedServices($set)->columns($this->getLeftBranchedColumns());
+ $query = $this->db->select()->from(['u' => $db->select()->union([
+ 'l' => new DbSelectParenthesis($left),
+ 'r' => new DbSelectParenthesis($right),
+ ])]);
+ $query->order('service_set');
+ } else {
+ $query = $this->selectServices($set)->columns($this->getColumns());
+ }
+
+ return $query;
+ }
+
+ protected function selectServices(IcingaServiceSet $set)
+ {
+ return $this->db
+ ->select()
+ ->from(['o' =>self::TABLE], [])
+ ->joinLeft(['os' => self::SET_TABLE], 'os.id = o.service_set_id', [])
+ ->where('os.uuid = ?', $this->connection->quoteBinary($set->getUniqueId()->getBytes()));
+ }
+
+ protected function selectLeftBranchedServices(IcingaServiceSet $set)
+ {
+ return $this
+ ->selectServices($set)
+ ->joinLeft(
+ ['bo' => self::BRANCHED_TABLE],
+ $this->db->quoteInto('bo.uuid = o.uuid AND bo.branch_uuid = ?', $this->getQuotedBranchUuid()),
+ []
+ );
+ }
+
+ protected function selectRightBranchedServices(IcingaServiceSet $set)
+ {
+ return $this->db
+ ->select()
+ ->from(['o' => self::TABLE], [])
+ ->joinRight(['bo' => self::BRANCHED_TABLE], 'bo.uuid = o.uuid', [])
+ ->where('bo.service_set = ?', $set->get('object_name'))
+ ->where('bo.branch_uuid = ?', $this->getQuotedBranchUuid());
+ }
+
+ protected static function resetQueryProperties(\Zend_Db_Select $query)
+ {
+ // TODO: Keep existing UUID, becomes important when using this for other tables too (w/o UNION)
+ // $columns = $query->getPart($query::COLUMNS);
+ $query->reset($query::COLUMNS);
+ $query->columns('uuid');
+ return $query;
+ }
+
+ public function fetchServicesWithQuery(\Zend_Db_Select $query)
+ {
+ static::resetQueryProperties($query);
+ $db = $this->connection->getDbAdapter();
+ $uuids = $db->fetchCol($query);
+
+ $services = [];
+ foreach ($uuids as $uuid) {
+ $service = IcingaService::loadWithUniqueId(Uuid::fromBytes(DbUtil::binaryResult($uuid)), $this->connection);
+ $service->set('service_set', null); // TODO: CHECK THIS!!!!
+
+ $services[$service->getObjectName()] = $service;
+ }
+
+ return $services;
+ }
+
+ protected function getColumns()
+ {
+ return [
+ 'uuid' => 'o.uuid', // MUST be first because of UNION column order, see branchifyColumns()
+ 'id' => 'o.id',
+ 'branch_uuid' => '(null)',
+ 'service_set' => 'os.object_name',
+ 'service' => 'o.object_name',
+ 'disabled' => 'o.disabled',
+ 'object_type' => 'o.object_type',
+ 'blacklisted' => "('n')",
+ ];
+ }
+
+ protected function getLeftBranchedColumns()
+ {
+ $columns = $this->getColumns();
+ $columns['branch_uuid'] = 'bo.branch_uuid';
+ $columns['service_set'] = 'COALESCE(os.object_name, bo.service_set)';
+
+ return $this->branchifyColumns($columns);
+ }
+
+ protected function getRightBranchedColumns()
+ {
+ $columns = $this->getColumns();
+ $columns = $this->branchifyColumns($columns);
+ $columns['branch_uuid'] = 'bo.branch_uuid';
+ $columns['service_set'] = 'bo.service_set';
+ $columns['id'] = '(NULL)';
+
+ return $columns;
+ }
+
+ protected function getQuotedBranchUuid()
+ {
+ return $this->connection->quoteBinary($this->branchUuid->getBytes());
+ }
+}
diff --git a/library/Director/Data/Exporter.php b/library/Director/Data/Exporter.php
new file mode 100644
index 0000000..a2e3191
--- /dev/null
+++ b/library/Director/Data/Exporter.php
@@ -0,0 +1,303 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use gipfl\ZfDb\Adapter\Adapter;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\DirectorDatalist;
+use Icinga\Module\Director\Objects\DirectorDatalistEntry;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Objects\IcingaTemplateChoice;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\InstantiatedViaHook;
+use Icinga\Module\Director\Objects\SyncRule;
+use RuntimeException;
+
+class Exporter
+{
+ /** @var Adapter|\Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var FieldReferenceLoader */
+ protected $fieldReferenceLoader;
+
+ /** @var ?HostServiceLoader */
+ protected $serviceLoader = null;
+
+ protected $exportHostServices = false;
+ protected $showDefaults = false;
+ protected $showIds = false;
+ protected $resolveObjects = false;
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var ?array */
+ protected $chosenProperties = null;
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ $this->fieldReferenceLoader = new FieldReferenceLoader($connection);
+ }
+
+ public function export(DbObject $object)
+ {
+ $props = $object instanceof IcingaObject
+ ? $this->exportIcingaObject($object)
+ : $this->exportDbObject($object);
+
+ ImportExportDeniedProperties::strip($props, $object, $this->showIds);
+ $this->appendTypeSpecificRelations($props, $object);
+
+ if ($this->chosenProperties !== null) {
+ $chosen = [];
+ foreach ($this->chosenProperties as $k) {
+ if (array_key_exists($k, $props)) {
+ $chosen[$k] = $props[$k];
+ }
+ }
+
+ $props = $chosen;
+ }
+
+ ksort($props);
+ return (object) $props;
+ }
+
+ public function enableHostServices($enable = true)
+ {
+ $this->exportHostServices = $enable;
+ return $this;
+ }
+
+ public function showDefaults($show = true)
+ {
+ $this->showDefaults = $show;
+ return $this;
+ }
+
+ public function showIds($show = true)
+ {
+ $this->showIds = $show;
+ return $this;
+ }
+
+ public function resolveObjects($resolve = true)
+ {
+ $this->resolveObjects = $resolve;
+ if ($this->serviceLoader) {
+ $this->serviceLoader->resolveObjects($resolve);
+ }
+
+ return $this;
+ }
+
+ public function filterProperties(array $properties)
+ {
+ $this->chosenProperties = $properties;
+ return $this;
+ }
+
+ protected function appendTypeSpecificRelations(array &$props, DbObject $object)
+ {
+ if ($object instanceof DirectorDatalist) {
+ $props['entries'] = $this->exportDatalistEntries($object);
+ } elseif ($object instanceof DirectorDatafield) {
+ if (isset($props['settings']->datalist_id)) {
+ $props['settings']->datalist = $this->getDatalistNameForId($props['settings']->datalist_id);
+ unset($props['settings']->datalist_id);
+ }
+
+ $props['category'] = isset($props['category_id'])
+ ? $this->getDatafieldCategoryNameForId($props['category_id'])
+ : null;
+ unset($props['category_id']);
+ } elseif ($object instanceof ImportSource) {
+ $props['modifiers'] = $this->exportRowModifiers($object);
+ } elseif ($object instanceof SyncRule) {
+ $props['properties'] = $this->exportSyncProperties($object);
+ } elseif ($object instanceof IcingaCommand) {
+ if (isset($props['arguments'])) {
+ foreach ($props['arguments'] as $key => $argument) {
+ if (property_exists($argument, 'command_id')) {
+ unset($props['arguments'][$key]->command_id);
+ }
+ }
+ }
+ } elseif ($object instanceof DirectorJob) {
+ if ($object->hasTimeperiod()) {
+ $props['timeperiod'] = $object->timeperiod()->getObjectName();
+ }
+ unset($props['timeperiod_id']);
+ } elseif ($object instanceof IcingaTemplateChoice) {
+ if (isset($props['required_template_id'])) {
+ $requiredId = $props['required_template_id'];
+ unset($props['required_template_id']);
+ $props = $this->loadTemplateName($object->getObjectTableName(), $requiredId);
+ }
+
+ $props['members'] = array_values($object->getMembers());
+ } elseif ($object instanceof IcingaServiceSet) {
+ if ($object->get('host_id')) {
+ // Sets on Host
+ throw new RuntimeException('Not yet');
+ }
+ $props['services'] = [];
+ foreach ($object->getServiceObjects() as $serviceObject) {
+ $props['services'][$serviceObject->getObjectName()] = $this->export($serviceObject);
+ }
+ ksort($props['services']);
+ } elseif ($object instanceof IcingaHost) {
+ if ($this->exportHostServices) {
+ $services = [];
+ foreach ($this->serviceLoader()->fetchServicesForHost($object) as $service) {
+ $services[] = $this->export($service);
+ }
+
+ $props['services'] = $services;
+ }
+ }
+ }
+
+ public function serviceLoader()
+ {
+ if ($this->serviceLoader === null) {
+ $this->serviceLoader = new HostServiceLoader($this->connection);
+ $this->serviceLoader->resolveObjects($this->resolveObjects);
+ }
+
+ return $this->serviceLoader;
+ }
+
+ protected function loadTemplateName($table, $id)
+ {
+ $db = $this->db;
+ $query = $db->select()
+ ->from(['o' => $table], 'o.object_name')->where("o.object_type = 'template'")
+ ->where('o.id = ?', $id);
+
+ return $db->fetchOne($query);
+ }
+
+ protected function getDatalistNameForId($id)
+ {
+ $db = $this->db;
+ $query = $db->select()->from('director_datalist', 'list_name')->where('id = ?', (int) $id);
+ return $db->fetchOne($query);
+ }
+
+ protected function getDatafieldCategoryNameForId($id)
+ {
+ $db = $this->db;
+ $query = $db->select()->from('director_datafield_category', 'category_name')->where('id = ?', (int) $id);
+ return $db->fetchOne($query);
+ }
+
+ protected function exportRowModifiers(ImportSource $object)
+ {
+ $modifiers = [];
+ // Hint: they're sorted by priority
+ foreach ($object->fetchRowModifiers() as $modifier) {
+ $modifiers[] = $this->export($modifier);
+ }
+
+ return $modifiers;
+ }
+
+ public function exportSyncProperties(SyncRule $object)
+ {
+ $all = [];
+ $db = $this->db;
+ $sourceNames = $db->fetchPairs(
+ $db->select()->from('import_source', ['id', 'source_name'])
+ );
+
+ foreach ($object->getSyncProperties() as $property) {
+ $properties = $property->getProperties();
+ $properties['source'] = $sourceNames[$properties['source_id']];
+ unset($properties['id']);
+ unset($properties['rule_id']);
+ unset($properties['source_id']);
+ ksort($properties);
+ $all[] = (object) $properties;
+ }
+
+ return $all;
+ }
+
+ /**
+ * @param DbObject $object
+ * @return array
+ */
+ protected function exportDbObject(DbObject $object)
+ {
+ $props = $object->getProperties();
+ if ($object instanceof DbObjectWithSettings) {
+ if ($object instanceof InstantiatedViaHook) {
+ $props['settings'] = (object) $object->getInstance()->exportSettings();
+ } else {
+ $props['settings'] = (object) $object->getSettings(); // Already sorted
+ }
+ }
+ unset($props['uuid']); // Not yet
+ if (! $this->showDefaults) {
+ foreach ($props as $key => $value) {
+ // We assume NULL as a default value for all non-IcingaObject properties
+ if ($value === null) {
+ unset($props[$key]);
+ }
+ }
+ }
+
+ return $props;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @return array
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function exportIcingaObject(IcingaObject $object)
+ {
+ $props = (array) $object->toPlainObject($this->resolveObjects, !$this->showDefaults);
+ if ($object->supportsFields()) {
+ $props['fields'] = $this->fieldReferenceLoader->loadFor($object);
+ }
+
+ return $props;
+ }
+
+ protected function exportDatalistEntries(DirectorDatalist $list)
+ {
+ $entries = [];
+ $id = $list->get('id');
+ if ($id === null) {
+ return $entries;
+ }
+
+ $dbEntries = DirectorDatalistEntry::loadAllForList($list);
+ // Hint: they are loaded with entry_name key
+ ksort($dbEntries);
+
+ foreach ($dbEntries as $entry) {
+ if ($entry->shouldBeRemoved()) {
+ continue;
+ }
+ $plainEntry = $entry->getProperties();
+ unset($plainEntry['list_id']);
+
+ $entries[] = $plainEntry;
+ }
+
+ return $entries;
+ }
+}
diff --git a/library/Director/Data/FieldReferenceLoader.php b/library/Director/Data/FieldReferenceLoader.php
new file mode 100644
index 0000000..1e3d92e
--- /dev/null
+++ b/library/Director/Data/FieldReferenceLoader.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use gipfl\ZfDb\Adapter\Adapter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class FieldReferenceLoader
+{
+ /** @var Adapter|\Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ public function __construct(Db $connection)
+ {
+ $this->db = $connection->getDbAdapter();
+ }
+
+ /**
+ * @param int $id
+ * @return array
+ */
+ public function loadFor(IcingaObject $object)
+ {
+ $db = $this->db;
+ $id = $object->get('id');
+ if ($id === null) {
+ return [];
+ }
+ $type = $object->getShortTableName();
+ $res = $db->fetchAll(
+ $db->select()->from(['f' => "icinga_${type}_field"], [
+ 'f.datafield_id',
+ 'f.is_required',
+ 'f.var_filter',
+ ])->join(['df' => 'director_datafield'], 'df.id = f.datafield_id', [])
+ ->where("${type}_id = ?", (int) $id)
+ ->order('varname ASC')
+ );
+
+ if (empty($res)) {
+ return [];
+ }
+
+ foreach ($res as $field) {
+ $field->datafield_id = (int) $field->datafield_id;
+ }
+
+ return $res;
+ }
+}
diff --git a/library/Director/Data/HostServiceLoader.php b/library/Director/Data/HostServiceLoader.php
new file mode 100644
index 0000000..4cc4b96
--- /dev/null
+++ b/library/Director/Data/HostServiceLoader.php
@@ -0,0 +1,170 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use gipfl\IcingaWeb2\Table\QueryBasedTable;
+use gipfl\ZfDb\Select;
+use Icinga\Data\SimpleQuery;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\AppliedServiceSetLoader;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use Icinga\Module\Director\Web\Table\IcingaHostAppliedServicesTable;
+use Icinga\Module\Director\Web\Table\IcingaServiceSetServiceTable;
+use Icinga\Module\Director\Web\Table\ObjectsTableService;
+use Ramsey\Uuid\Uuid;
+use RuntimeException;
+use Zend_Db_Select;
+
+class HostServiceLoader
+{
+ /** @var Db */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var bool */
+ protected $resolveHostServices = false;
+
+ /** @var bool */
+ protected $resolveObjects = false;
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ }
+
+ public function fetchServicesForHost(IcingaHost $host)
+ {
+ $table = (new ObjectsTableService($this->connection))->setHost($host);
+ $services = $this->fetchServicesForTable($table);
+ if ($this->resolveHostServices) {
+ foreach ($this->fetchAllServicesForHost($host) as $service) {
+ $services[] = $service;
+ }
+ }
+
+ return $services;
+ }
+
+ public function resolveHostServices($enable = true)
+ {
+ $this->resolveHostServices = $enable;
+ return $this;
+ }
+
+ public function resolveObjects($resolve = true)
+ {
+ $this->resolveObjects = $resolve;
+ return $this;
+ }
+
+ protected function fetchAllServicesForHost(IcingaHost $host)
+ {
+ $services = [];
+ /** @var IcingaHost[] $parents */
+ $parents = IcingaTemplateRepository::instanceByObject($host)->getTemplatesFor($host, true);
+ foreach ($parents as $parent) {
+ $table = (new ObjectsTableService($this->connection))
+ ->setHost($parent)
+ ->setInheritedBy($host);
+ foreach ($this->fetchServicesForTable($table) as $service) {
+ $services[] = $service;
+ }
+ }
+
+ foreach ($this->getHostServiceSetTables($host) as $table) {
+ foreach ($this->fetchServicesForTable($table) as $service) {
+ $services[] = $service;
+ }
+ }
+ foreach ($parents as $parent) {
+ foreach ($this->getHostServiceSetTables($parent, $host) as $table) {
+ foreach ($this->fetchServicesForTable($table) as $service) {
+ $services[] = $service;
+ }
+ }
+ }
+
+ $appliedSets = AppliedServiceSetLoader::fetchForHost($host);
+ foreach ($appliedSets as $set) {
+ $table = IcingaServiceSetServiceTable::load($set)
+ // ->setHost($host)
+ ->setAffectedHost($host);
+ foreach ($this->fetchServicesForTable($table) as $service) {
+ $services[] = $service;
+ }
+ }
+
+ $table = IcingaHostAppliedServicesTable::load($host);
+ foreach ($this->fetchServicesForTable($table) as $service) {
+ $services[] = $service;
+ }
+
+ return $services;
+ }
+
+ /**
+ * Duplicates Logic in HostController
+ *
+ * @param IcingaHost $host
+ * @param IcingaHost|null $affectedHost
+ * @return IcingaServiceSetServiceTable[]
+ */
+ protected function getHostServiceSetTables(IcingaHost $host, IcingaHost $affectedHost = null)
+ {
+ $tables = [];
+ $db = $this->connection;
+ if ($affectedHost === null) {
+ $affectedHost = $host;
+ }
+ if ($host->get('id') === null) {
+ return $tables;
+ }
+
+ $query = $db->getDbAdapter()->select()
+ ->from(['ss' => 'icinga_service_set'], 'ss.*')
+ ->join(['hsi' => 'icinga_service_set_inheritance'], 'hsi.parent_service_set_id = ss.id', [])
+ ->join(['hs' => 'icinga_service_set'], 'hs.id = hsi.service_set_id', [])
+ ->where('hs.host_id = ?', $host->get('id'));
+
+ $sets = IcingaServiceSet::loadAll($db, $query, 'object_name');
+ /** @var IcingaServiceSet $set*/
+ foreach ($sets as $name => $set) {
+ $tables[] = IcingaServiceSetServiceTable::load($set)
+ ->setHost($host)
+ ->setAffectedHost($affectedHost);
+ }
+
+ return $tables;
+ }
+
+ protected function fetchServicesForTable(QueryBasedTable $table)
+ {
+ $query = $table->getQuery();
+ if ($query instanceof Select || $query instanceof Zend_Db_Select) {
+ // What about SimpleQuery? IcingaHostAppliedServicesTable with branch in place?
+ $query->reset(Select::LIMIT_COUNT);
+ $query->reset(Select::LIMIT_OFFSET);
+ $rows = $this->db->fetchAll($query);
+ } elseif ($query instanceof SimpleQuery) {
+ $rows = $query->fetchAll();
+ } else {
+ throw new RuntimeException('Table query needs to be either a Select or a SimpleQuery instance');
+ }
+ $services = [];
+ foreach ($rows as $row) {
+ $service = IcingaService::loadWithUniqueId(Uuid::fromBytes($row->uuid), $this->connection);
+ if ($this->resolveObjects) {
+ $service = $service::fromPlainObject($service->toPlainObject(true), $this->connection);
+ }
+ $services[] = $service;
+ }
+
+ return $services;
+ }
+}
diff --git a/library/Director/Data/ImportExportDeniedProperties.php b/library/Director/Data/ImportExportDeniedProperties.php
new file mode 100644
index 0000000..747eb0f
--- /dev/null
+++ b/library/Director/Data/ImportExportDeniedProperties.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Objects\ImportRowModifier;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\SyncRule;
+
+class ImportExportDeniedProperties
+{
+ protected static $denyProperties = [
+ DirectorJob::class => [
+ 'last_attempt_succeeded',
+ 'last_error_message',
+ 'ts_last_attempt',
+ 'ts_last_error',
+ ],
+ ImportSource::class => [
+ // No state export
+ 'import_state',
+ 'last_error_message',
+ 'last_attempt',
+ ],
+ ImportRowModifier::class => [
+ // Not state, but to be removed:
+ 'source_id',
+ ],
+ SyncRule::class => [
+ 'sync_state',
+ 'last_error_message',
+ 'last_attempt',
+ ],
+ ];
+
+ public static function strip(array &$props, DbObject $object, $showIds = false)
+ {
+ // TODO: this used to exist. Double-check all imports to verify it's not in use
+ // $originalId = $props['id'];
+
+ if (! $showIds) {
+ unset($props['id']);
+ }
+ $class = get_class($object);
+ if (isset(self::$denyProperties[$class])) {
+ foreach (self::$denyProperties[$class] as $key) {
+ unset($props[$key]);
+ }
+ }
+ }
+}
diff --git a/library/Director/Data/InvalidDataException.php b/library/Director/Data/InvalidDataException.php
new file mode 100644
index 0000000..9abaf7c
--- /dev/null
+++ b/library/Director/Data/InvalidDataException.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use InvalidArgumentException;
+
+class InvalidDataException extends InvalidArgumentException
+{
+ /**
+ * @param string $expected
+ * @param mixed $value
+ */
+ public function __construct($expected, $value)
+ {
+ parent::__construct("$expected expected, got " . static::getPhpType($value));
+ }
+
+ public static function getPhpType($var)
+ {
+ if (is_object($var)) {
+ return get_class($var);
+ }
+
+ return gettype($var);
+ }
+}
diff --git a/library/Director/Data/Json.php b/library/Director/Data/Json.php
new file mode 100644
index 0000000..78b3e67
--- /dev/null
+++ b/library/Director/Data/Json.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use Icinga\Module\Director\Exception\JsonEncodeException;
+use function json_decode;
+use function json_encode;
+use function json_last_error;
+
+class Json
+{
+ const DEFAULT_FLAGS = JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
+
+ /**
+ * Encode with well-known flags, as we require the result to be reproducible
+ *
+ * @param $mixed
+ * @param int|null $flags
+ * @return string
+ * @throws JsonEncodeException
+ */
+ public static function encode($mixed, $flags = null)
+ {
+ if ($flags === null) {
+ $flags = self::DEFAULT_FLAGS;
+ } else {
+ $flags = self::DEFAULT_FLAGS | $flags;
+ }
+ $result = json_encode($mixed, $flags);
+
+ if ($result === false && json_last_error() !== JSON_ERROR_NONE) {
+ throw JsonEncodeException::forLastJsonError();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Decode the given JSON string and make sure we get a meaningful Exception
+ *
+ * @param string $string
+ * @return mixed
+ * @throws JsonEncodeException
+ */
+ public static function decode($string)
+ {
+ $result = json_decode($string);
+
+ if ($result === null && json_last_error() !== JSON_ERROR_NONE) {
+ throw JsonEncodeException::forLastJsonError();
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param $string
+ * @return ?string
+ * @throws JsonEncodeException
+ */
+ public static function decodeOptional($string)
+ {
+ if ($string === null) {
+ return null;
+ }
+
+ return static::decode($string);
+ }
+}
diff --git a/library/Director/Data/PropertiesFilter.php b/library/Director/Data/PropertiesFilter.php
new file mode 100644
index 0000000..a8c3906
--- /dev/null
+++ b/library/Director/Data/PropertiesFilter.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+class PropertiesFilter
+{
+ public static $CUSTOM_PROPERTY = 'CUSTOM_PROPERTY';
+ public static $HOST_PROPERTY = 'HOST_PROPERTY';
+ public static $SERVICE_PROPERTY = 'SERVICE_PROPERTY';
+
+ protected $blacklist = array(
+ 'id',
+ 'object_name',
+ 'object_type',
+ 'disabled',
+ 'has_agent',
+ 'master_should_connect',
+ 'accept_config',
+ );
+
+ public function match($type, $name, $object = null)
+ {
+ return ($type != self::$HOST_PROPERTY || !in_array($name, $this->blacklist));
+ }
+}
diff --git a/library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php b/library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php
new file mode 100644
index 0000000..ef9f2d4
--- /dev/null
+++ b/library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Director\Data\PropertiesFilter;
+
+class ArrayCustomVariablesFilter extends CustomVariablesFilter
+{
+ public function match($type, $name, $object = null)
+ {
+ return parent::match($type, $name, $object)
+ && $object !== null
+ && isset($object->datatype)
+ && (
+ preg_match('/DataTypeArray[\w]*$/', $object->datatype)
+ || (
+ preg_match('/DataTypeDatalist$/', $object->datatype)
+ && $object->format === 'json'
+ )
+ );
+ }
+}
diff --git a/library/Director/Data/PropertiesFilter/CustomVariablesFilter.php b/library/Director/Data/PropertiesFilter/CustomVariablesFilter.php
new file mode 100644
index 0000000..91ef9cd
--- /dev/null
+++ b/library/Director/Data/PropertiesFilter/CustomVariablesFilter.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Icinga\Module\Director\Data\PropertiesFilter;
+
+use Icinga\Module\Director\Data\PropertiesFilter;
+
+class CustomVariablesFilter extends PropertiesFilter
+{
+ public function match($type, $name, $object = null)
+ {
+ return parent::match($type, $name, $object) && $type === self::$CUSTOM_PROPERTY;
+ }
+}
diff --git a/library/Director/Data/PropertyMangler.php b/library/Director/Data/PropertyMangler.php
new file mode 100644
index 0000000..a457f1d
--- /dev/null
+++ b/library/Director/Data/PropertyMangler.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use InvalidArgumentException;
+
+class PropertyMangler
+{
+ public static function appendToArrayProperties(IcingaObject $object, $properties)
+ {
+ foreach ($properties as $key => $value) {
+ $current = $object->$key;
+ if ($current === null) {
+ $current = [$value];
+ } elseif (is_array($current)) {
+ $current[] = $value;
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'I can only append to arrays, %s is %s',
+ $key,
+ var_export($current, 1)
+ ));
+ }
+
+ $object->$key = $current;
+ }
+ }
+
+ public static function removeProperties(IcingaObject $object, $properties)
+ {
+ foreach ($properties as $key => $value) {
+ if ($value === true) {
+ $object->$key = null;
+ }
+ $current = $object->$key;
+ if ($current === null) {
+ continue;
+ } elseif (is_array($current)) {
+ $new = [];
+ foreach ($current as $item) {
+ if ($item !== $value) {
+ $new[] = $item;
+ }
+ }
+ $object->$key = $new;
+ } elseif (is_string($current)) {
+ if ($current === $value) {
+ $object->$key = null;
+ }
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'I can only remove strings or from arrays, %s is %s',
+ $key,
+ var_export($current, 1)
+ ));
+ }
+ }
+ }
+}
diff --git a/library/Director/Data/RecursiveUtf8Validator.php b/library/Director/Data/RecursiveUtf8Validator.php
new file mode 100644
index 0000000..cadfc21
--- /dev/null
+++ b/library/Director/Data/RecursiveUtf8Validator.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use InvalidArgumentException;
+use ipl\Html\Error;
+
+class RecursiveUtf8Validator
+{
+ protected static $rowNum;
+
+ protected static $column;
+
+ /**
+ * @param array $rows Usually array of stdClass
+ * @return bool
+ */
+ public static function validateRows($rows)
+ {
+ foreach ($rows as self::$rowNum => $row) {
+ foreach ($row as self::$column => $value) {
+ static::assertUtf8($value);
+ }
+ }
+
+ return true;
+ }
+
+ protected static function assertUtf8($value)
+ {
+ if (\is_string($value)) {
+ static::assertUtf8String($value);
+ } elseif (\is_array($value) || $value instanceof \stdClass) {
+ foreach ((array) $value as $k => $v) {
+ static::assertUtf8($k);
+ static::assertUtf8($v);
+ }
+ } elseif ($value !== null && !\is_scalar($value)) {
+ throw new InvalidArgumentException("Cannot validate " . Error::getPhpTypeName($value));
+ }
+ }
+
+ protected static function assertUtf8String($string)
+ {
+ if (@\iconv('UTF-8', 'UTF-8', $string) != $string) {
+ $row = self::$rowNum;
+ if (is_int($row)) {
+ $row++;
+ }
+ throw new InvalidArgumentException(\sprintf(
+ 'Invalid UTF-8 on row %s, column %s: "%s" (%s)',
+ $row,
+ self::$column,
+ \iconv('UTF-8', 'UTF-8//IGNORE', $string),
+ '0x' . \bin2hex($string)
+ ));
+ }
+ }
+}
diff --git a/library/Director/Data/Serializable.php b/library/Director/Data/Serializable.php
new file mode 100644
index 0000000..9f8cb63
--- /dev/null
+++ b/library/Director/Data/Serializable.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use JsonSerializable;
+
+interface Serializable extends JsonSerializable
+{
+ public static function fromSerialization($value);
+}
diff --git a/library/Director/Data/SerializableValue.php b/library/Director/Data/SerializableValue.php
new file mode 100644
index 0000000..5784224
--- /dev/null
+++ b/library/Director/Data/SerializableValue.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use InvalidArgumentException;
+use JsonSerializable;
+use stdClass;
+use function get_class;
+use function gettype;
+use function is_array;
+use function is_object;
+use function is_scalar;
+
+class SerializableValue implements Serializable
+{
+ protected $value = [];
+
+ /**
+ * @param stdClass|array $object
+ * @return static
+ */
+ public static function fromSerialization($value)
+ {
+ $self = new static;
+ static::assertSerializableValue($value);
+ $self->value = $value;
+
+ return $self;
+ }
+
+ public static function wantSerializable($value)
+ {
+ if ($value instanceof SerializableValue) {
+ return $value;
+ }
+
+ return static::fromSerialization($value);
+ }
+
+ /**
+ * TODO: Check whether json_encode() is faster
+ *
+ * @param mixed $value
+ * @return bool
+ */
+ protected static function assertSerializableValue($value)
+ {
+ if ($value === null || is_scalar($value)) {
+ return true;
+ }
+ if (is_object($value)) {
+ if ($value instanceof JsonSerializable) {
+ return true;
+ }
+
+ if ($value instanceof stdClass) {
+ foreach ((array) $value as $val) {
+ static::assertSerializableValue($val);
+ }
+
+ return true;
+ }
+ }
+
+ if (is_array($value)) {
+ foreach ($value as $val) {
+ static::assertSerializableValue($val);
+ }
+
+ return true;
+ }
+
+ throw new InvalidArgumentException('Serializable value expected, got ' . static::getPhpType($value));
+ }
+
+ protected static function getPhpType($var)
+ {
+ if (is_object($var)) {
+ return get_class($var);
+ }
+
+ return gettype($var);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ return $this->value;
+ }
+}
diff --git a/library/Director/Data/ValueFilter.php b/library/Director/Data/ValueFilter.php
new file mode 100644
index 0000000..926214f
--- /dev/null
+++ b/library/Director/Data/ValueFilter.php
@@ -0,0 +1,10 @@
+<?php
+
+// TODO: move elsewhere, this is for forms
+namespace Icinga\Module\Director\Data;
+
+use Zend_Filter_Interface;
+
+interface ValueFilter extends Zend_Filter_Interface
+{
+}
diff --git a/library/Director/Data/ValueFilter/FilterBoolean.php b/library/Director/Data/ValueFilter/FilterBoolean.php
new file mode 100644
index 0000000..1fadec3
--- /dev/null
+++ b/library/Director/Data/ValueFilter/FilterBoolean.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\Data\ValueFilter;
+
+use Icinga\Module\Director\Data\ValueFilter;
+
+class FilterBoolean implements ValueFilter
+{
+ public function filter($value)
+ {
+ if ($value === 'y' || $value === true) {
+ return true;
+ } elseif ($value === 'n' || $value === false) {
+ return false;
+ }
+
+ return null;
+ }
+}
diff --git a/library/Director/Data/ValueFilter/FilterInt.php b/library/Director/Data/ValueFilter/FilterInt.php
new file mode 100644
index 0000000..d51ce8d
--- /dev/null
+++ b/library/Director/Data/ValueFilter/FilterInt.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Icinga\Module\Director\Data\ValueFilter;
+
+use Icinga\Module\Director\Data\ValueFilter;
+
+class FilterInt implements ValueFilter
+{
+ public function filter($value)
+ {
+ if ($value === '' || $value === null) {
+ return null;
+ }
+
+ if (is_string($value) && ! ctype_digit($value)) {
+ return $value;
+ }
+
+ return (int) ((string) $value);
+ }
+}