diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index f077dfe02..206d3c88b 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -53,7 +53,7 @@ class ConfigController extends Controller )); $tabs->add('usergroupbackend', array( 'title' => $this->translate('Configure how users are associated with groups by Icinga Web 2'), - 'label' => $this->translate('Usergroup Backends'), + 'label' => $this->translate('User Group Backends'), 'url' => 'usergroupbackend/list' )); return $tabs; diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php index 477c1b28d..961c72132 100644 --- a/application/controllers/UsergroupbackendController.php +++ b/application/controllers/UsergroupbackendController.php @@ -160,7 +160,7 @@ class UsergroupbackendController extends Controller )); $tabs->add('usergroupbackend', array( 'title' => $this->translate('Configure how users are associated with groups by Icinga Web 2'), - 'label' => $this->translate('Usergroup Backends'), + 'label' => $this->translate('User Group Backends'), 'url' => 'usergroupbackend/list' )); return $tabs; diff --git a/application/forms/Config/Resource/DbResourceForm.php b/application/forms/Config/Resource/DbResourceForm.php index 0ee64eda3..75d935361 100644 --- a/application/forms/Config/Resource/DbResourceForm.php +++ b/application/forms/Config/Resource/DbResourceForm.php @@ -129,16 +129,13 @@ class DbResourceForm extends Form */ public static function isValidResource(Form $form) { - try { - $resource = ResourceFactory::createResource(new ConfigObject($form->getValues())); - $resource->getConnection()->getConnection(); - } catch (Exception $e) { - $form->addError( - $form->translate('Connectivity validation failed, connection to the given resource not possible.') - ); - return false; + $result = ResourceFactory::createResource(new ConfigObject($form->getValues()))->inspect(); + if ($result->hasError()) { + $form->addError(sprintf($form->translate('Connectivity validation failed: %s'), $result->getError())); } - return true; + // TODO: display diagnostics in $result->toArray() to the user + + return ! $result->hasError(); } } diff --git a/application/forms/Config/Resource/LdapResourceForm.php b/application/forms/Config/Resource/LdapResourceForm.php index 604e34199..b385610a8 100644 --- a/application/forms/Config/Resource/LdapResourceForm.php +++ b/application/forms/Config/Resource/LdapResourceForm.php @@ -154,19 +154,17 @@ class LdapResourceForm extends Form */ public static function isValidResource(Form $form) { - try { - $resource = ResourceFactory::createResource(new ConfigObject($form->getValues())); - $resource->bind(); - } catch (Exception $e) { - $msg = $form->translate('Connectivity validation failed, connection to the given resource not possible.'); - if (($error = $e->getMessage())) { - $msg .= ' (' . $error . ')'; - } - - $form->addError($msg); - return false; + $result = ResourceFactory::createResource(new ConfigObject($form->getValues()))->inspect(); + if ($result->hasError()) { + $form->addError(sprintf( + '%s (%s)', + $form->translate('Connectivity validation failed, connection to the given resource not possible.'), + $result->getError() + )); } - return true; + // TODO: display diagnostics in $result->toArray() to the user + + return ! $result->hasError(); } } diff --git a/application/forms/Config/UserBackend/DbBackendForm.php b/application/forms/Config/UserBackend/DbBackendForm.php index f096f3e35..c09ec166c 100644 --- a/application/forms/Config/UserBackend/DbBackendForm.php +++ b/application/forms/Config/UserBackend/DbBackendForm.php @@ -105,18 +105,15 @@ class DbBackendForm extends Form */ public static function isValidUserBackend(Form $form) { - try { - $dbUserBackend = new DbUserBackend(ResourceFactory::createResource($form->getResourceConfig())); - if ($dbUserBackend->select()->where('is_active', true)->count() < 1) { - $form->addError($form->translate('No active users found under the specified database backend')); - return false; - } - } catch (Exception $e) { - $form->addError(sprintf($form->translate('Using the specified backend failed: %s'), $e->getMessage())); - return false; + $backend = new DbUserBackend(ResourceFactory::createResource($form->getResourceConfig())); + $result = $backend->inspect(); + if ($result->hasError()) { + $form->addError(sprintf($form->translate('Using the specified backend failed: %s'), $result->getError())); } - return true; + // TODO: display diagnostics in $result->toArray() to the user + + return ! $result->hasError(); } /** diff --git a/application/forms/Config/UserBackend/LdapBackendForm.php b/application/forms/Config/UserBackend/LdapBackendForm.php index 9151134c3..6427fbf34 100644 --- a/application/forms/Config/UserBackend/LdapBackendForm.php +++ b/application/forms/Config/UserBackend/LdapBackendForm.php @@ -4,6 +4,8 @@ namespace Icinga\Forms\Config\UserBackend; use Exception; +use Icinga\Authentication\User\LdapUserBackend; +use Icinga\Data\Inspection; use Icinga\Web\Form; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; @@ -184,22 +186,16 @@ class LdapBackendForm extends Form */ public static function isValidUserBackend(Form $form) { - try { - $ldapUserBackend = UserBackend::create(null, new ConfigObject($form->getValues())); - $ldapUserBackend->assertAuthenticationPossible(); - } catch (AuthenticationException $e) { - if (($previous = $e->getPrevious()) !== null) { - $form->addError($previous->getMessage()); - } else { - $form->addError($e->getMessage()); - } - - return false; - } catch (Exception $e) { - $form->addError(sprintf($form->translate('Unable to validate authentication: %s'), $e->getMessage())); - return false; + /** + * @var $result Inspection + */ + $result = UserBackend::create(null, new ConfigObject($form->getValues()))->inspect(); + if ($result->hasError()) { + $form->addError($result->getError()); } - return true; + // TODO: display diagnostics in $result->toArray() to the user + + return ! $result->hasError(); } } diff --git a/icingaweb2.spec b/icingaweb2.spec index 7cddee0d3..e3513e979 100644 --- a/icingaweb2.spec +++ b/icingaweb2.spec @@ -46,8 +46,6 @@ Requires: %{name}-vendor-HTMLPurifier Requires: %{name}-vendor-JShrink Requires: %{name}-vendor-lessphp Requires: %{name}-vendor-Parsedown -Requires: %{zend} -Obsoletes: %{name}-vendor-zend %description @@ -84,6 +82,10 @@ Requires: %{php}-gd %{php}-intl %{?fedora:Requires: php-pecl-imagick} %{?rhel:Requires: php-pecl-imagick} %{?suse_version:Requires: %{php}-gettext %{php}-json %{php}-openssl %{php}-posix} +Requires: %{zend} +Obsoletes: %{name}-vendor-zend +Requires: %{zend}-Db-Adapter-Pdo-Mysql +Requires: %{zend}-Db-Adapter-Pdo-Pgsql %description -n php-Icinga Icinga Web 2 PHP library diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php index 0e5dad5fa..3c28a2fdd 100644 --- a/library/Icinga/Authentication/User/DbUserBackend.php +++ b/library/Icinga/Authentication/User/DbUserBackend.php @@ -4,13 +4,15 @@ namespace Icinga\Authentication\User; use Exception; +use Icinga\Data\Inspectable; +use Icinga\Data\Inspection; use PDO; use Icinga\Data\Filter\Filter; use Icinga\Exception\AuthenticationException; use Icinga\Repository\DbRepository; use Icinga\User; -class DbUserBackend extends DbRepository implements UserBackendInterface +class DbUserBackend extends DbRepository implements UserBackendInterface, Inspectable { /** * The algorithm to use when hashing passwords @@ -246,4 +248,26 @@ class DbUserBackend extends DbRepository implements UserBackendInterface { return crypt($password, self::HASH_ALGORITHM . ($salt !== null ? $salt : $this->generateSalt())); } + + /** + * Inspect this object to gain extended information about its health + * + * @return Inspection The inspection result + */ + public function inspect() + { + $insp = new Inspection('Db User Backend'); + $insp->write($this->ds->inspect()); + try { + $users = $this->select()->where('is_active', true)->count(); + if ($users > 1) { + $insp->write(sprintf('%s active users', $users)); + } else { + return $insp->error('0 active users', $users); + } + } catch (Exception $e) { + $insp->error(sprintf('Query failed: %s', $e->getMessage())); + } + return $insp; + } } diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index c6efd0673..df2c0a8e0 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -5,6 +5,8 @@ namespace Icinga\Authentication\User; use DateTime; use Icinga\Data\ConfigObject; +use Icinga\Data\Inspectable; +use Icinga\Data\Inspection; use Icinga\Exception\AuthenticationException; use Icinga\Exception\ProgrammingError; use Icinga\Repository\LdapRepository; @@ -13,7 +15,7 @@ use Icinga\Protocol\Ldap\LdapException; use Icinga\Protocol\Ldap\Expression; use Icinga\User; -class LdapUserBackend extends LdapRepository implements UserBackendInterface +class LdapUserBackend extends LdapRepository implements UserBackendInterface, Inspectable { /** * The base DN to use for a query @@ -306,41 +308,14 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface } /** - * Probe the backend to test if authentication is possible - * - * Try to bind to the backend and fetch a single user to check if: - * + + * @param Inspection $info Optional inspection to fill with diagnostic info * * @throws AuthenticationException When authentication is not possible */ - public function assertAuthenticationPossible() + public function assertAuthenticationPossible(Inspection $insp = null) { - try { - $result = $this->select()->fetchRow(); - } catch (LdapException $e) { - throw new AuthenticationException('Connection not possible.', $e); - } - if ($result === false) { - throw new AuthenticationException( - 'No objects with objectClass "%s" in DN "%s" found. (Filter: %s)', - $this->userClass, - $this->baseDn ?: $this->ds->getDn(), - $this->filter ?: 'None' - ); - } - - if (! isset($result->user_name)) { - throw new AuthenticationException( - 'UserNameAttribute "%s" not existing in objectClass "%s"', - $this->userNameAttribute, - $this->userClass - ); - } } /** @@ -377,4 +352,58 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface ); } } + + /** + * Inspect if this LDAP User Backend is working as expected by probing the backend + * and testing if thea uthentication is possible + * + * Try to bind to the backend and fetch a single user to check if: + * + * + * @return Inspection Inspection result + */ + public function inspect() + { + $result = new Inspection('Ldap User Backend'); + + // inspect the used connection to get more diagnostic info in case the connection is not working + $result->write($this->ds->inspect()); + try { + try { + $res = $this->select()->fetchRow(); + } catch (LdapException $e) { + throw new AuthenticationException('Connection not possible', $e); + } + $result->write('Searching for: ' . sprintf( + 'objectClass "%s" in DN "%s" (Filter: %s)', + $this->userClass, + $this->baseDn ?: $this->ds->getDn(), + $this->filter ?: 'None' + )); + if ($res === false) { + throw new AuthenticationException('Error, no users found in backend'); + } + $result->write(sprintf('%d users found in backend', $this->select()->count())); + if (! isset($res->user_name)) { + throw new AuthenticationException( + 'UserNameAttribute "%s" not existing in objectClass "%s"', + $this->userNameAttribute, + $this->userClass + ); + } + } catch (AuthenticationException $e) { + if (($previous = $e->getPrevious()) !== null) { + $result->error($previous->getMessage()); + } else { + $result->error($e->getMessage()); + } + } catch (Exception $e) { + $result->error(sprintf('Unable to validate authentication: %s', $e->getMessage())); + } + return $result; + } } diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index 95f3e9235..ba7a3c6f0 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -3,6 +3,9 @@ namespace Icinga\Data\Db; +use Exception; +use Icinga\Data\Inspectable; +use Icinga\Data\Inspection; use PDO; use Iterator; use Zend_Db; @@ -23,7 +26,7 @@ use Icinga\Exception\ProgrammingError; /** * Encapsulate database connections and query creation */ -class DbConnection implements Selectable, Extensible, Updatable, Reducible +class DbConnection implements Selectable, Extensible, Updatable, Reducible, Inspectable { /** * Connection config @@ -435,4 +438,42 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible return $column . ' ' . $sign . ' ' . $this->dbAdapter->quote($value); } } + + public function inspect() + { + $insp = new Inspection('Db Connection'); + try { + $this->getDbAdapter()->getConnection(); + $config = $this->dbAdapter->getConfig(); + $insp->write(sprintf( + 'Connection to %s as %s on %s:%s successful', + $config['dbname'], + $config['username'], + $config['host'], + $config['port'] + )); + switch ($this->dbType) { + case 'mysql': + $rows = $this->dbAdapter->query( + 'SHOW VARIABLES WHERE variable_name ' . + 'IN (\'version\', \'protocol_version\', \'version_compile_os\');' + )->fetchAll(); + $sqlinsp = new Inspection('MySQL'); + foreach ($rows as $row) { + $sqlinsp->write($row->variable_name . ': ' . $row->value); + } + $insp->write($sqlinsp); + break; + case 'pgsql': + $row = $this->dbAdapter->query('SELECT version();')->fetchAll(); + $sqlinsp = new Inspection('PostgreSQL'); + $sqlinsp->write($row[0]->version); + $insp->write($sqlinsp); + break; + } + } catch (Exception $e) { + return $insp->error(sprintf('Connection failed %s', $e->getMessage())); + } + return $insp; + } } diff --git a/library/Icinga/Data/Inspectable.php b/library/Icinga/Data/Inspectable.php index 1e037113a..8b37d9503 100644 --- a/library/Icinga/Data/Inspectable.php +++ b/library/Icinga/Data/Inspectable.php @@ -3,24 +3,18 @@ namespace Icinga\Data; - /** * An object for which the user can retrieve status information + * + * This interface is useful for providing summaries or diagnostic information about objects + * to users. */ interface Inspectable { /** - * Get information about this objects state + * Inspect this object to gain extended information about its health * - * @return array An array of strings that describe the state in a human-readable form, each array element - * represents one fact about this object + * @return Inspection The inspection result */ - public function getInfo(); - - /** - * If this object is working in its current configuration - * - * @return Bool True if the object is working, false if not - */ - public function isHealthy(); + public function inspect(); } diff --git a/library/Icinga/Data/Inspection.php b/library/Icinga/Data/Inspection.php new file mode 100644 index 000000000..eea91617e --- /dev/null +++ b/library/Icinga/Data/Inspection.php @@ -0,0 +1,127 @@ +description = $description; + } + + /** + * Get the name of this Inspection + * + * @return mixed + */ + public function getDescription() + { + return $this->description; + } + + /** + * Append the given log entry or nested inspection + * + * @throws ProgrammingError When called after erroring + * + * @param $entry string|Inspection A log entry or nested inspection + */ + public function write($entry) + { + if (isset($this->error)) { + throw new ProgrammingError('Inspection object used after error'); + } + if ($entry instanceof Inspection) { + $this->log[$entry->description] = $entry->toArray(); + } else { + $this->log[] = $entry; + } + } + + /** + * Append the given log entry and fail this inspection with the given error + * + * @param $entry string|Inspection A log entry or nested inspection + * + * @throws ProgrammingError When called multiple times + * + * @return this fluent interface + */ + public function error($entry) + { + if (isset($this->error)) { + throw new ProgrammingError('Inspection object used after error'); + } + $this->write($entry); + $this->error = $entry; + return $this; + } + + /** + * If the inspection resulted in an error + * + * @return bool + */ + public function hasError() + { + return isset($this->error); + } + + /** + * The error that caused the inspection to fail + * + * @return Inspection|string + */ + public function getError() + { + return $this->error; + } + + /** + * Convert the inspection to an array + * + * @return array An array of strings that describe the state in a human-readable form, each array element + * represents one log entry about this object. + */ + public function toArray() + { + return $this->log; + } + + /** + * Return a text representation of the inspection log entries + */ + public function __toString() + { + return sprintf( + 'Inspection: description: "%s" error: "%s"', + $this->description, + $this->error + ); + } +} diff --git a/library/Icinga/Protocol/Ldap/LdapCapabilities.php b/library/Icinga/Protocol/Ldap/LdapCapabilities.php index e98c57818..1ee64eaba 100644 --- a/library/Icinga/Protocol/Ldap/LdapCapabilities.php +++ b/library/Icinga/Protocol/Ldap/LdapCapabilities.php @@ -263,14 +263,15 @@ class LdapCapabilities * Discover the capabilities of the given LDAP server * * @param LdapConnection $connection The ldap connection to use - * @param int $ds The link identifier of the current LDAP connection * * @return LdapCapabilities * * @throws LdapException In case the capability query has failed */ - public static function discoverCapabilities(LdapConnection $connection, $ds) + public static function discoverCapabilities(LdapConnection $connection) { + $ds = $connection->getConnection(); + $fields = array( 'defaultNamingContext', 'namingContexts', diff --git a/library/Icinga/Protocol/Ldap/LdapConnection.php b/library/Icinga/Protocol/Ldap/LdapConnection.php index 099c25006..38440e029 100644 --- a/library/Icinga/Protocol/Ldap/LdapConnection.php +++ b/library/Icinga/Protocol/Ldap/LdapConnection.php @@ -10,8 +10,10 @@ use Icinga\Application\Logger; use Icinga\Application\Platform; use Icinga\Data\ConfigObject; use Icinga\Data\Inspectable; +use Icinga\Data\Inspection; use Icinga\Data\Selectable; use Icinga\Data\Sortable; +use Icinga\Exception\InspectionException; use Icinga\Exception\ProgrammingError; use Icinga\Protocol\Ldap\LdapException; @@ -162,16 +164,6 @@ class LdapConnection implements Selectable, Inspectable */ protected $encrypted = null; - /** - * @var array - */ - protected $info = null; - - /** - * @var Boolean - */ - protected $healthy = null; - /** * Create a new connection object * @@ -261,7 +253,7 @@ class LdapConnection implements Selectable, Inspectable { if ($this->capabilities === null) { try { - $this->capabilities = $this->discoverCapabilities($this->getConnection()); + $this->capabilities = LdapCapabilities::discoverCapabilities($this); $this->discoverySuccess = true; } catch (LdapException $e) { Logger::debug($e); @@ -711,8 +703,7 @@ class LdapConnection implements Selectable, Inspectable array_flip($fields) ); } - } while ( - (! $serverSorting || $limit === 0 || $limit !== count($entries)) + } while ((! $serverSorting || $limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($ds, $entry)) ); @@ -953,21 +944,27 @@ class LdapConnection implements Selectable, Inspectable /** * Prepare and establish a connection with the LDAP server * - * @return resource A LDAP link identifier + * @param Inspection $info Optional inspection to fill with diagnostic info * - * @throws LdapException In case the connection is not possible + * @return resource A LDAP link identifier + * + * @throws LdapException In case the connection is not possible */ - protected function prepareNewConnection() + protected function prepareNewConnection(Inspection $info = null) { + if (! isset($info)) { + $info = new Inspection(''); + } + if ($this->encryption === static::STARTTLS || $this->encryption === static::LDAPS) { $this->prepareTlsEnvironment(); } $hostname = $this->hostname; if ($this->encryption === static::LDAPS) { - $this->logInfo('Connect using LDAPS'); + $info->write('Connect using LDAPS'); if (! $this->validateCertificate) { - $this->logInfo('Skipping certificate validation'); + $info->write('Skip certificate validation'); } $hostname = 'ldaps://' . $hostname; } @@ -984,9 +981,9 @@ class LdapConnection implements Selectable, Inspectable if ($this->encryption === static::STARTTLS) { $this->encrypted = true; - $this->logInfo('Connect using STARTTLS'); + $info->write('Connect using STARTTLS'); if (! $this->validateCertificate) { - $this->logInfo('Skipping certificate validation'); + $info->write('Skip certificate validation'); } if (! ldap_start_tls($ds)) { throw new LdapException('LDAP STARTTLS failed: %s', ldap_error($ds)); @@ -994,60 +991,12 @@ class LdapConnection implements Selectable, Inspectable } elseif ($this->encryption !== static::LDAPS) { $this->encrypted = false; - $this->logInfo('Connect without encryption'); + $info->write('Connect without encryption'); } return $ds; } - /** - * Test if needed aspects of the LDAP connection are working as expected - * - * Extended information about the - * - * @throws \Icinga\Protocol\Ldap\LdapException When a critical aspect of the health test fails - */ - public function testConnectionHealth() - { - $this->healthy = false; - $this->info = array(); - - // Try to connect to the server with the given connection parameters - $ds = $this->prepareNewConnection(); - - // Try a bind-command with the given user credentials, this must not fail - $success = @ldap_bind($ds, $this->bindDn, $this->bindPw); - $msg = sprintf( - 'LDAP bind to %s:%s (%s / %s)', - $this->hostname, - $this->port, - $this->bindDn, - '***' /* $this->bindPw */ - ); - if (! $success) { - throw new LdapException('%s failed: %s', $msg, ldap_error($ds)); - } - $this->logInfo(sprintf($msg . ' successful')); - - // Try to execute a schema discovery, this may fail if schema discovery is not supported - try { - $cap = LdapCapabilities::discoverCapabilities($this, $ds); - $infos []= $cap->getVendor(); - - $version = $cap->getVersion(); - if (isset($version)) { - $infos []= $version; - } - $infos []= 'Supports STARTTLS: ' . ($cap->hasStartTls() ? 'True' : 'False'); - $infos []= 'Default naming context: ' . $cap->getDefaultNamingContext(); - $this->info['Discovery Results:'] = $infos; - } catch (Exception $e) { - $this->logInfo('Schema discovery not possible: ', $e->getMessage()); - } - - $this->healthy = true; - } - /** * Set up how to handle StartTLS connections * @@ -1137,43 +1086,55 @@ class LdapConnection implements Selectable, Inspectable return $dir; } - protected function logInfo($message) - { - Logger::debug($message); - if (! isset($this->info)) { - $this->info = array(); - } - $this->info[] = $message; - } - /** - * Get information about this objects state + * Inspect if this LDAP Connection is working as expected * - * @return array An array of strings that describe the state in a human-readable form, each array element - * represents one fact about this object + * Check if connection, bind and encryption is working as expected and get additional + * information about the used + * + * @return Inspection Inspection result */ - public function getInfo() + public function inspect() { - if (! isset($this->info)) { - $this->testConnectionHealth(); - } - return $this->info; - } + $insp = new Inspection('Ldap Connection'); - /** - * If this object is working in its current configuration - * - * @return Bool True if the object is working, false if not - */ - public function isHealthy() - { - if (! isset($this->healthy)) { - try { - $this->testConnectionHealth(); - } catch (Exception $e) { + // Try to connect to the server with the given connection parameters + try { + $ds = $this->prepareNewConnection($insp); + } catch (Exception $e) { + return $insp->error($e->getMessage()); + } + + // Try a bind-command with the given user credentials, this must not fail + $success = @ldap_bind($ds, $this->bindDn, $this->bindPw); + $msg = sprintf( + 'LDAP bind to %s:%s (%s / %s)', + $this->hostname, + $this->port, + $this->bindDn, + '***' /* $this->bindPw */ + ); + if (! $success) { + return $insp->error(sprintf('%s failed: %s', $msg, ldap_error($ds))); + } + $insp->write(sprintf($msg . ' successful')); + + // Try to execute a schema discovery this may fail if schema discovery is not supported + try { + $cap = LdapCapabilities::discoverCapabilities($this); + $discovery = new Inspection('Discovery Results'); + $discovery->write($cap->getVendor()); + $version = $cap->getVersion(); + if (isset($version)) { + $discovery->write($version); } + $discovery->write('Supports STARTTLS: ' . ($cap->hasStartTls() ? 'True' : 'False')); + $discovery->write('Default naming context: ' . $cap->getDefaultNamingContext()); + $insp->write($discovery); + } catch (Exception $e) { + $insp->write('Schema discovery not possible: ' . $e->getMessage()); } - return $this->healthy; + return $insp; } /** diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php index 39cc5ecbb..dc3eaf1cc 100644 --- a/library/Icinga/Web/Form.php +++ b/library/Icinga/Web/Form.php @@ -160,6 +160,13 @@ class Form extends Zend_Form */ protected $notifications; + /** + * The hints of this form + * + * @var array + */ + protected $hints; + /** * Whether the Autosubmit decorator should be applied to this form * @@ -574,6 +581,49 @@ class Form extends Zend_Form return $this->notifications; } + /** + * Set the hints for this form + * + * @param array $hints + * + * @return $this + */ + public function setHints(array $hints) + { + $this->hints = $hints; + return $this; + } + + /** + * Add a hint for this form + * + * If $hint is an array the second value should be an + * array as well containing additional HTML properties. + * + * @param string|array $hint + * + * @return $this + */ + public function addHint($hint) + { + $this->hints[] = $hint; + return $this; + } + + /** + * Return the hints of this form + * + * @return array + */ + public function getHints() + { + if ($this->hints === null) { + return array(); + } + + return $this->hints; + } + /** * Set whether the Autosubmit decorator should be applied to this form * @@ -1103,10 +1153,11 @@ class Form extends Zend_Form ->addDecorator('HtmlTag', array('tag' => 'div', 'class' => 'header')); } - $this->addDecorator('FormErrors', array('onlyCustomFormErrors' => true)) + $this->addDecorator('FormDescriptions') ->addDecorator('FormNotifications') - ->addDecorator('FormDescriptions') + ->addDecorator('FormErrors', array('onlyCustomFormErrors' => true)) ->addDecorator('FormElements') + ->addDecorator('FormHints') //->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form')) ->addDecorator('Form'); } diff --git a/library/Icinga/Web/Form/Decorator/FormDescriptions.php b/library/Icinga/Web/Form/Decorator/FormDescriptions.php index c8ba16ead..c5d460df0 100644 --- a/library/Icinga/Web/Form/Decorator/FormDescriptions.php +++ b/library/Icinga/Web/Form/Decorator/FormDescriptions.php @@ -7,35 +7,10 @@ use Zend_Form_Decorator_Abstract; use Icinga\Web\Form; /** - * Decorator to add a list of descriptions at the top of a form - * - * The description for required form elements is automatically being handled. + * Decorator to add a list of descriptions at the top or bottom of a form */ class FormDescriptions extends Zend_Form_Decorator_Abstract { - /** - * A list of element class names to be ignored when detecting which message to use to describe required elements - * - * @var array - */ - protected $blacklist; - - /** - * {@inheritdoc} - */ - public function __construct($options = null) - { - parent::__construct($options); - $this->blacklist = array( - 'Zend_Form_Element_Hidden', - 'Zend_Form_Element_Submit', - 'Zend_Form_Element_Button', - 'Icinga\Web\Form\Element\Note', - 'Icinga\Web\Form\Element\Button', - 'Icinga\Web\Form\Element\CsrfCounterMeasure' - ); - } - /** * Render form descriptions * @@ -55,18 +30,7 @@ class FormDescriptions extends Zend_Form_Decorator_Abstract return $content; } - $descriptions = $this->recurseForm($form, $entirelyRequired); - if ($entirelyRequired) { - $descriptions[] = $form->getView()->translate( - 'All fields are required and must be filled in to complete the form.' - ); - } elseif ($entirelyRequired === false) { - $descriptions[] = $form->getView()->translate(sprintf( - 'Required fields are marked with %s and must be filled in to complete the form.', - $form->getRequiredCue() - )); - } - + $descriptions = $this->recurseForm($form); if (empty($descriptions)) { return $content; } @@ -92,53 +56,15 @@ class FormDescriptions extends Zend_Form_Decorator_Abstract /** * Recurse the given form and return the descriptions for it and all of its subforms * - * @param Form $form The form to recurse - * @param mixed $entirelyRequired Set by reference, true means all elements in the hierarchy are - * required, false only a partial subset and null none at all - * @param bool $elementsPassed Whether there were any elements passed during the recursion until now + * @param Form $form The form to recurse * * @return array */ - protected function recurseForm(Form $form, & $entirelyRequired = null, $elementsPassed = false) + protected function recurseForm(Form $form) { - $requiredLabels = array(); - if ($form->getRequiredCue() !== null) { - $partiallyRequired = $partiallyOptional = false; - foreach ($form->getElements() as $element) { - if (! in_array($element->getType(), $this->blacklist)) { - if (! $element->isRequired()) { - $partiallyOptional = true; - if ($entirelyRequired) { - $entirelyRequired = false; - } - } else { - $partiallyRequired = true; - if (($label = $element->getDecorator('label')) !== false) { - $requiredLabels[] = $label; - } - } - } - } - - if (! $elementsPassed) { - $elementsPassed = $partiallyRequired || $partiallyOptional; - if ($entirelyRequired === null && $partiallyRequired) { - $entirelyRequired = ! $partiallyOptional; - } - } elseif ($entirelyRequired === null && $partiallyRequired) { - $entirelyRequired = false; - } - } - $descriptions = array($form->getDescriptions()); foreach ($form->getSubForms() as $subForm) { - $descriptions[] = $this->recurseForm($subForm, $entirelyRequired, $elementsPassed); - } - - if ($entirelyRequired) { - foreach ($requiredLabels as $label) { - $label->setRequiredSuffix(''); - } + $descriptions[] = $this->recurseForm($subForm); } return call_user_func_array('array_merge', $descriptions); diff --git a/library/Icinga/Web/Form/Decorator/FormHints.php b/library/Icinga/Web/Form/Decorator/FormHints.php new file mode 100644 index 000000000..91e906684 --- /dev/null +++ b/library/Icinga/Web/Form/Decorator/FormHints.php @@ -0,0 +1,142 @@ +blacklist = array( + 'Zend_Form_Element_Hidden', + 'Zend_Form_Element_Submit', + 'Zend_Form_Element_Button', + 'Icinga\Web\Form\Element\Note', + 'Icinga\Web\Form\Element\Button', + 'Icinga\Web\Form\Element\CsrfCounterMeasure' + ); + } + + /** + * Render form hints + * + * @param string $content The html rendered so far + * + * @return string The updated html + */ + public function render($content = '') + { + $form = $this->getElement(); + if (! $form instanceof Form) { + return $content; + } + + $view = $form->getView(); + if ($view === null) { + return $content; + } + + $hints = $this->recurseForm($form, $entirelyRequired); + if ($entirelyRequired !== null) { + $hints[] = $form->getView()->translate(sprintf( + 'Required fields are marked with %s and must be filled in to complete the form.', + $form->getRequiredCue() + )); + } + + if (empty($hints)) { + return $content; + } + + $html = ''; + case self::PREPEND: + return $html . '' . $content; + } + } + + /** + * Recurse the given form and return the hints for it and all of its subforms + * + * @param Form $form The form to recurse + * @param mixed $entirelyRequired Set by reference, true means all elements in the hierarchy are + * required, false only a partial subset and null none at all + * @param bool $elementsPassed Whether there were any elements passed during the recursion until now + * + * @return array + */ + protected function recurseForm(Form $form, & $entirelyRequired = null, $elementsPassed = false) + { + $requiredLabels = array(); + if ($form->getRequiredCue() !== null) { + $partiallyRequired = $partiallyOptional = false; + foreach ($form->getElements() as $element) { + if (! in_array($element->getType(), $this->blacklist)) { + if (! $element->isRequired()) { + $partiallyOptional = true; + if ($entirelyRequired) { + $entirelyRequired = false; + } + } else { + $partiallyRequired = true; + if (($label = $element->getDecorator('label')) !== false) { + $requiredLabels[] = $label; + } + } + } + } + + if (! $elementsPassed) { + $elementsPassed = $partiallyRequired || $partiallyOptional; + if ($entirelyRequired === null && $partiallyRequired) { + $entirelyRequired = ! $partiallyOptional; + } + } elseif ($entirelyRequired === null && $partiallyRequired) { + $entirelyRequired = false; + } + } + + $hints = array($form->getHints()); + foreach ($form->getSubForms() as $subForm) { + $hints[] = $this->recurseForm($subForm, $entirelyRequired, $elementsPassed); + } + + if ($entirelyRequired) { + foreach ($requiredLabels as $label) { + $label->setRequiredSuffix(''); + } + } + + return call_user_func_array('array_merge', $hints); + } +} diff --git a/library/Icinga/Web/MenuRenderer.php b/library/Icinga/Web/MenuRenderer.php index 4dec9404e..7b6ad9856 100644 --- a/library/Icinga/Web/MenuRenderer.php +++ b/library/Icinga/Web/MenuRenderer.php @@ -118,7 +118,13 @@ class MenuRenderer extends RecursiveIteratorIterator try { return $child->getRenderer()->render($child); } catch (Exception $e) { - Logger::error('Could not invoke custom renderer. Exception: '. $e->getMessage()); + Logger::error( + 'Could not invoke custom menu renderer. %s in %s:%d with message: %s', + get_class($e), + $e->getFile(), + $e->getLine(), + $e->getMessage() + ); } } diff --git a/modules/monitoring/library/Monitoring/Web/Menu/BackendAvailabilityMenuItemRenderer.php b/modules/monitoring/library/Monitoring/Web/Menu/BackendAvailabilityMenuItemRenderer.php index 186351a40..4359d5da5 100644 --- a/modules/monitoring/library/Monitoring/Web/Menu/BackendAvailabilityMenuItemRenderer.php +++ b/modules/monitoring/library/Monitoring/Web/Menu/BackendAvailabilityMenuItemRenderer.php @@ -3,41 +3,50 @@ namespace Icinga\Module\Monitoring\Web\Menu; -use Icinga\Web\Menu as Menu; -use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Web\Menu; use Icinga\Web\Menu\MenuItemRenderer; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; class BackendAvailabilityMenuItemRenderer extends MenuItemRenderer { /** - * Checks whether the monitoring backend is running or not + * Get whether or not the monitoring backend is currently running * - * @return mixed + * @return bool */ protected function isCurrentlyRunning() { - return MonitoringBackend::instance()->select()->from( - 'programstatus', - array( - 'is_currently_running' + $programStatus = MonitoringBackend::instance() + ->select() + ->from( + 'programstatus', + array('is_currently_running') ) - )->getQuery()->fetchRow()->is_currently_running; + ->fetchRow(); + return $programStatus !== false ? (bool) $programStatus : false; } /** - * @see MenuItemRenderer::render() + * {@inheritdoc} */ public function render(Menu $menu) { return $this->getBadge() . $this->createLink($menu); } + /** + * Get the problem badge HTML + * + * @return string + */ protected function getBadge() { - if (! (bool)$this->isCurrentlyRunning()) { + if (! $this->isCurrentlyRunning()) { return sprintf( - '
%s
', - mt('monitoring', 'monitoring backend is not running'), + '
%d
', + sprintf( + mt('monitoring', 'Monitoring backend %s is not running'), MonitoringBackend::instance()->getName() + ), 1 ); } @@ -51,10 +60,12 @@ class BackendAvailabilityMenuItemRenderer extends MenuItemRenderer */ public function getSummary() { - if (! (bool)$this->isCurrentlyRunning()) { + if (! $this->isCurrentlyRunning()) { return array( - 'problems' => 1, - 'title' => mt('monitoring', 'monitoring backend is not running') + 'problems' => 1, + 'title' => sprintf( + mt('monitoring', 'Monitoring backend %s is not running'), MonitoringBackend::instance()->getName() + ) ); } return null; diff --git a/modules/setup/application/forms/AdminAccountPage.php b/modules/setup/application/forms/AdminAccountPage.php index 90ccd6fe5..191f27b8b 100644 --- a/modules/setup/application/forms/AdminAccountPage.php +++ b/modules/setup/application/forms/AdminAccountPage.php @@ -89,9 +89,6 @@ class AdminAccountPage extends Form } if (count($choices) > 1) { - $this->addDescription($this->translate( - 'Below are several options you can choose from for how to define the desired account.' - )); $this->addElement( 'select', 'user_type', @@ -99,6 +96,7 @@ class AdminAccountPage extends Form 'required' => true, 'autosubmit' => true, 'label' => $this->translate('Type Of Definition'), + 'description' => $this->translate('Choose how to define the desired account.'), 'multiOptions' => $choices, 'value' => $choice ) @@ -124,7 +122,7 @@ class AdminAccountPage extends Form 'label' => $this->translate('Username'), 'description' => $this->translate( 'Define the initial administrative account by providing a username that reflects' - . ' a user created later or one that is authenticated using external mechanisms' + . ' a user created later or one that is authenticated using external mechanisms.' ) ) ); @@ -139,7 +137,7 @@ class AdminAccountPage extends Form 'label' => $this->translate('Username'), 'description' => sprintf( $this->translate( - 'Choose a user reported by the %s backend as the initial administrative account', + 'Choose a user reported by the %s backend as the initial administrative account.', 'setup.admin' ), $this->backendConfig['backend'] === 'db' @@ -159,7 +157,7 @@ class AdminAccountPage extends Form 'required' => true, 'label' => $this->translate('Username'), 'description' => $this->translate( - 'Enter the username to be used when creating an initial administrative account' + 'Enter the username to be used when creating an initial administrative account.' ) ) ); @@ -170,7 +168,9 @@ class AdminAccountPage extends Form 'required' => true, 'renderPassword' => true, 'label' => $this->translate('Password'), - 'description' => $this->translate('Enter the password to assign to the newly created account') + 'description' => $this->translate( + 'Enter the password to assign to the newly created account.' + ) ) ); $this->addElement( @@ -181,7 +181,7 @@ class AdminAccountPage extends Form 'renderPassword' => true, 'label' => $this->translate('Repeat password'), 'description' => $this->translate( - 'Please repeat the password given above to avoid typing errors' + 'Please repeat the password given above to avoid typing errors.' ), 'validators' => array( array('identical', false, array('new_user_password')) diff --git a/public/css/icinga/forms.less b/public/css/icinga/forms.less index 42f04d7f3..38289877e 100644 --- a/public/css/icinga/forms.less +++ b/public/css/icinga/forms.less @@ -259,3 +259,13 @@ form ul.descriptions { } } } + +form ul.hints { + .non-list-like-list; + padding: 0.5em 0.5em 0 0.5em; + + li { + font-size: 0.8em; + padding-bottom: 0.5em; + } +} diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index 72cffa19b..c51ef8722 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -104,7 +104,7 @@ table.benchmark { .info-box { padding: 0.5em; border: 1px solid lightgrey; - background-color: #fbfcc5; + background-color: #f2f4fd; } /* Action table */ diff --git a/test/php/application/forms/Config/Resource/DbResourceFormTest.php b/test/php/application/forms/Config/Resource/DbResourceFormTest.php index 3cf23a70a..6aa51dbff 100644 --- a/test/php/application/forms/Config/Resource/DbResourceFormTest.php +++ b/test/php/application/forms/Config/Resource/DbResourceFormTest.php @@ -26,7 +26,7 @@ class DbResourceFormTest extends BaseTestCase public function testValidDbResourceIsValid() { $this->setUpResourceFactoryMock( - Mockery::mock()->shouldReceive('getConnection')->atMost()->twice()->andReturn(Mockery::self())->getMock() + Mockery::mock()->shouldReceive('inspect')->andReturn(self::createInspector(false))->getMock() ); $this->assertTrue( @@ -42,7 +42,7 @@ class DbResourceFormTest extends BaseTestCase public function testInvalidDbResourceIsNotValid() { $this->setUpResourceFactoryMock( - Mockery::mock()->shouldReceive('getConnection')->once()->andThrow('\Exception')->getMock() + Mockery::mock()->shouldReceive('inspect')->andReturn(self::createInspector(true))->getMock() ); $this->assertFalse( @@ -58,4 +58,21 @@ class DbResourceFormTest extends BaseTestCase ->with(Mockery::type('Icinga\Data\ConfigObject')) ->andReturn($resourceMock); } + + public static function createInspector($error = false, $log = array('log')) + { + if (! $error) { + $calls = array( + 'hasError' => false, + 'toArray' => $log + ); + } else { + $calls = array( + 'hasError' => true, + 'getError' => 'Error', + 'toArray' => $log + ); + } + return Mockery::mock('Icinga\Data\Inspection', $calls); + } } diff --git a/test/php/application/forms/Config/Resource/LdapResourceFormTest.php b/test/php/application/forms/Config/Resource/LdapResourceFormTest.php index 859288256..bef49edca 100644 --- a/test/php/application/forms/Config/Resource/LdapResourceFormTest.php +++ b/test/php/application/forms/Config/Resource/LdapResourceFormTest.php @@ -26,14 +26,16 @@ class LdapResourceFormTest extends BaseTestCase public function testValidLdapResourceIsValid() { $this->setUpResourceFactoryMock( - Mockery::mock()->shouldReceive('bind')->once()->getMock() + Mockery::mock()->shouldReceive('inspect')->andReturn(self::createInspector(false))->getMock() ); // Passing array(null) is required to make Mockery call the constructor... $form = Mockery::mock('Icinga\Forms\Config\Resource\LdapResourceForm[getView]', array(null)); $form->shouldReceive('getView->escape') ->with(Mockery::type('string')) - ->andReturnUsing(function ($s) { return $s; }); + ->andReturnUsing(function ($s) { + return $s; + }); $form->setTokenDisabled(); $this->assertTrue( @@ -49,14 +51,16 @@ class LdapResourceFormTest extends BaseTestCase public function testInvalidLdapResourceIsNotValid() { $this->setUpResourceFactoryMock( - Mockery::mock()->shouldReceive('bind')->andThrow('\Exception')->getMock() + Mockery::mock()->shouldReceive('inspect')->andReturn(self::createInspector(true))->getMock() ); // Passing array(null) is required to make Mockery call the constructor... $form = Mockery::mock('Icinga\Forms\Config\Resource\LdapResourceForm[getView]', array(null)); $form->shouldReceive('getView->escape') ->with(Mockery::type('string')) - ->andReturnUsing(function ($s) { return $s; }); + ->andReturnUsing(function ($s) { + return $s; + }); $form->setTokenDisabled(); $this->assertFalse( @@ -72,4 +76,21 @@ class LdapResourceFormTest extends BaseTestCase ->with(Mockery::type('Icinga\Data\ConfigObject')) ->andReturn($resourceMock); } + + public static function createInspector($error = false, $log = array('log')) + { + if (! $error) { + $calls = array( + 'hasError' => false, + 'toArray' => $log + ); + } else { + $calls = array( + 'hasError' => true, + 'getError' => 'Error', + 'toArray' => $log + ); + } + return Mockery::mock('Icinga\Data\Inspection', $calls); + } } diff --git a/test/php/application/forms/Config/UserBackend/DbBackendFormTest.php b/test/php/application/forms/Config/UserBackend/DbBackendFormTest.php index d58ff8a33..cc6919c93 100644 --- a/test/php/application/forms/Config/UserBackend/DbBackendFormTest.php +++ b/test/php/application/forms/Config/UserBackend/DbBackendFormTest.php @@ -28,14 +28,16 @@ class DbBackendFormTest extends BaseTestCase { $this->setUpResourceFactoryMock(); Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend') - ->shouldReceive('select->where->count') - ->andReturn(2); + ->shouldReceive('inspect') + ->andReturn(self::createInspector(false)); // Passing array(null) is required to make Mockery call the constructor... $form = Mockery::mock('Icinga\Forms\Config\UserBackend\DbBackendForm[getView]', array(null)); $form->shouldReceive('getView->escape') ->with(Mockery::type('string')) - ->andReturnUsing(function ($s) { return $s; }); + ->andReturnUsing(function ($s) { + return $s; + }); $form->setTokenDisabled(); $form->setResources(array('test_db_backend')); $form->populate(array('resource' => 'test_db_backend')); @@ -54,14 +56,16 @@ class DbBackendFormTest extends BaseTestCase { $this->setUpResourceFactoryMock(); Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend') - ->shouldReceive('count') - ->andReturn(0); + ->shouldReceive('inspect') + ->andReturn(self::createInspector(true)); // Passing array(null) is required to make Mockery call the constructor... $form = Mockery::mock('Icinga\Forms\Config\UserBackend\DbBackendForm[getView]', array(null)); $form->shouldReceive('getView->escape') ->with(Mockery::type('string')) - ->andReturnUsing(function ($s) { return $s; }); + ->andReturnUsing(function ($s) { + return $s; + }); $form->setTokenDisabled(); $form->setResources(array('test_db_backend')); $form->populate(array('resource' => 'test_db_backend')); @@ -80,4 +84,21 @@ class DbBackendFormTest extends BaseTestCase ->shouldReceive('getResourceConfig') ->andReturn(new ConfigObject()); } + + public static function createInspector($error = false, $log = array('log')) + { + if (! $error) { + $calls = array( + 'hasError' => false, + 'toArray' => $log + ); + } else { + $calls = array( + 'hasError' => true, + 'getError' => 'Error', + 'toArray' => $log + ); + } + return Mockery::mock('Icinga\Data\Inspection', $calls); + } } diff --git a/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php b/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php index 6fe63adf4..c5bb8c9af 100644 --- a/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php +++ b/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php @@ -12,6 +12,7 @@ use Icinga\Data\ConfigObject; use Icinga\Test\BaseTestCase; use Icinga\Forms\Config\UserBackend\LdapBackendForm; use Icinga\Exception\AuthenticationException; +use Tests\Icinga\Forms\Config\Resource\LdapResourceFormTest; class LdapBackendFormTest extends BaseTestCase { @@ -27,15 +28,18 @@ class LdapBackendFormTest extends BaseTestCase */ public function testValidBackendIsValid() { - $ldapUserBackendMock = Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend'); - $ldapUserBackendMock->shouldReceive('assertAuthenticationPossible')->andReturnNull(); + $ldapUserBackendMock = Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend') + ->shouldReceive('inspect') + ->andReturn(self::createInspector(false))->getMock(); $this->setUpUserBackendMock($ldapUserBackendMock); // Passing array(null) is required to make Mockery call the constructor... $form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null)); $form->shouldReceive('getView->escape') ->with(Mockery::type('string')) - ->andReturnUsing(function ($s) { return $s; }); + ->andReturnUsing(function ($s) { + return $s; + }); $form->setTokenDisabled(); $form->setResources(array('test_ldap_backend')); $form->populate(array('resource' => 'test_ldap_backend')); @@ -52,7 +56,9 @@ class LdapBackendFormTest extends BaseTestCase */ public function testInvalidBackendIsNotValid() { - $ldapUserBackendMock = Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend'); + $ldapUserBackendMock = Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend') + ->shouldReceive('inspect') + ->andReturn(self::createInspector(true))->getMock(); $ldapUserBackendMock->shouldReceive('assertAuthenticationPossible')->andThrow(new AuthenticationException); $this->setUpUserBackendMock($ldapUserBackendMock); @@ -60,7 +66,9 @@ class LdapBackendFormTest extends BaseTestCase $form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null)); $form->shouldReceive('getView->escape') ->with(Mockery::type('string')) - ->andReturnUsing(function ($s) { return $s; }); + ->andReturnUsing(function ($s) { + return $s; + }); $form->setTokenDisabled(); $form->setResources(array('test_ldap_backend')); $form->populate(array('resource' => 'test_ldap_backend')); @@ -77,4 +85,21 @@ class LdapBackendFormTest extends BaseTestCase ->shouldReceive('create') ->andReturn($ldapUserBackendMock); } + + public static function createInspector($error = false, $log = array('log')) + { + if (! $error) { + $calls = array( + 'hasError' => false, + 'toArray' => $log + ); + } else { + $calls = array( + 'hasError' => true, + 'getError' => 'Error', + 'toArray' => $log + ); + } + return Mockery::mock('Icinga\Data\Inspection', $calls); + } }