From aab69a41e84d93bf38722d0d7996b43a2653ffe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Mon, 3 Jun 2013 17:02:08 +0200 Subject: [PATCH] Add tested ldap and web libraries refs #4212 --- library/Icinga/Protocol/Ldap/Connection.php | 376 ++++++++++++++++++ library/Icinga/Protocol/Ldap/Exception.php | 7 + library/Icinga/Protocol/Ldap/LdapUtils.php | 115 ++++++ library/Icinga/Protocol/Ldap/Node.php | 47 +++ library/Icinga/Protocol/Ldap/Query.php | 314 +++++++++++++++ library/Icinga/Protocol/Ldap/Root.php | 155 ++++++++ library/Icinga/Web/ActionController.php | 350 ++++++++++++++++ library/Icinga/Web/Hook.php | 90 +++++ .../Icinga/Web/.ActionControllerTest.php.swp | Bin 0 -> 12288 bytes .../Icinga/Web/ActionControllerTest.php | 2 +- tests/php/library/Icinga/Web/HookTest.php | 4 +- 11 files changed, 1457 insertions(+), 3 deletions(-) create mode 100644 library/Icinga/Protocol/Ldap/Connection.php create mode 100644 library/Icinga/Protocol/Ldap/Exception.php create mode 100644 library/Icinga/Protocol/Ldap/LdapUtils.php create mode 100644 library/Icinga/Protocol/Ldap/Node.php create mode 100644 library/Icinga/Protocol/Ldap/Query.php create mode 100644 library/Icinga/Protocol/Ldap/Root.php create mode 100755 library/Icinga/Web/ActionController.php create mode 100755 library/Icinga/Web/Hook.php create mode 100644 tests/php/library/Icinga/Web/.ActionControllerTest.php.swp diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php new file mode 100644 index 000000000..823e42876 --- /dev/null +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -0,0 +1,376 @@ + + * $lconf = new Connection((object) array( + * 'hostname' => 'localhost', + * 'root_dn' => 'dc=monitoring,dc=...', + * 'bind_dn' => 'cn=Mangager,dc=monitoring,dc=...', + * 'bind_pw' => '***' + * )); + * + * + * @copyright Copyright (c) 2013 Icinga-Web Team + * @author Icinga-Web Team + * @package Icinga\Protocol\Ldap + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Connection +{ + protected $ds; + protected $hostname; + protected $bind_dn; + protected $bind_pw; + protected $root_dn; + protected $count; + protected $ldap_extension = array( + '1.3.6.1.4.1.1466.20037' => 'STARTTLS', // notes? + // '1.3.6.1.4.1.4203.1.11.1' => '11.1', // PASSWORD_MODIFY + // '1.3.6.1.4.1.4203.1.11.3' => '11.3', // Whoami + // '1.3.6.1.1.8' => '8', // Cancel Extended Request + ); + + protected $ms_capability = array( + // Prefix LDAP_CAP_ + // Source: http://msdn.microsoft.com/en-us/library/cc223359.aspx + + // Running Active Directory as AD DS: + '1.2.840.113556.1.4.800' => 'ACTIVE_DIRECTORY_OID', + + // Capable of signing and sealing on an NTLM authenticated connection + // and of performing subsequent binds on a signed or sealed connection. + '1.2.840.113556.1.4.1791' => 'ACTIVE_DIRECTORY_LDAP_INTEG_OID', + + // If AD DS: running at least W2K3, if AD LDS running at least W2K8 + '1.2.840.113556.1.4.1670' => 'ACTIVE_DIRECTORY_V51_OID', + + // If AD LDS: accepts DIGEST-MD5 binds for AD LDSsecurity principals + '1.2.840.113556.1.4.1880' => 'ACTIVE_DIRECTORY_ADAM_DIGEST', + + // Running Active Directory as AD LDS + '1.2.840.113556.1.4.1851' => 'ACTIVE_DIRECTORY_ADAM_OID', + + // If AD DS: it's a Read Only DC (RODC) + '1.2.840.113556.1.4.1920' => 'ACTIVE_DIRECTORY_PARTIAL_SECRETS_OID', + + // Running at least W2K8 + '1.2.840.113556.1.4.1935' => 'ACTIVE_DIRECTORY_V60_OID', + + // Running at least W2K8r2 + '1.2.840.113556.1.4.2080' => 'ACTIVE_DIRECTORY_V61_R2_OID', + + // Running at least W2K12 + '1.2.840.113556.1.4.2237' => 'ACTIVE_DIRECTORY_W8_OID', + + ); + + protected $root; + + /** + * Constructor + * + * TODO: Allow to pass port and SSL options + * + * @param array LDAP connection credentials + */ + public function __construct($config) + { + $this->hostname = $config->hostname; + $this->bind_dn = $config->bind_dn; + $this->bind_pw = $config->bind_pw; + $this->root_dn = $config->root_dn; + + } + + public function getDN() + { + return $this->root_dn; + } + + public function root() + { + if ($this->root === null) { + $this->root = Root::forConnection($this); + } + return $this->root; + } + + public function select() + { + return new Query($this); + } + + public function fetchOne($query, $fields = array()) + { + $row = (array) $this->fetchRow($query, $fields); + return array_shift($row); + } + + public function fetchDN($query, $fields = array()) + { + $rows = $this->fetchAll($query, $fields); + if (count($rows) !== 1) { + throw new Exception(sprintf( + 'Cannot fetch single DN for %s', + $query + )); + } + return key($rows); + } + + + public function fetchRow($query, $fields = array()) + { + // TODO: This is ugly, make it better! + $results = $this->fetchAll($query, $fields); + return array_shift($results); + } + + public function count(Query $query) + { + $results = $this->runQuery($query, '+'); + return ldap_count_entries($this->ds, $results); + } + + public function fetchAll($query, $fields = array()) + { + $offset = null; + $limit = null; + if ($query->hasLimit()) { + $offset = $query->getOffset(); + $limit = $query->getLimit(); + } + $entries = array(); + $results = $this->runQuery($query, $fields); + $entry = ldap_first_entry($this->ds, $results); + $count = 0; + while ($entry) { + if (($offset === null || $offset <= $count) + && ($limit === null || ($offset + $limit) >= $count) + ) { + $attrs = ldap_get_attributes($this->ds, $entry); + $entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes($attrs); + } + $count++; + $entry = ldap_next_entry($this->ds, $entry); + } + ldap_free_result($results); + return $entries; + } + + public function cleanupAttributes(& $attrs) + { + $clean = (object) array(); + for ($i = 0; $i < $attrs['count']; $i++) { + $attr_name = $attrs[$i]; + if ($attrs[$attr_name]['count'] === 1) { + $clean->$attr_name = $attrs[$attr_name][0]; + } else { + for ($j = 0; $j < $attrs[$attr_name]['count']; $j++) { + $clean->{$attr_name}[] = $attrs[$attr_name][$j]; + } + } + } + return $clean; + } + + protected function runQuery($query, $fields) + { + $this->connect(); + if ($query instanceof Query) { + $fields = $query->listFields(); + } + // WARNING: + // We do not support pagination right now, and there is no chance to + // do so for PHP < 5.4. Warnings about "Sizelimit exceeded" will + // therefore not be hidden right now. + $results = ldap_search( + $this->ds, + $this->root_dn, + (string) $query, + $fields, + 0, // Attributes and values + 0 // No limit - at least where possible + ); + if (! $results) { + throw new Exception(sprintf( + 'LDAP query "%s" (root %s) failed: %s', + $query, + $this->root_dn, + ldap_error($this->ds) + )); + die('Query failed'); + } + $list = array(); + if ($query instanceof Query) { + foreach ($query->getSortColumns() as $col) { + ldap_sort($this->ds, $results, $col[0]) ; + } + } + return $results; + } + + public function testCredentials($username, $password) + { + Log::debug("Trying to connect to %s", $this->hostname); + $ds = ldap_connect($this->hostname); + Log::debug("ldap_bind (%s)", $username); + $r = @ldap_bind($ds, $username, $password); + if ($r) { + return true; + } else { + log::fatal('LDAP connection (%s / %s) failed: %s', + $username, + '***', + ldap_error($ds)); + return false; + /* TODO: Log failure + throw new Exception(sprintf( + 'LDAP connection (%s / %s) failed: %s', + $username, + '***', + ldap_error($ds) + )); + */ + } + } + + protected function getConfigDir() + { + return Config::getInstance()->getConfigDir() . '/ldap'; + } + + protected function discoverServerlistForDomain($domain) + { + $ldaps_records = dns_get_record('_ldaps._tcp.' . $domain, DNS_SRV); + $ldap_records = dns_get_record('_ldap._tcp.' . $domain, DNS_SRV); + } + + protected function prepareTlsEnvironment() + { + $strict_tls = true; + $use_local_ca = true; + if (Platform::isWindows()) { + } else { + $cfg_dir = $this->getConfigDir(); + if ($strict_tls) { + putenv(sprintf('LDAPRC=%s/%s', $cfg_dir, 'ldap_ca.conf')); + } else { + putenv(sprintf('LDAPRC=%s/%s', $cfg_dir, 'ldap_nocert.conf')); + } + } + // file_put_contents('/tmp/tom_LDAP.conf', "TLS_REQCERT never\n"); + } + + protected function fetchRootDseDetails() + { + $query = $this->select()->from('*', array('+')) + /*, array( + 'defaultNamingContext', + 'namingContexts', + 'supportedSaslMechanisms', + 'dnsHostName', + 'schemaNamingContext', + 'supportedLDAPVersion', // => array(3, 2) + 'supportedCapabilities' + ))*/ + ; + $fields = $query->listFields(); + + $result = ldap_read( + $this->ds, + '', + (string) $query, + $query->listFields(), + 0, + 0 + ); + + $entry = ldap_first_entry($this->ds, $result); + $result = $this->cleanupAttributes(ldap_get_attributes($this->ds, $entry)); + + + if (isset($result->supportedCapabilities)) { + foreach ($result->supportedCapabilities as $oid) { + if (array_key_exists($oid, $this->ms_capability)) { + echo $this->ms_capability[$oid] . "\n"; + } + } + } + if (isset($result->supportedExtension)) { + foreach ($result->supportedExtension as $oid) { + if (array_key_exists($oid, $this->ldap_extension)) { + echo $this->ldap_extension[$oid] . "\n"; + // STARTTLS -> läuft mit OpenLDAP + } + } + } + return $result; + } + + public function discoverCapabilities() + { + $this->fetchRootDseDetails(); + } + + public function connect() + { + if ($this->ds !== null) return; + $use_tls = true; + $force_tls = true; + + if ($use_tls) { + $this->prepareTlsEnvironment(); + } + Log::debug("Trying to connect to %s", $this->hostname); + $this->ds = ldap_connect($this->hostname, 389); + $this->discoverCapabilities(); + Log::debug("Trying ldap_start_tls()"); + if (ldap_start_tls($this->ds)) { + Log::debug("Trying ldap_start_tls() succeeded"); + } else { + Log::warn("ldap_start_tls() failed: %s. Does your ldap_ca.conf point to the certificate? ",ldap_error($this->ds)); + } + + + // ldap_rename requires LDAPv3: + if (! ldap_set_option($this->ds, LDAP_OPT_PROTOCOL_VERSION, 3)) { + throw new Exception('LDAPv3 is required'); + } +// + // Not setting this results in "Operations error" on AD when using the + // whole domain as search base: + ldap_set_option($this->ds, LDAP_OPT_REFERRALS, 0); + // ldap_set_option($this->ds, LDAP_OPT_DEREF, LDAP_DEREF_NEVER); + Log::debug("Trying ldap_bind(%s)",$this->bind_dn); + $r = @ldap_bind($this->ds, $this->bind_dn, $this->bind_pw); + + if (! $r) { + log::fatal('LDAP connection (%s / %s) failed: %s', + $this->bind_dn, + '***', + ldap_error($this->ds)); + throw new Exception(sprintf( + 'LDAP connection (%s / %s) failed: %s', + $this->bind_dn, + '***' /* $this->bind_pw */, + ldap_error($this->ds) + )); + } + } +} + diff --git a/library/Icinga/Protocol/Ldap/Exception.php b/library/Icinga/Protocol/Ldap/Exception.php new file mode 100644 index 000000000..7dec43df9 --- /dev/null +++ b/library/Icinga/Protocol/Ldap/Exception.php @@ -0,0 +1,7 @@ + + * @author Icinga-Web Team + * @package Icinga\Protocol\Ldap + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class LdapUtils +{ + /** + * Extends PHPs ldap_explode_dn() function + * + * UTF-8 chars like German umlauts would otherwise be escaped and shown + * as backslash-prefixed hexcode-sequenzes. + * + * @param string DN + * @param boolean Returns 'type=value' when true and 'value' when false + * @return string + */ + public static function explodeDN($dn, $with_type = true) + { + $res = ldap_explode_dn($dn, $with_type ? 0 : 1); + foreach ($res as $k => $v) { + $res[$k] = preg_replace( + '/\\\([0-9a-f]{2})/ei', + "chr(hexdec('\\1'))", + $v + ); + } + unset($res['count']); + return $res; + } + + /** + * Implode unquoted RDNs to a DN + * + * TODO: throw away, this is not how it shall be done + * + * @param string DN-component + * @return string + */ + public static function implodeDN($parts) + { + $str = ''; + foreach ($parts as $part) { + if ($str !== '') { $str .= ','; } + list($key, $val) = preg_split('~=~', $part, 2); + $str .= $key . '=' . self::quoteForDN($val); + } + return $str; + } + + /** + * Quote a string that should be used in a DN + * + * Special characters will be escaped + * + * @param string DN-component + * @return string + */ + public static function quoteForDN($str) + { + return self::quoteChars($str, array( + ',', '=', '+', '<', '>', ';', '\\', '"', '#' + )); + } + + /** + * Quote a string that should be used in an LDAP search + * + * Special characters will be escaped + * + * @param string String to be escaped + * @return string + */ + public static function quoteForSearch($str, $allow_wildcard = false) + { + if ($allow_wildcard) { + return self::quoteChars($str, array('(', ')', '\\', chr(0))); + } + return self::quoteChars($str, array('*', '(', ')', '\\', chr(0))); + } + + /** + * Escape given characters in the given string + * + * Special characters will be escaped + * + * @param string String to be escaped + * @return string + */ + protected static function quoteChars($str, $chars) + { + $quotedChars = array(); + foreach ($chars as $k => $v) { + // Temporarily prefixing with illegal '(' + $quotedChars[$k] = '(' . str_pad(dechex(ord($v)), 2, '0'); + } + $str = str_replace($chars, $quotedChars, $str); + // Replacing temporary '(' with '\\'. This is a workaround, as + // str_replace behaves pretty strange with leading a backslash: + $str = preg_replace('~\(~', '\\', $str); + return $str; + } +} + diff --git a/library/Icinga/Protocol/Ldap/Node.php b/library/Icinga/Protocol/Ldap/Node.php new file mode 100644 index 000000000..1141eeb1d --- /dev/null +++ b/library/Icinga/Protocol/Ldap/Node.php @@ -0,0 +1,47 @@ + + * @author Icinga-Web Team + * @package Icinga\Protocol\Ldap + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Node extends Root +{ + protected $connection; + protected $rdn; + protected $parent; + + protected function __construct(Root $parent) + { + $this->connection = $parent->getConnection(); + $this->parent = $parent; + } + + public static function createWithRDN($parent, $rdn, $props = array()) + { + $node = new Node($parent); + $node->rdn = $rdn; + $node->props = $props; + return $node; + } + + public function getRDN() + { + return $this->rdn; + } + + public function getDN() + { + return $this->parent->getDN() . '.' . $this->getRDN(); + } +} + diff --git a/library/Icinga/Protocol/Ldap/Query.php b/library/Icinga/Protocol/Ldap/Query.php new file mode 100644 index 000000000..748fe0053 --- /dev/null +++ b/library/Icinga/Protocol/Ldap/Query.php @@ -0,0 +1,314 @@ + + * $connection->select()->from('user')->where('sAMAccountName = ?', 'icinga'); + * + * + * @copyright Copyright (c) 2013 Icinga-Web Team + * @author Icinga-Web Team + * @package Icinga\Protocol\Ldap + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Query +{ + protected $connection; + protected $filters = array(); + protected $fields = array(); + protected $limit_count; + protected $limit_offset; + protected $sort_columns = array(); + protected $count; + + /** + * Constructor + * + * @param Connection LDAP Connection object + * @return void + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * Count result set, ignoring limits + * + * @return int + */ + public function count() + { + if ($this->count === null) { + $this->count = $this->connection->count($this); + } + return $this->count; + } + + /** + * Count result set, ignoring limits + * + * @return int + */ + public function limit($count = null, $offset = null) + { + if (! preg_match('~^\d+~', $count . $offset)) { + throw new Exception(sprintf( + 'Got invalid limit: %s, %s', + $count, + $offset + )); + } + $this->limit_count = (int) $count; + $this->limit_offset = (int) $offset; + return $this; + } + + /** + * Whether a limit has been set + * + * @return boolean + */ + public function hasLimit() + { + return $this->limit_count !== null; + } + + /** + * Whether an offset (limit) has been set + * + * @return boolean + */ + public function hasOffset() + { + return $this->limit_offset > 0; + } + + /** + * Retrieve result limit + * + * @return int + */ + public function getLimit() + { + return $this->limit_count; + } + + /** + * Retrieve result offset + * + * @return int + */ + public function getOffset() + { + return $this->limit_offset; + } + + /** + * Fetch result as tree + * + * @return Node + */ + public function fetchTree() + { + $result = $this->fetchAll(); + $sorted = array(); + foreach ($result as $key => & $item) + { + $new_key = LdapUtils::implodeDN(array_reverse(LdapUtils::explodeDN( + preg_replace( + '/,' . preg_quote($this->connection->getDN(), '/') . '$/', + '', + $key + ) + ))); + $sorted[$new_key] = $key; + } + unset($groups); + ksort($sorted); + + $tree = Root::forConnection($this->connection); + foreach ($sorted as $sort_key => & $key) { + $tree->createChildByDN($key, $result[$key]); + } + return $tree; + } + + /** + * Fetch result as an array of objects + * + * @return array + */ + public function fetchAll() + { + return $this->connection->fetchAll($this); + } + + /** + * Fetch first result row + * + * @return object + */ + public function fetchRow() + { + return $this->connection->fetchRow($this); + } + + /** + * Fetch first column value from first result row + * + * @return mixed + */ + public function fetchOne() + { + return $this->connection->fetchOne($this); + } + + /** + * Fetch a key/value list, first column is key, second is value + * + * @return array + */ + public function fetchPairs() + { + // STILL TODO!! + return $this->connection->fetchPairs($this); + } + + /** + * Where to select (which fields) from + * + * This creates an objectClass filter + * + * @return Query + */ + public function from($objectClass, $fields = array()) + { + $this->filters['objectClass'] = $objectClass; + $this->fields = $fields; + return $this; + } + + /** + * Add a new filter to the query + * + * @param string Column to search in + * @param string Filter text (asterisks are allowed) + * @return Query + */ + public function where($key, $val) + { + $this->filters[$key] = $val; + return $this; + } + + /** + * Sort by given column + * + * TODO: Sort direction is not implemented yet + * + * @param string Order column + * @param string Order direction + * @return Query + */ + public function order($column, $direction = 'ASC') + { + $this->sort_columns[] = array($column, $direction); + return $this; + } + + /** + * Retrieve a list of the desired fields + * + * @return array + */ + public function listFields() + { + return $this->fields; + } + + /** + * Retrieve a list containing current sort columns + * + * @return array + */ + public function getSortColumns() + { + return $this->sort_columns; + } + + /** + * Return a pagination adapter for the current query + * + * @return \Zend_Paginator + */ + public function paginate($limit = null, $page = null) + { + if ($page === null || $limit === null) { + $request = \Zend_Controller_Front::getInstance()->getRequest(); + if ($page === null) { + $page = $request->getParam('page', 0); + } + if ($limit === null) { + $limit = $request->getParam('limit', 20); + } + } + $paginator = new \Zend_Paginator( + // TODO: Adapter doesn't fit yet: + new \Icinga\Web\Paginator\Adapter\QueryAdapter($this) + ); + $paginator->setItemCountPerPage($limit); + $paginator->setCurrentPageNumber($page); + return $paginator; + } + + /** + * Returns the LDAP filter that will be applied + * + * @string + */ + public function __toString() + { + return $this->render(); + } + + /** + * Returns the LDAP filter that will be applied + * + * @string + */ + protected function render() + { + $parts = array(); + if ($this->filters['objectClass'] === null) { + throw new Exception('Object class is mandatory'); + } + foreach ($this->filters as $key => $value) { + $parts[] = sprintf( + '%s=%s', + LdapUtils::quoteForSearch($key), + LdapUtils::quoteForSearch($value, true) + ); + } + return '(&(' . implode(')(', $parts) . '))'; + } + + /** + * Descructor + */ + public function __destruct() + { + // To be on the safe side: + unset($this->connection); + } +} + diff --git a/library/Icinga/Protocol/Ldap/Root.php b/library/Icinga/Protocol/Ldap/Root.php new file mode 100644 index 000000000..a4b489cfa --- /dev/null +++ b/library/Icinga/Protocol/Ldap/Root.php @@ -0,0 +1,155 @@ + + * @author Icinga-Web Team + * @package Icinga\Protocol\Ldap + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Root +{ + protected $rdn; + protected $connection; + protected $children = array(); + protected $props = array(); + + protected function __construct(Connection $connection) + { + $this->connection = $connection; + } + + public function hasParent() + { + return false; + } + + public static function forConnection(Connection $connection) + { + $root = new Root($connection); + return $root; + } + + public function createChildByDN($dn, $props = array()) + { + $dn = $this->stripMyDN($dn); + $parts = array_reverse(LdapUtils::explodeDN($dn)); + $parent = $this; + while($rdn = array_shift($parts)) { + if ($parent->hasChildRDN($rdn)) { + $child = $parent->getChildByRDN($rdn); + } else { + $child = Node::createWithRDN($parent, $rdn, (array) $props); + $parent->addChild($child); + } + $parent = $child; + } + return $child; + } + + public function hasChildRDN($rdn) + { + return array_key_exists(strtolower($rdn), $this->children); + } + + public function getChildByRDN($rdn) + { + if (! $this->hasChildRDN($rdn)) { + throw new Exception(sprintf( + 'The child RDN "%s" is not available', + $rdn + )); + } + return $this->children[strtolower($rdn)]; + } + + public function children() + { + return $this->children; + } + + public function hasChildren() + { + return ! empty($this->children); + } + + public function addChild(Node $child) + { + $this->children[strtolower($child->getRDN())] = $child; + return $this; + } + + protected function stripMyDN($dn) + { + $this->assertSubDN($dn); + return substr($dn, 0, strlen($dn) - strlen($this->getDN()) - 1); + } + + protected function assertSubDN($dn) + { + $mydn = $this->getDN(); + $end = substr($dn, -1 * strlen($mydn)); + if (strtolower($end) !== strtolower($mydn)) { + throw new Exception(sprintf( + '"%s" is not a child of "%s"', + $dn, + $mydn + )); + } + if (strlen($dn) === strlen($mydn)) { + throw new Exception(sprintf( + '"%s" is not a child of "%s", they are equal', + $dn, + $mydn + )); + } + return $this; + } + + public function setConnection(Connection $connection) + { + $this->connection = $connection; + return $this; + } + + public function getConnection() + { + return $this->connection; + } + + public function hasBeenChanged() + { + return false; + } + + public function getRDN() + { + return $this->getDN(); + } + + public function getDN() + { + return $this->connection->getDN(); + } + + public function __get($key) + { + if (! array_key_exists($key, $this->props)) { + return null; + } + return $this->props[$key]; + } + + public function __isset($key) + { + return array_key_exists($key, $this->props); + } +} + diff --git a/library/Icinga/Web/ActionController.php b/library/Icinga/Web/ActionController.php new file mode 100755 index 000000000..48087eb80 --- /dev/null +++ b/library/Icinga/Web/ActionController.php @@ -0,0 +1,350 @@ + + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class ActionController extends ZfController +{ + /** + * The Icinga Config object is available in all controllers. This is the + * modules config for module action controllers. + * + * @var Config + */ + protected $config; + + protected $replaceLayout = false; + + /** + * The current module name. TODO: Find out whether this shall be null for + * non-module actions + * + * @var string + */ + protected $module_name; + + /** + * The current controller name + * + * @var string + */ + protected $controller_name; + + /** + * The current action name + * + * @var string + */ + protected $action_name; + + protected $handlesAuthentication = false; + + protected $modifiesSession = false; + + protected $allowAccess = false; + + /** + * The constructor starts benchmarking, loads the configuration and sets + * other useful controller properties + * + * @param ZfRequest $request + * @param ZfResponse $response + * @param array $invokeArgs Any additional invocation arguments + */ + public function __construct( + ZfRequest $request, + ZfResponse $response, + array $invokeArgs = array() + ) { + Benchmark::measure('Action::__construct()'); + if (Auth::getInstance()->isAuthenticated() + && ! $this->modifiesSession + && ! Notification::getInstance()->hasMessages() + ) { + session_write_close(); + } + $this->module_name = $request->getModuleName(); + $this->controller_name = $request->getControllerName(); + $this->action_name = $request->getActionName(); + + $this->loadConfig(); + $this->setRequest($request) + ->setResponse($response) + ->_setInvokeArgs($invokeArgs); + $this->_helper = new ZfActionHelper($this); + + if ($this->handlesAuthentication() + || Auth::getInstance()->isAuthenticated()) { + $this->allowAccess = true; + $this->init(); + } + } + + /** + * This is where the configuration is going to be loaded + * + * @return void + */ + protected function loadConfig() + { + $this->config = Config::getInstance(); + } + + /** + * Translates the given string with the global translation catalog + * + * @param string $string The string that should be translated + * + * @return string + */ + public function translate($string) + { + return t($string); + } + + /** + * Helper function creating a new widget + * + * @param string $name The widget name + * @param string $properties Optional widget properties + * + * @return Widget\AbstractWidget + */ + public function widget($name, $properties = array()) + { + return Widget::create($name, $properties); + } + + /** + * Whether the current user has the given permission + * + * TODO: This has not been implemented yet + * + * @param string $permission Permission name + * @param string $object No idea what this should have been :-) + * + * @return bool + */ + final protected function hasPermission($uri, $permission = 'read') + { + return Auth::getInstance()->hasPermission($uri, $permission); + } + + /** + * Assert the current user has the given permission + * + * TODO: This has not been implemented yet + * + * @param string $permission Permission name + * @param string $object No idea what this should have been :-) + * + * @return self + */ + final protected function assertPermission($permission, $object = null) + { + if (! $this->hasPermission($permission, $object)) { + // TODO: Log violation, create dedicated Exception class + throw new \Exception('Permission denied'); + } + return $this; + } + + /** + * Our benchmark wants to know when we started our dispatch loop + * + * @return void + */ + public function preDispatch() + { + Benchmark::measure('Action::preDispatch()'); + if (! $this->allowAccess) { + $this->_request->setModuleName('default') + ->setControllerName('authentication') + ->setActionName('login') + ->setDispatched(false); + return; + } + + $this->view->action_name = $this->action_name; + $this->view->controller_name = $this->controller_name; + $this->view->module_name = $this->module_name; + + //$this->quickRedirect('/authentication/login?a=e'); + } + + public function redirectNow($url, array $params = array()) + { + $this->_helper->Redirector->gotoUrlAndExit($url); + } + + public function handlesAuthentication() + { + return $this->handlesAuthentication; + } + + /** + * Render our benchmark + * + * @return string + */ + protected function renderBenchmark() + { + return '
'
+             . Benchmark::renderToHtml()
+             . '
'; + } + + /** + * After dispatch happend we are going to do some automagic stuff + * + * - Benchmark is completed and rendered + * - Notifications will be collected here + * - Layout is disabled for XHR requests + * - TODO: Headers with required JS and other things will be created + * for XHR requests + * + * @return void + */ + public function postDispatch() + { + Benchmark::measure('Action::postDispatch()'); + + + // TODO: Move this elsewhere, this is just an ugly test: + if ($this->_request->getParam('filetype') === 'pdf') { + + // Snippet stolen from less compiler in public/css.php: + + require_once 'vendor/lessphp/lessc.inc.php'; + $less = new \lessc; + $cssdir = dirname(ICINGA_LIBDIR) . '/public/css'; + // TODO: We need a way to retrieve public dir, even if located elsewhere + + $css = $less->compileFile($cssdir . '/pdfprint.less'); +/* + foreach ($app->moduleManager()->getLoadedModules() as $name => $module) { + if ($module->hasCss()) { + $css .= $less->compile( + '.icinga-module.module-' + . $name + . " {\n" + . file_get_contents($module->getCssFilename()) + . "}\n\n" + ); + } + } +*/ + + // END of CSS test + + $this->render( + null, + $this->_helper->viewRenderer->getResponseSegment(), + $this->_helper->viewRenderer->getNoController() + ); + $html = '' . (string) $this->getResponse(); + + $pdf = new \Icinga\Pdf\File(); + $pdf->AddPage(); + $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); + $pdf->Output('docs.pdf', 'I'); + exit; + } + // END of PDF test + + + if ($this->_request->isXmlHttpRequest()) { + if ($this->replaceLayout || $this->_getParam('_render') === 'body') { + $this->_helper->layout()->setLayout('just-the-body'); + header('X-Icinga-Target: body'); + } else { + $this->_helper->layout()->setLayout('inline'); + } + } + $notification = Notification::getInstance(); + if ($notification->hasMessages()) { + $nhtml = '
    '; + foreach ($notification->getMessages() as $msg) { + $nhtml .= '
  • [' + . $msg->type + . '] ' + . htmlspecialchars($msg->message); + } + $nhtml .= '
