Add tested ldap and web libraries

refs #4212
This commit is contained in:
Jannis Moßhammer 2013-06-03 17:02:08 +02:00
parent 2a9d7aa187
commit aab69a41e8
11 changed files with 1457 additions and 3 deletions

View File

@ -0,0 +1,376 @@
<?php
namespace Icinga\Protocol\Ldap;
use Icinga\Application\Platform;
use Icinga\Application\Config;
use Icinga\Application\Logger as Log;
/**
* Connection class
*
* @package Icinga\Protocol\Ldap
*/
/**
* Backend class managing all the LDAP stuff for you.
*
* Usage example:
*
* <code>
* $lconf = new Connection((object) array(
* 'hostname' => 'localhost',
* 'root_dn' => 'dc=monitoring,dc=...',
* 'bind_dn' => 'cn=Mangager,dc=monitoring,dc=...',
* 'bind_pw' => '***'
* ));
* </code>
*
* @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.org>
* @author Icinga-Web Team <info@icinga.org>
* @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)
));
}
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Icinga\Protocol\Ldap;
class Exception extends \Exception
{
}

View File

@ -0,0 +1,115 @@
<?php
namespace Icinga\Protocol\Ldap;
/**
* LdapUtils class
*
* @package Icinga\Protocol\Ldap
*/
/**
* This class provides useful LDAP-related functions
*
* @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.org>
* @author Icinga-Web Team <info@icinga.org>
* @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;
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Icinga\Protocol\Ldap;
/**
* Node class
*
* @package Icinga\Protocol\Ldap
*/
/**
* This class represents an LDAP node object
*
* @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.org>
* @author Icinga-Web Team <info@icinga.org>
* @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();
}
}

View File

@ -0,0 +1,314 @@
<?php
namespace Icinga\Protocol\Ldap;
/**
* Search class
*
* @package Icinga\Protocol\Ldap
*/
/**
* Search abstraction class
*
* Usage example:
*
* <code>
* $connection->select()->from('user')->where('sAMAccountName = ?', 'icinga');
* </code>
*
* @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.org>
* @author Icinga-Web Team <info@icinga.org>
* @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);
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace Icinga\Protocol\Ldap;
/**
* Root class
*
* @package Icinga\Protocol\Ldap
*/
/**
* This class is a special node object, representing your connections root node
*
* @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.org>
* @author Icinga-Web Team <info@icinga.org>
* @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);
}
}

View File

@ -0,0 +1,350 @@
<?php
/**
* Action controller
*/
namespace Icinga\Web;
use Icinga\Authentication\Auth;
use Icinga\Application\Benchmark;
use Icinga\Exception;
use Icinga\Application\Config;
use Icinga\Web\Notification;
use Zend_Layout as ZfLayout;
use Zend_Controller_Action as ZfController;
use Zend_Controller_Request_Abstract as ZfRequest;
use Zend_Controller_Response_Abstract as ZfResponse;
use Zend_Controller_Action_HelperBroker as ZfActionHelper;
/**
* Base class for all core action controllers
*
* All Icinga Web core controllers should extend this class
*
* @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.org>
* @author Icinga-Web Team <info@icinga.org>
* @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 '<pre class="benchmark">'
. Benchmark::renderToHtml()
. '</pre>';
}
/**
* 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 = '<style>' . $css . '</style>' . (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 = '<ul class="notification">';
foreach ($notification->getMessages() as $msg) {
$nhtml .= '<li>['
. $msg->type
. '] '
. htmlspecialchars($msg->message);
}
$nhtml .= '</ul>';
$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);
}
}

90
library/Icinga/Web/Hook.php Executable file
View File

@ -0,0 +1,90 @@
<?php
namespace Icinga\Web;
use Icinga\Application\Logger as Log;
use Icinga\Exception\ProgrammingError;
class Hook
{
protected static $hooks = array();
protected static $instances = array();
public static $BASE_NS = 'Icinga\\Web\\Hook\\';
public static function clean()
{
self::$hooks = array();
self::$instances = array();
self::$BASE_NS = 'Icinga\\Web\\Hook\\';
}
public static function has($name,$key=null)
{
if ($key !== null) {
return isset(self::$hooks[$name][$key]);
} else {
return isset(self::$hooks[$name]);
}
}
public static function createInstance($name,$key)
{
if (!self::has($name,$key)) {
return null;
}
if (isset(self::$instances[$name][$key])) {
return self::$instances[$name][$key];
}
$class = self::$hooks[$name][$key];
try {
$instance = new $class();
} catch (\Exception $e) {
Log::debug(
'Hook "%s" (%s) (%s) failed, will be unloaded: %s',
$name,
$key,
$class,
$e->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;
}
}

View File

@ -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

View File

@ -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