'; + $this->getResponse()->append('notification', $nhtml); + } + + if (Session::getInstance()->show_benchmark) { + Benchmark::measure('Response ready'); + $this->getResponse()->append('benchmark', $this->renderBenchmark()); + } + + } + + /** + * Whether the token parameter is valid + * + * TODO: Could this make use of Icinga\Web\Session once done? + * + * @param int $maxAge Max allowed token age + * @param string $sessionId A specific session id (useful for tests?) + * + * return bool + */ + public function hasValidToken($maxAge = 600, $sessionId = null) + { + $sessionId = $sessionId ? $sessionId : session_id(); + $seed = $this->_getParam('seed'); + if (! is_numeric($seed)) { + return false; + } + + // Remove quantitized timestamp portion so maxAge applies + $seed -= (intval(time() / $maxAge) * $maxAge); + $token = $this->_getParam('token'); + return $token === hash('sha256', $sessionId . $seed); + } + + /** + * Get a new seed/token pair + * + * TODO: Could this make use of Icinga\Web\Session once done? + * + * @param int $maxAge Max allowed token age + * @param string $sessionId A specific session id (useful for tests?) + * + * return array + */ + public function getSeedTokenPair($maxAge = 600, $sessionId = null) + { + $sessionId = $sessionId ? $sessionId : session_id(); + $seed = mt_rand(); + $hash = hash('sha256', $sessionId . $seed); + + // Add quantitized timestamp portion to apply maxAge + $seed += (intval(time() / $maxAge) * $maxAge); + return array($seed, $hash); + } +} diff --git a/library/Icinga/Web/Hook.php b/library/Icinga/Web/Hook.php new file mode 100755 index 000000000..3a544c2d7 --- /dev/null +++ b/library/Icinga/Web/Hook.php @@ -0,0 +1,90 @@ +getMessage() + ); + unset(self::$hooks[$name][$key]); + return null; + } + self::assertValidHook($instance,$name); + self::$instances[$name][$key] = $instance; + return $instance; + } + + private static function assertValidHook(&$instance, $name) + { + $base_class = self::$BASE_NS.ucfirst($name); + if (! $instance instanceof $base_class) { + throw new ProgrammingError(sprintf( + '%s is not an instance of %s', + get_class($instance), + $base_class + )); + } + } + + public static function all($name) + { + if (!self::has($name)) { + return array(); + } + foreach (self::$hooks[$name] as $key=>$hook) { + if(self::createInstance($name,$key) === null) + return array(); + } + return self::$instances[$name]; + } + + public static function first($name) + { + return self::createInstance($name,key(self::$hooks[$name])); + } + + public static function register($name, $key, $class) + { + self::$hooks[$name][$key] = $class; + } +} + diff --git a/tests/php/library/Icinga/Web/.ActionControllerTest.php.swp b/tests/php/library/Icinga/Web/.ActionControllerTest.php.swp new file mode 100644 index 0000000000000000000000000000000000000000..62f393d16527e9f3aa8aa749a33283a82708c44e GIT binary patch literal 12288 zcmeI2&2Jk;7{;fFmO^Ryk~n}sQ{y)4$o9r|OOZlCqliKeg+S6&fGab$$M!bs-R;b* z(2%?I_!6I9{8T3+udYD=w9^gHB+(PF*muVV{p$?MrA}Xeh_` z7JW|%-*vfIfpHPGLtahMC}0%WQGrmk3Qrv+m2zoD+dh#wP9J*k;*Q;nfl2NhsVLgw!zOEfB*mOK0Ve0P|TcfeKfA}E0q;D@^i`2u_dwtxf` zPyl&w1RMmn4iRz#^uQ}%7TDkyX!kw%5WEMpmilfkqkvJsD6lsL-XIiL{8N{*s^iGQ zJd=_Ob&k1`TUizzk1EV}m|SJ9(_Hb_xM!8}8GWcKlgrH#-BVxEZBQbKc8wLZATrG%p*yWy*788PPSOX*1k;=+O z9b?})I!6aR+gvTfXEgF9=7@Ck>EIJ%bo2N+jn9$v&FFbSSa+QUZG~Q> zEY&(*VAmGf+{%&2VlNZ&r(7EE@{8I6uTX@8+Mv1TFJ-ZRkq(cCTf~`DJNda`|?ss zyp;CfWq%vlGBkURdtCJAcat<+!S`(mx8cg#g5 zIU(bzNpKYkk7k>^#X?u5>rro6{l+AHiMD(#8!&i%0gf-Hwoc1(klV1;PqxhEqTMAB zzTyqVo9QN9Lk?Kd9<9`Ieh`h`^+VUq(apcwz!6fEQE-pL8P5VE*(u9lH}JNZ<^w(0 zNmun%C|XO;FxOt@GVndgYYTO$1adgdilizDHcz7*NVAUDLpDWf)Dgycv9H5ermEE% z@To-DP}YPeUJWsOBM*#f`p+Xe@IW{{rZ{qIq{R5z(AjdZKH?+_P3g1^=-GfZ)>xZU zy-H*?KEhqS&}&rFfor?Pu?44{qFy(t4##4N!maknt+O*0 zLYJ$K-;@x|b^UdmET^HQ%BQU7J55^meWhg%0<`F!EBHH#;^X;>GU_=EPCE!bPwSzg zO`l6|3Jt9mR#_BNop)Hz@k2rV7O|(N3DHqHg4cYn!L6xc(Z=U*9A4K4*Qc=Qjds_o w!%js?j=cA6Nhmo@)Wm`KYOH%|PFer;oh2S#D2aI(n}kW-X7P>u8GVlaPtNy%>Hq)$ literal 0 HcmV?d00001 diff --git a/tests/php/library/Icinga/Web/ActionControllerTest.php b/tests/php/library/Icinga/Web/ActionControllerTest.php index a8c4ea473..44686595e 100755 --- a/tests/php/library/Icinga/Web/ActionControllerTest.php +++ b/tests/php/library/Icinga/Web/ActionControllerTest.php @@ -4,7 +4,7 @@ namespace Tests\Icinga\Web\ActionController; use Icinga\Web\ActionController as Action; require_once('Zend/Controller/Action.php'); -require_once('../library/Icinga/Web/ActionController.php'); +require_once('../../library/Icinga/Web/ActionController.php'); /** * This is not a nice hack, but doesn't affect the behaviour of diff --git a/tests/php/library/Icinga/Web/HookTest.php b/tests/php/library/Icinga/Web/HookTest.php index 3dc719a48..c10ec2fad 100644 --- a/tests/php/library/Icinga/Web/HookTest.php +++ b/tests/php/library/Icinga/Web/HookTest.php @@ -7,8 +7,8 @@ namespace Tests\Icinga\Web; * Created Fri, 22 Mar 2013 09:44:40 +0000 * **/ -require_once("../library/Icinga/Exception/ProgrammingError.php"); -require_once("../library/Icinga/Web/Hook.php"); +require_once("../../library/Icinga/Exception/ProgrammingError.php"); +require_once("../../library/Icinga/Web/Hook.php"); use Icinga\Web\Hook as Hook; class Base