summaryrefslogtreecommitdiffstats
path: root/library/Director/Web/Controller
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Director/Web/Controller/ActionController.php253
-rw-r--r--library/Director/Web/Controller/BranchHelper.php76
-rw-r--r--library/Director/Web/Controller/Extension/CoreApi.php46
-rw-r--r--library/Director/Web/Controller/Extension/DirectorDb.php160
-rw-r--r--library/Director/Web/Controller/Extension/ObjectRestrictions.php48
-rw-r--r--library/Director/Web/Controller/Extension/RestApi.php114
-rw-r--r--library/Director/Web/Controller/Extension/SingleObjectApiHandler.php236
-rw-r--r--library/Director/Web/Controller/ObjectController.php733
-rw-r--r--library/Director/Web/Controller/ObjectsController.php548
-rw-r--r--library/Director/Web/Controller/TemplateController.php243
10 files changed, 2457 insertions, 0 deletions
diff --git a/library/Director/Web/Controller/ActionController.php b/library/Director/Web/Controller/ActionController.php
new file mode 100644
index 0000000..6282a16
--- /dev/null
+++ b/library/Director/Web/Controller/ActionController.php
@@ -0,0 +1,253 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller;
+
+use gipfl\Translation\StaticTranslator;
+use Icinga\Application\Benchmark;
+use Icinga\Data\Paginatable;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Monitoring;
+use Icinga\Module\Director\Web\Controller\Extension\CoreApi;
+use Icinga\Module\Director\Web\Controller\Extension\DirectorDb;
+use Icinga\Module\Director\Web\Controller\Extension\RestApi;
+use Icinga\Module\Director\Web\Window;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Controller;
+use Icinga\Web\UrlParams;
+use InvalidArgumentException;
+use gipfl\IcingaWeb2\Translator;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Widget\ControlsAndContent;
+use gipfl\IcingaWeb2\Controller\Extension\ControlsAndContentHelper;
+use gipfl\IcingaWeb2\Zf1\SimpleViewRenderer;
+use GuzzleHttp\Psr7\ServerRequest;
+use Psr\Http\Message\ServerRequestInterface;
+
+abstract class ActionController extends Controller implements ControlsAndContent
+{
+ use DirectorDb;
+ use CoreApi;
+ use RestApi;
+ use ControlsAndContentHelper;
+
+ protected $isApified = false;
+
+ /** @var UrlParams Hint for IDE, somehow does not work in web */
+ protected $params;
+
+ /** @var Monitoring */
+ private $monitoring;
+
+ /**
+ * @throws SecurityException
+ * @throws \Icinga\Exception\AuthenticationException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function init()
+ {
+ if (! $this->getRequest()->isApiRequest()
+ && $this->Config()->get('frontend', 'disabled', 'no') === 'yes'
+ ) {
+ throw new NotFoundError('Not found');
+ }
+ $this->initializeTranslator();
+ Benchmark::measure('Director base Controller init()');
+ $this->checkForRestApiRequest();
+ $this->checkDirectorPermissions();
+ $this->checkSpecialDirectorPermissions();
+ }
+
+ protected function initializeTranslator()
+ {
+ StaticTranslator::set(new Translator('director'));
+ }
+
+ public function getAuth()
+ {
+ return $this->Auth();
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ * @return Window
+ */
+ public function Window()
+ {
+ // @codingStandardsIgnoreEnd
+ if ($this->window === null) {
+ $this->window = new Window(
+ $this->_request->getHeader('X-Icinga-WindowId', Window::UNDEFINED)
+ );
+ }
+ return $this->window;
+ }
+
+ /**
+ * @throws SecurityException
+ */
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/admin');
+ }
+
+ /**
+ * @throws SecurityException
+ */
+ protected function checkSpecialDirectorPermissions()
+ {
+ if ($this->params->get('format') === 'sql') {
+ $this->assertPermission('director/showsql');
+ }
+ }
+
+ /**
+ * Assert that the current user has one of the given permission
+ *
+ * @param array $permissions Permission name list
+ *
+ * @return $this
+ * @throws SecurityException If the current user lacks the given permission
+ */
+ protected function assertOneOfPermissions($permissions)
+ {
+ $auth = $this->Auth();
+
+ foreach ($permissions as $permission) {
+ if ($auth->hasPermission($permission)) {
+ return $this;
+ }
+ }
+
+ throw new SecurityException(
+ 'Got none of the following permissions: %s',
+ implode(', ', $permissions)
+ );
+ }
+
+ /**
+ * @param int $interval
+ * @return $this
+ */
+ public function setAutorefreshInterval($interval)
+ {
+ if (! $this->getRequest()->isApiRequest()) {
+ try {
+ parent::setAutorefreshInterval($interval);
+ } catch (ProgrammingError $e) {
+ throw new InvalidArgumentException($e->getMessage());
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return ServerRequestInterface
+ */
+ protected function getServerRequest()
+ {
+ return ServerRequest::fromGlobals();
+ }
+
+ protected function applyPaginationLimits(Paginatable $paginatable, $limit = 25, $offset = null)
+ {
+ $limit = $this->params->get('limit', $limit);
+ $page = $this->params->get('page', $offset);
+
+ $paginatable->limit($limit, $page > 0 ? ($page - 1) * $limit : 0);
+
+ return $paginatable;
+ }
+
+ protected function addAddLink($title, $url, $urlParams = null, $target = '_next')
+ {
+ $this->actions()->add(Link::create(
+ $this->translate('Add'),
+ $url,
+ $urlParams,
+ [
+ 'class' => 'icon-plus',
+ 'title' => $title,
+ 'data-base-target' => $target
+ ]
+ ));
+
+ return $this;
+ }
+
+ protected function addBackLink($url, $urlParams = null)
+ {
+ $this->actions()->add(new Link(
+ $this->translate('back'),
+ $url,
+ $urlParams,
+ ['class' => 'icon-left-big']
+ ));
+
+ return $this;
+ }
+
+ protected function sendUnsupportedMethod()
+ {
+ $method = strtoupper($this->getRequest()->getMethod()) ;
+ $response = $this->getResponse();
+ $this->sendJsonError($response, sprintf(
+ 'Method %s is not supported',
+ $method
+ ), 422); // TODO: check response code
+ }
+
+ /**
+ * @param string $permission
+ * @return $this
+ * @throws SecurityException
+ */
+ public function assertPermission($permission)
+ {
+ parent::assertPermission($permission);
+ return $this;
+ }
+
+ public function postDispatch()
+ {
+ Benchmark::measure('Director postDispatch');
+ if ($this->view->content || $this->view->controls) {
+ $viewRenderer = new SimpleViewRenderer();
+ $viewRenderer->replaceZendViewRenderer();
+ $this->view = $viewRenderer->view;
+ // Hint -> $this->view->compact is the only way since v2.8.0
+ if ($this->view->compact || $this->getOriginalUrl()->getParam('view') === 'compact') {
+ if ($this->view->controls) {
+ $this->controls()->getAttributes()->add('style', 'display: none;');
+ }
+ }
+ } else {
+ $viewRenderer = null;
+ }
+
+ $cType = $this->getResponse()->getHeader('Content-Type', true);
+ if ($this->getRequest()->isApiRequest() || ($cType !== null && $cType !== 'text/html')) {
+ $this->_helper->layout()->disableLayout();
+ if ($viewRenderer) {
+ $viewRenderer->disable();
+ } else {
+ $this->_helper->viewRenderer->setNoRender(true);
+ }
+ }
+
+ parent::postDispatch(); // TODO: Change the autogenerated stub
+ }
+
+ /**
+ * @return Monitoring
+ */
+ protected function monitoring()
+ {
+ if ($this->monitoring === null) {
+ $this->monitoring = new Monitoring;
+ }
+
+ return $this->monitoring;
+ }
+}
diff --git a/library/Director/Web/Controller/BranchHelper.php b/library/Director/Web/Controller/BranchHelper.php
new file mode 100644
index 0000000..ac2a480
--- /dev/null
+++ b/library/Director/Web/Controller/BranchHelper.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller;
+
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchStore;
+use Icinga\Module\Director\Db\Branch\BranchSupport;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Widget\NotInBranchedHint;
+
+trait BranchHelper
+{
+ /** @var Branch */
+ protected $branch;
+
+ /** @var BranchStore */
+ protected $branchStore;
+
+ /**
+ * @return false|\Ramsey\Uuid\UuidInterface
+ */
+ protected function getBranchUuid()
+ {
+ return $this->getBranch()->getUuid();
+ }
+
+ protected function getBranch()
+ {
+ if ($this->branch === null) {
+ /** @var ActionController $this */
+ $this->branch = Branch::forRequest($this->getRequest(), $this->getBranchStore(), $this->Auth());
+ }
+
+ return $this->branch;
+ }
+
+ /**
+ * @return BranchStore
+ */
+ protected function getBranchStore()
+ {
+ if ($this->branchStore === null) {
+ $this->branchStore = new BranchStore($this->db());
+ }
+
+ return $this->branchStore;
+ }
+
+ protected function hasBranch()
+ {
+ return $this->getBranchUuid() !== null;
+ }
+
+ protected function enableStaticObjectLoader($table)
+ {
+ if (BranchSupport::existsForTableName($table)) {
+ IcingaObject::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch()));
+ }
+ }
+
+ /**
+ * @param string $subject
+ * @return bool
+ */
+ protected function showNotInBranch($subject)
+ {
+ if ($this->getBranch()->isBranch()) {
+ $this->content()->add(new NotInBranchedHint($subject, $this->getBranch(), $this->Auth()));
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/library/Director/Web/Controller/Extension/CoreApi.php b/library/Director/Web/Controller/Extension/CoreApi.php
new file mode 100644
index 0000000..75cba50
--- /dev/null
+++ b/library/Director/Web/Controller/Extension/CoreApi.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller\Extension;
+
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Module\Director\Core\CoreApi as Api;
+
+trait CoreApi
+{
+ /** @var Api */
+ private $api;
+
+ /**
+ * @return Api|null
+ */
+ public function getApiIfAvailable()
+ {
+ if ($this->api === null) {
+ if ($this->db()->hasDeploymentEndpoint()) {
+ $endpoint = $this->db()->getDeploymentEndpoint();
+ $this->api = $endpoint->api();
+ }
+ }
+
+ return $this->api;
+ }
+
+ /**
+ * @param string $endpointName
+ * @return Api
+ */
+ public function api($endpointName = null)
+ {
+ if ($this->api === null) {
+ if ($endpointName === null) {
+ $endpoint = $this->db()->getDeploymentEndpoint();
+ } else {
+ $endpoint = IcingaEndpoint::load($endpointName, $this->db());
+ }
+
+ $this->api = $endpoint->api();
+ }
+
+ return $this->api;
+ }
+}
diff --git a/library/Director/Web/Controller/Extension/DirectorDb.php b/library/Director/Web/Controller/Extension/DirectorDb.php
new file mode 100644
index 0000000..03bec81
--- /dev/null
+++ b/library/Director/Web/Controller/Extension/DirectorDb.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller\Extension;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Window;
+use RuntimeException;
+
+trait DirectorDb
+{
+ /** @var Db */
+ private $db;
+
+ protected function getDbResourceName()
+ {
+ if ($name = $this->getDbResourceNameFromRequest()) {
+ return $name;
+ } elseif ($name = $this->getPreferredDbResourceName()) {
+ return $name;
+ } else {
+ return $this->getFirstDbResourceName();
+ }
+ }
+
+ protected function getDbResourceNameFromRequest()
+ {
+ $param = 'dbResourceName';
+ // We shouldn't access _POST and _GET. However, this trait is used
+ // in various places - and our Request is going to be replaced anyways.
+ // So, let's not over-engineer things, this is quick & dirty:
+ if (isset($_POST[$param])) {
+ $name = $_POST[$param];
+ } elseif (isset($_GET[$param])) {
+ $name = $_GET[$param];
+ } else {
+ return null;
+ }
+
+ if (in_array($name, $this->listAllowedDbResourceNames())) {
+ return $name;
+ } else {
+ return null;
+ }
+ }
+
+ protected function getPreferredDbResourceName()
+ {
+ return $this->getWindowSessionValue('db_resource');
+ }
+
+ protected function getFirstDbResourceName()
+ {
+ $names = $this->listAllowedDbResourceNames();
+ if (empty($names)) {
+ return null;
+ } else {
+ return array_shift($names);
+ }
+ }
+
+ protected function listAllowedDbResourceNames()
+ {
+ /** @var \Icinga\Authentication\Auth $auth */
+ $auth = $this->Auth();
+
+ $available = $this->listAvailableDbResourceNames();
+ if ($resourceNames = $auth->getRestrictions('director/db_resource')) {
+ $names = [];
+ foreach ($resourceNames as $rNames) {
+ foreach ($this->splitList($rNames) as $name) {
+ if (array_key_exists($name, $available)) {
+ $names[] = $name;
+ }
+ }
+ }
+
+ return $names;
+ } else {
+ return $available;
+ }
+ }
+
+ /**
+ * @param string $string
+ * @return array
+ */
+ protected function splitList($string)
+ {
+ return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY);
+ }
+
+ protected function isMultiDbSetup()
+ {
+ return count($this->listAvailableDbResourceNames()) > 1;
+ }
+
+ /**
+ * @return array
+ */
+ protected function listAvailableDbResourceNames()
+ {
+ /** @var \Icinga\Application\Config $config */
+ $config = $this->Config();
+ $resources = $config->get('db', 'resources');
+ if ($resources === null) {
+ $resource = $config->get('db', 'resource');
+ if ($resource === null) {
+ return [];
+ } else {
+ return [$resource => $resource];
+ }
+ } else {
+ $resources = $this->splitList($resources);
+ $resources = array_combine($resources, $resources);
+ // natsort doesn't work!?
+ ksort($resources, SORT_NATURAL);
+ if ($resource = $config->get('db', 'resource')) {
+ unset($resources[$resource]);
+ $resources = [$resource => $resource] + $resources;
+ }
+
+ return $resources;
+ }
+ }
+
+ protected function getWindowSessionValue($value, $default = null)
+ {
+ /** @var Window $window */
+ $window = $this->Window();
+ /** @var \Icinga\Web\Session\SessionNamespace $session */
+ $session = $window->getSessionNamespace('director');
+
+ return $session->get($value, $default);
+ }
+
+ /**
+ *
+ * @return Db
+ */
+ public function db()
+ {
+ if ($this->db === null) {
+ $resourceName = $this->getDbResourceName();
+ if ($resourceName) {
+ $this->db = Db::fromResourceName($resourceName);
+ } elseif ($this instanceof ActionController) {
+ if ($this->getRequest()->isApiRequest()) {
+ throw new RuntimeException('Icinga Director is not correctly configured');
+ } else {
+ $this->redirectNow('director');
+ }
+ } else {
+ throw new RuntimeException('Icinga Director is not correctly configured');
+ }
+ }
+
+ return $this->db;
+ }
+}
diff --git a/library/Director/Web/Controller/Extension/ObjectRestrictions.php b/library/Director/Web/Controller/Extension/ObjectRestrictions.php
new file mode 100644
index 0000000..bedb3f1
--- /dev/null
+++ b/library/Director/Web/Controller/Extension/ObjectRestrictions.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller\Extension;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Restriction\HostgroupRestriction;
+use Icinga\Module\Director\Restriction\ObjectRestriction;
+
+trait ObjectRestrictions
+{
+ /** @var ObjectRestriction[] */
+ private $objectRestrictions;
+
+ /**
+ * @return ObjectRestriction[]
+ */
+ public function getObjectRestrictions()
+ {
+ if ($this->objectRestrictions === null) {
+ $this->objectRestrictions = $this->loadObjectRestrictions($this->db(), $this->Auth());
+ }
+
+ return $this->objectRestrictions;
+ }
+
+ /**
+ * @return ObjectRestriction[]
+ */
+ protected function loadObjectRestrictions(Db $db, Auth $auth)
+ {
+ return [
+ new HostgroupRestriction($db, $auth)
+ ];
+ }
+
+ public function allowsObject(IcingaObject $object)
+ {
+ foreach ($this->getObjectRestrictions() as $restriction) {
+ if (! $restriction->allows($object)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/library/Director/Web/Controller/Extension/RestApi.php b/library/Director/Web/Controller/Extension/RestApi.php
new file mode 100644
index 0000000..3158f49
--- /dev/null
+++ b/library/Director/Web/Controller/Extension/RestApi.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller\Extension;
+
+use Icinga\Exception\AuthenticationException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Exception\JsonException;
+use Icinga\Web\Response;
+use InvalidArgumentException;
+use Zend_Controller_Response_Exception;
+
+trait RestApi
+{
+ protected function isApified()
+ {
+ if (property_exists($this, 'isApified')) {
+ return $this->isApified;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ protected function sendNotFoundForRestApi()
+ {
+ /** @var \Icinga\Web\Request $request */
+ $request = $this->getRequest();
+ if ($request->isApiRequest()) {
+ $this->sendJsonError($this->getResponse(), 'Not found', 404);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ protected function sendNotFoundUnlessRestApi()
+ {
+ /** @var \Icinga\Web\Request $request */
+ $request = $this->getRequest();
+ if ($request->isApiRequest()) {
+ return false;
+ } else {
+ $this->sendJsonError($this->getResponse(), 'Not found', 404);
+ return true;
+ }
+ }
+
+ /**
+ * @throws AuthenticationException
+ */
+ protected function assertApiPermission()
+ {
+ if (! $this->hasPermission('director/api')) {
+ throw new AuthenticationException('You are not allowed to access this API');
+ }
+ }
+
+ /**
+ * @throws AuthenticationException
+ * @throws NotFoundError
+ */
+ protected function checkForRestApiRequest()
+ {
+ /** @var \Icinga\Web\Request $request */
+ $request = $this->getRequest();
+ if ($request->isApiRequest()) {
+ $this->assertApiPermission();
+ if (! $this->isApified()) {
+ throw new NotFoundError('No such API endpoint found');
+ }
+ }
+ }
+
+ /**
+ * @param Response $response
+ * @param $object
+ */
+ protected function sendJson(Response $response, $object)
+ {
+ $response->setHeader('Content-Type', 'application/json', true);
+ echo json_encode($object, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
+ }
+
+ /**
+ * @param Response $response
+ * @param string $message
+ * @param int|null $code
+ */
+ protected function sendJsonError(Response $response, $message, $code = null)
+ {
+ if ($code !== null) {
+ try {
+ $response->setHttpResponseCode((int) $code);
+ } catch (Zend_Controller_Response_Exception $e) {
+ throw new InvalidArgumentException($e->getMessage(), 0, $e);
+ }
+ }
+
+ $this->sendJson($response, (object) ['error' => $message]);
+ }
+
+ /**
+ * @return string
+ */
+ protected function getLastJsonError()
+ {
+ return JsonException::getJsonErrorMessage(json_last_error());
+ }
+}
diff --git a/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php b/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php
new file mode 100644
index 0000000..bc51548
--- /dev/null
+++ b/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php
@@ -0,0 +1,236 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller\Extension;
+
+use Exception;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Forms\IcingaDeleteObjectForm;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Web\Request;
+use Icinga\Web\Response;
+
+class SingleObjectApiHandler
+{
+ use DirectorDb;
+
+ /** @var IcingaObject */
+ private $object;
+
+ /** @var string */
+ private $type;
+
+ /** @var Request */
+ private $request;
+
+ /** @var Response */
+ private $response;
+
+ /** @var \Icinga\Web\UrlParams */
+ private $params;
+
+ public function __construct($type, Request $request, Response $response)
+ {
+ $this->type = $type;
+ $this->request = $request;
+ $this->response = $response;
+ $this->params = $request->getUrl()->getParams();
+ }
+
+ public function runFailSafe()
+ {
+ try {
+ $this->loadObject();
+ $this->run();
+ } catch (NotFoundError $e) {
+ $this->sendJsonError($e->getMessage(), 404);
+ } catch (Exception $e) {
+ $response = $this->response;
+ if ($response->getHttpResponseCode() === 200) {
+ $response->setHttpResponseCode(500);
+ }
+
+ $this->sendJsonError($e->getMessage());
+ }
+ }
+
+ protected function retrieveObject()
+ {
+ $this->requireObject();
+ $this->sendJson(
+ $this->object->toPlainObject(
+ $this->params->shift('resolved'),
+ ! $this->params->shift('withNull'),
+ $this->params->shift('properties')
+ )
+ );
+ }
+
+ protected function deleteObject()
+ {
+ $this->requireObject();
+ $obj = $this->object->toPlainObject(false, true);
+ $form = new IcingaDeleteObjectForm();
+ $form->setObject($this->object)
+ ->setRequest($this->request)
+ ->onSuccess();
+
+ $this->sendJson($obj);
+ }
+
+ protected function storeObject()
+ {
+ $data = json_decode($this->request->getRawBody());
+
+ if ($data === null) {
+ $this->response->setHttpResponseCode(400);
+ throw new IcingaException(
+ 'Invalid JSON: %s' . $this->request->getRawBody(),
+ $this->getLastJsonError()
+ );
+ } else {
+ $data = (array) $data;
+ }
+
+ if ($object = $this->object) {
+ if ($this->request->getMethod() === 'POST') {
+ $object->setProperties($data);
+ } else {
+ $data = array_merge([
+ 'object_type' => $object->object_type,
+ 'object_name' => $object->object_name
+ ], $data);
+ $object->replaceWith(
+ IcingaObject::createByType($this->type, $data, $db)
+ );
+ }
+ } else {
+ $object = IcingaObject::createByType($this->type, $data, $db);
+ }
+
+ if ($object->hasBeenModified()) {
+ $status = $object->hasBeenLoadedFromDb() ? 200 : 201;
+ $object->store();
+ $this->response->setHttpResponseCode($status);
+ } else {
+ $this->response->setHttpResponseCode(304);
+ }
+
+ $this->sendJson($object->toPlainObject(false, true));
+ }
+
+ public function run()
+ {
+ switch ($this->request->getMethod()) {
+ case 'DELETE':
+ $this->deleteObject();
+ break;
+
+ case 'POST':
+ case 'PUT':
+ $this->storeObject();
+ break;
+
+ case 'GET':
+ $this->retrieveObject();
+ break;
+
+ default:
+ $this->response->setHttpResponseCode(400);
+ throw new IcingaException(
+ 'Unsupported method: %s',
+ $this->request->getMethod()
+ );
+ }
+ }
+
+ protected function requireObject()
+ {
+ if (! $this->object) {
+ $this->response->setHttpResponseCode(404);
+ if (! $this->params->get('name')) {
+ throw new NotFoundError('You need to pass a "name" parameter to access a specific object');
+ } else {
+ throw new NotFoundError('No such object available');
+ }
+ }
+ }
+
+ // TODO: just return json_last_error_msg() for PHP >= 5.5.0
+ protected function getLastJsonError()
+ {
+ switch (json_last_error()) {
+ case JSON_ERROR_DEPTH:
+ return 'The maximum stack depth has been exceeded';
+ case JSON_ERROR_CTRL_CHAR:
+ return 'Control character error, possibly incorrectly encoded';
+ case JSON_ERROR_STATE_MISMATCH:
+ return 'Invalid or malformed JSON';
+ case JSON_ERROR_SYNTAX:
+ return 'Syntax error';
+ case JSON_ERROR_UTF8:
+ return 'Malformed UTF-8 characters, possibly incorrectly encoded';
+ default:
+ return 'An error occured when parsing a JSON string';
+ }
+ }
+
+ protected function sendJson($object)
+ {
+ $this->response->setHeader('Content-Type', 'application/json', true);
+ $this->_helper->layout()->disableLayout();
+ $this->_helper->viewRenderer->setNoRender(true);
+ echo json_encode($object, JSON_PRETTY_PRINT) . "\n";
+ }
+
+ protected function sendJsonError($message, $code = null)
+ {
+ $response = $this->response;
+
+ if ($code !== null) {
+ $response->setHttpResponseCode((int) $code);
+ }
+
+ $this->sendJson((object) ['error' => $message]);
+ }
+
+ protected function loadObject()
+ {
+ if ($this->object === null) {
+ if ($name = $this->params->get('name')) {
+ $this->object = IcingaObject::loadByType(
+ $this->type,
+ $name,
+ $this->db()
+ );
+
+ if (! $this->allowsObject($this->object)) {
+ $this->object = null;
+ throw new NotFoundError('No such object available');
+ }
+ } elseif ($id = $this->params->get('id')) {
+ $this->object = IcingaObject::loadByType(
+ $this->type,
+ (int) $id,
+ $this->db()
+ );
+ } elseif ($this->request->isApiRequest()) {
+ if ($this->request->isGet()) {
+ $this->response->setHttpResponseCode(422);
+
+ throw new InvalidPropertyException(
+ 'Cannot load object, missing parameters'
+ );
+ }
+ }
+ }
+
+ return $this->object;
+ }
+
+ protected function allowsObject(IcingaObject $object)
+ {
+ return true;
+ }
+}
diff --git a/library/Director/Web/Controller/ObjectController.php b/library/Director/Web/Controller/ObjectController.php
new file mode 100644
index 0000000..0c06937
--- /dev/null
+++ b/library/Director/Web/Controller/ObjectController.php
@@ -0,0 +1,733 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchedObject;
+use Icinga\Module\Director\Db\Branch\UuidLookup;
+use Icinga\Module\Director\Deployment\DeploymentInfo;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\Forms\DeploymentLinkForm;
+use Icinga\Module\Director\Forms\IcingaCloneObjectForm;
+use Icinga\Module\Director\Forms\IcingaObjectFieldForm;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaObjectGroup;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\RestApi\IcingaObjectHandler;
+use Icinga\Module\Director\Web\Controller\Extension\ObjectRestrictions;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\ObjectPreview;
+use Icinga\Module\Director\Web\Table\ActivityLogTable;
+use Icinga\Module\Director\Web\Table\BranchActivityTable;
+use Icinga\Module\Director\Web\Table\GroupMemberTable;
+use Icinga\Module\Director\Web\Table\IcingaObjectDatafieldTable;
+use Icinga\Module\Director\Web\Tabs\ObjectTabs;
+use Icinga\Module\Director\Web\Widget\BranchedObjectHint;
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Html;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+
+abstract class ObjectController extends ActionController
+{
+ use ObjectRestrictions;
+ use BranchHelper;
+
+ /** @var IcingaObject */
+ protected $object;
+
+ /** @var bool This controller handles REST API requests */
+ protected $isApified = true;
+
+ /** @var array Allowed object types we are allowed to edit anyways */
+ protected $allowedExternals = array(
+ 'apiuser',
+ 'endpoint'
+ );
+
+ protected $type;
+
+ /** @var string|null */
+ protected $objectBaseUrl;
+
+ public function init()
+ {
+ parent::init();
+ $this->enableStaticObjectLoader($this->getTableName());
+ if ($this->getRequest()->isApiRequest()) {
+ $this->initializeRestApi();
+ } else {
+ $this->initializeWebRequest();
+ }
+ }
+
+ protected function initializeRestApi()
+ {
+ $handler = new IcingaObjectHandler($this->getRequest(), $this->getResponse(), $this->db());
+ try {
+ $this->loadOptionalObject();
+ } catch (NotFoundError $e) {
+ // Silently ignore the error, the handler will complain
+ $handler->sendJsonError($e, 404);
+ // TODO: nice shutdown
+ exit;
+ }
+
+ $handler->setApi($this->api());
+ if ($this->object) {
+ $handler->setObject($this->object);
+ }
+ $handler->dispatch();
+ // Hint: also here, hard exit. There is too much magic going on.
+ // Letting this bubble up smoothly would be "correct", but proved
+ // to be too fragile. Web 2, all kinds of pre/postDispatch magic,
+ // different view renderers - hard exit is the only safe bet right
+ // now.
+ exit;
+ }
+
+ protected function initializeWebRequest()
+ {
+ $this->loadOptionalObject();
+ if ($this->getRequest()->getActionName() === 'add') {
+ $this->addSingleTab(
+ sprintf($this->translate('Add %s'), ucfirst($this->getType())),
+ null,
+ 'add'
+ );
+ } else {
+ $this->tabs(new ObjectTabs(
+ $this->getRequest()->getControllerName(),
+ $this->getAuth(),
+ $this->object
+ ));
+ }
+ if ($this->object !== null) {
+ $this->addDeploymentLink();
+ }
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function indexAction()
+ {
+ if (! $this->getRequest()->isApiRequest()) {
+ $this->redirectToPreviewForExternals()
+ ->editAction();
+ }
+ }
+
+ public function addAction()
+ {
+ $this->tabs()->activate('add');
+ $url = sprintf('director/%ss', $this->getPluralType());
+
+ $imports = $this->params->get('imports');
+ $form = $this->loadObjectForm()
+ ->presetImports($imports)
+ ->setSuccessUrl($url);
+
+ if ($oType = $this->params->get('type', 'object')) {
+ $form->setPreferredObjectType($oType);
+ }
+ if ($oType === 'template') {
+ if ($this->showNotInBranch($this->translate('Creating Templates'))) {
+ $this->addTitle($this->translate('Create a new Template'));
+ return;
+ }
+
+ $this->addTemplate();
+ } else {
+ $this->addObject();
+ }
+ $branch = $this->getBranch();
+ if ($branch->isBranch() && ! $this->getRequest()->isApiRequest()) {
+ $this->content()->add(new BranchedObjectHint($branch, $this->Auth()));
+ }
+
+ $form->handleRequest();
+ $this->content()->add($form);
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function editAction()
+ {
+ $object = $this->requireObject();
+ $this->tabs()->activate('modify');
+ $this->addObjectTitle();
+ // Hint: Service Sets are 'templates' (as long as not being assigned to a host
+ if ($this->getTableName() !== 'icinga_service_set'
+ && $object->isTemplate()
+ && $this->showNotInBranch($this->translate('Modifying Templates'))
+ ) {
+ return;
+ }
+ if ($object->isApplyRule() && $this->showNotInBranch($this->translate('Modifying Apply Rules'))) {
+ return;
+ }
+
+ $this->addObjectForm($object)
+ ->addActionClone()
+ ->addActionUsage()
+ ->addActionBasket();
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function renderAction()
+ {
+ $this->assertTypePermission()
+ ->assertPermission('director/showconfig');
+ $this->tabs()->activate('render');
+ $preview = new ObjectPreview($this->requireObject(), $this->getRequest());
+ if ($this->object->isExternal()) {
+ $this->addActionClone();
+ }
+ $this->addActionBasket();
+ $preview->renderTo($this);
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function cloneAction()
+ {
+ $this->assertTypePermission();
+ $object = $this->requireObject();
+ $this->addTitle($this->translate('Clone: %s'), $object->getObjectName())
+ ->addBackToObjectLink();
+
+ if ($object->isTemplate() && $this->showNotInBranch($this->translate('Cloning Templates'))) {
+ return;
+ }
+
+ if ($object->isTemplate() && $this->showNotInBranch($this->translate('Cloning Apply Rules'))) {
+ return;
+ }
+
+ $form = IcingaCloneObjectForm::load()
+ ->setBranch($this->getBranch())
+ ->setObject($object)
+ ->setObjectBaseUrl($this->getObjectBaseUrl())
+ ->handleRequest();
+
+ if ($object->isExternal()) {
+ $this->tabs()->activate('render');
+ } else {
+ $this->tabs()->activate('modify');
+ }
+ $this->content()->add($form);
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function fieldsAction()
+ {
+ $this->assertPermission('director/admin');
+ $object = $this->requireObject();
+ $type = $this->getType();
+
+ $this->addTitle(
+ $this->translate('Custom fields: %s'),
+ $object->getObjectName()
+ );
+ $this->tabs()->activate('fields');
+ if ($this->showNotInBranch($this->translate('Managing Fields'))) {
+ return;
+ }
+
+ try {
+ $this->addFieldsFormAndTable($object, $type);
+ } catch (NestingError $e) {
+ $this->content()->add(Hint::error($e->getMessage()));
+ }
+ }
+
+ protected function addFieldsFormAndTable($object, $type)
+ {
+ $form = IcingaObjectFieldForm::load()
+ ->setDb($this->db())
+ ->setIcingaObject($object);
+
+ if ($id = $this->params->get('field_id')) {
+ $form->loadObject([
+ "${type}_id" => $object->id,
+ 'datafield_id' => $id
+ ]);
+
+ $this->actions()->add(Link::create(
+ $this->translate('back'),
+ $this->url()->without('field_id'),
+ null,
+ ['class' => 'icon-left-big']
+ ));
+ }
+ $form->handleRequest();
+ $this->content()->add($form);
+ $table = new IcingaObjectDatafieldTable($object);
+ $table->getAttributes()->set('data-base-target', '_self');
+ $table->renderTo($this);
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function historyAction()
+ {
+ $this
+ ->assertTypePermission()
+ ->assertPermission('director/audit')
+ ->setAutorefreshInterval(10)
+ ->tabs()->activate('history');
+
+ $name = $this->requireObject()->getObjectName();
+ $this->addTitle($this->translate('Activity Log: %s'), $name);
+
+ $db = $this->db();
+ $objectTable = $this->object->getTableName();
+ $table = (new ActivityLogTable($db))
+ ->setLastDeployedId($db->getLastDeploymentActivityLogId())
+ ->filterObject($objectTable, $name);
+ if ($host = $this->params->get('host')) {
+ $table->filterHost($host);
+ }
+ $this->showOptionalBranchActivity($table);
+ $table->renderTo($this);
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function membershipAction()
+ {
+ $object = $this->requireObject();
+ if (! $object instanceof IcingaObjectGroup) {
+ throw new NotFoundError('Not Found');
+ }
+
+ $this
+ ->addTitle($this->translate('Group membership: %s'), $object->getObjectName())
+ ->setAutorefreshInterval(15)
+ ->tabs()->activate('membership');
+
+ $type = substr($this->getType(), 0, -5);
+ GroupMemberTable::create($type, $this->db())
+ ->setGroup($object)
+ ->renderTo($this);
+ }
+
+ /**
+ * @return $this
+ * @throws NotFoundError
+ */
+ protected function addObjectTitle()
+ {
+ $object = $this->requireObject();
+ $name = $object->getObjectName();
+ if ($object->isTemplate()) {
+ $this->addTitle($this->translate('Template: %s'), $name);
+ } else {
+ $this->addTitle($name);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws NotFoundError
+ */
+ protected function addActionUsage()
+ {
+ $type = $this->getType();
+ $object = $this->requireObject();
+ if ($object->isTemplate() && $type !== 'serviceSet') {
+ $this->actions()->add([
+ Link::create(
+ $this->translate('Usage'),
+ "director/${type}template/usage",
+ ['name' => $object->getObjectName()],
+ ['class' => 'icon-sitemap']
+ )
+ ]);
+ }
+
+ return $this;
+ }
+
+ protected function addActionClone()
+ {
+ $this->actions()->add(Link::create(
+ $this->translate('Clone'),
+ $this->getObjectBaseUrl() . '/clone',
+ $this->object->getUrlParams(),
+ array('class' => 'icon-paste')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addActionBasket()
+ {
+ if ($this->hasBasketSupport()) {
+ $object = $this->object;
+ if ($object instanceof ExportInterface) {
+ if ($object instanceof IcingaCommand) {
+ if ($object->isExternal()) {
+ $type = 'ExternalCommand';
+ } elseif ($object->isTemplate()) {
+ $type = 'CommandTemplate';
+ } else {
+ $type = 'Command';
+ }
+ } elseif ($object instanceof IcingaServiceSet) {
+ $type = 'ServiceSet';
+ } elseif ($object->isTemplate()) {
+ $type = ucfirst($this->getType()) . 'Template';
+ } elseif ($object->isGroup()) {
+ $type = ucfirst($this->getType());
+ } else {
+ // Command? Sure?
+ $type = ucfirst($this->getType());
+ }
+ $this->actions()->add(Link::create(
+ $this->translate('Add to Basket'),
+ 'director/basket/add',
+ [
+ 'type' => $type,
+ 'names' => $object->getUniqueIdentifier()
+ ],
+ ['class' => 'icon-tag']
+ ));
+ }
+ }
+
+ return $this;
+ }
+
+ protected function addTemplate()
+ {
+ $this->assertPermission('director/admin');
+ $this->addTitle(
+ $this->translate('Add new Icinga %s template'),
+ $this->getTranslatedType()
+ );
+ }
+
+ protected function addObject()
+ {
+ $this->assertTypePermission();
+ $imports = $this->params->get('imports');
+ if (is_string($imports) && strlen($imports)) {
+ $this->addTitle(
+ $this->translate('Add %s: %s'),
+ $this->getTranslatedType(),
+ $imports
+ );
+ } else {
+ $this->addTitle(
+ $this->translate('Add new Icinga %s'),
+ $this->getTranslatedType()
+ );
+ }
+ }
+
+ protected function redirectToPreviewForExternals()
+ {
+ if ($this->object
+ && $this->object->isExternal()
+ && ! in_array($this->object->getShortTableName(), $this->allowedExternals)
+ ) {
+ $this->redirectNow(
+ $this->getRequest()->getUrl()->setPath(sprintf('director/%s/render', $this->getType()))
+ );
+ }
+
+ return $this;
+ }
+
+ protected function getType()
+ {
+ if ($this->type === null) {
+ // Strip final 's' and upcase an eventual 'group'
+ $this->type = preg_replace(
+ array('/group$/', '/period$/', '/argument$/', '/apiuser$/', '/set$/'),
+ array('Group', 'Period', 'Argument', 'ApiUser', 'Set'),
+ $this->getRequest()->getControllerName()
+ );
+ }
+
+ return $this->type;
+ }
+
+ protected function getPluralType()
+ {
+ return $this->getType() . 's';
+ }
+
+ protected function getTranslatedType()
+ {
+ return $this->translate(ucfirst($this->getType()));
+ }
+
+ protected function assertTypePermission()
+ {
+ $type = strtolower($this->getPluralType());
+ // TODO: Check getPluralType usage, fix it there.
+ if ($type === 'scheduleddowntimes') {
+ $type = 'scheduled-downtimes';
+ }
+
+ return $this->assertPermission("director/$type");
+ }
+
+ protected function loadOptionalObject()
+ {
+ if ($this->params->get('uuid') || null !== $this->params->get('name') || $this->params->get('id')) {
+ $this->loadObject();
+ }
+ }
+
+ /**
+ * @return ?UuidInterface
+ * @throws InvalidPropertyException
+ * @throws NotFoundError
+ */
+ protected function getUuidFromUrl()
+ {
+ $key = null;
+ if ($uuid = $this->params->get('uuid')) {
+ $key = Uuid::fromString($uuid);
+ } elseif ($id = $this->params->get('id')) {
+ $key = (int) $id;
+ } elseif (null !== ($name = $this->params->get('name'))) {
+ $key = $name;
+ }
+ if ($key === null) {
+ $request = $this->getRequest();
+ if ($request->isApiRequest() && $request->isGet()) {
+ $this->getResponse()->setHttpResponseCode(422);
+
+ throw new InvalidPropertyException(
+ 'Cannot load object, missing parameters'
+ );
+ }
+
+ return null;
+ }
+
+ return $this->requireUuid($key);
+ }
+
+ protected function loadObject()
+ {
+ if ($this->object) {
+ throw new ProgrammingError('Loading an object twice is not very efficient');
+ }
+
+ $this->object = $this->loadSpecificObject($this->getTableName(), $this->getUuidFromUrl(), true);
+ }
+
+ protected function loadSpecificObject($tableName, $key, $showHint = false)
+ {
+ $branch = $this->getBranch();
+ $branchedObject = BranchedObject::load($this->db(), $tableName, $key, $branch);
+ $object = $branchedObject->getBranchedDbObject($this->db());
+ assert($object instanceof IcingaObject);
+ $object->setBeingLoadedFromDb();
+ if (! $this->allowsObject($object)) {
+ throw new NotFoundError('No such object available');
+ }
+ if ($showHint && $branch->isBranch() && $object->isObject() && ! $this->getRequest()->isApiRequest()) {
+ $this->content()->add(new BranchedObjectHint($branch, $this->Auth(), $branchedObject));
+ }
+
+ return $object;
+ }
+
+ protected function requireUuid($key)
+ {
+ if (! $key instanceof UuidInterface) {
+ $key = UuidLookup::findUuidForKey($key, $this->getTableName(), $this->db(), $this->getBranch());
+ if ($key === null) {
+ throw new NotFoundError('No such object available');
+ }
+ }
+
+ return $key;
+ }
+
+ protected function getTableName()
+ {
+ return DbObjectTypeRegistry::tableNameByType($this->getType());
+ }
+
+ protected function addDeploymentLink()
+ {
+ try {
+ $info = new DeploymentInfo($this->db());
+ $info->setObject($this->object);
+
+ if (! $this->getRequest()->isApiRequest()) {
+ if ($this->getBranch()->isBranch()) {
+ $this->actions()->add($this->linkToMergeBranch($this->getBranch()));
+ } else {
+ $this->actions()->add(
+ DeploymentLinkForm::create(
+ $this->db(),
+ $info,
+ $this->Auth(),
+ $this->api()
+ )->handleRequest()
+ );
+ }
+ }
+ } catch (IcingaException $e) {
+ // pass (deployment may not be set up yet)
+ }
+ }
+
+ protected function linkToMergeBranch(Branch $branch)
+ {
+ $link = Branch::requireHook()->linkToBranch($branch, $this->Auth(), $this->translate('Merge'));
+ if ($link instanceof Link) {
+ $link->addAttributes(['class' => 'icon-flapping']);
+ }
+
+ return $link;
+ }
+
+ protected function addBackToObjectLink()
+ {
+ $params = [
+ 'uuid' => $this->object->getUniqueId()->toString(),
+ ];
+
+ if ($this->object instanceof IcingaService) {
+ if (($host = $this->object->get('host')) !== null) {
+ $params['host'] = $host;
+ } elseif (($set = $this->object->get('service_set')) !== null) {
+ $params['set'] = $set;
+ }
+ }
+
+ $this->actions()->add(Link::create(
+ $this->translate('back'),
+ $this->getObjectBaseUrl(),
+ $params,
+ ['class' => 'icon-left-big']
+ ));
+
+ return $this;
+ }
+
+ protected function addObjectForm(IcingaObject $object = null)
+ {
+ $form = $this->loadObjectForm($object);
+ $this->content()->add($form);
+ $form->handleRequest();
+ return $this;
+ }
+
+ protected function loadObjectForm(IcingaObject $object = null)
+ {
+ /** @var DirectorObjectForm $class */
+ $class = sprintf(
+ 'Icinga\\Module\\Director\\Forms\\Icinga%sForm',
+ ucfirst($this->getType())
+ );
+
+ $form = $class::load()
+ ->setDb($this->db())
+ ->setAuth($this->Auth());
+
+ if ($object !== null) {
+ $form->setObject($object);
+ }
+ if (true || $form->supportsBranches()) {
+ $form->setBranch($this->getBranch());
+ }
+
+ $this->onObjectFormLoaded($form);
+
+ return $form;
+ }
+
+ protected function getObjectBaseUrl()
+ {
+ return $this->objectBaseUrl ?: 'director/' . strtolower($this->getType());
+ }
+
+ protected function hasBasketSupport()
+ {
+ return $this->object->isTemplate() || $this->object->isGroup();
+ }
+
+ protected function onObjectFormLoaded(DirectorObjectForm $form)
+ {
+ }
+
+ /**
+ * @return IcingaObject
+ * @throws NotFoundError
+ */
+ protected function requireObject()
+ {
+ if (! $this->object) {
+ $this->getResponse()->setHttpResponseCode(404);
+ if (null === $this->params->get('name')) {
+ throw new NotFoundError('You need to pass a "name" parameter to access a specific object');
+ } else {
+ throw new NotFoundError('No such object available');
+ }
+ }
+
+ return $this->object;
+ }
+
+ protected function showOptionalBranchActivity($activityTable)
+ {
+ $branch = $this->getBranch();
+ if ($branch->isBranch() && (int) $this->params->get('page', '1') === 1) {
+ $table = new BranchActivityTable($branch->getUuid(), $this->db(), $this->object->getUniqueId());
+ if (count($table) > 0) {
+ $this->content()->add(Hint::info(Html::sprintf($this->translate(
+ 'The following modifications are visible in this %s only...'
+ ), Branch::requireHook()->linkToBranch(
+ $branch,
+ $this->Auth(),
+ $this->translate('configuration branch')
+ ))));
+ $this->content()->add($table);
+ if (count($activityTable) === 0) {
+ return;
+ }
+ $this->content()->add(Html::tag('br'));
+ $this->content()->add(Hint::ok($this->translate(
+ '...and the modifications below are already in the main branch:'
+ )));
+ $this->content()->add(Html::tag('br'));
+ }
+ }
+ }
+}
diff --git a/library/Director/Web/Controller/ObjectsController.php b/library/Director/Web/Controller/ObjectsController.php
new file mode 100644
index 0000000..8c10b44
--- /dev/null
+++ b/library/Director/Web/Controller/ObjectsController.php
@@ -0,0 +1,548 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller;
+
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\NotFoundError;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Forms\IcingaMultiEditForm;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\RestApi\IcingaObjectsHandler;
+use Icinga\Module\Director\Web\ActionBar\ObjectsActionBar;
+use Icinga\Module\Director\Web\ActionBar\TemplateActionBar;
+use Icinga\Module\Director\Web\Form\FormLoader;
+use Icinga\Module\Director\Web\Table\ApplyRulesTable;
+use Icinga\Module\Director\Web\Table\ObjectSetTable;
+use Icinga\Module\Director\Web\Table\ObjectsTable;
+use Icinga\Module\Director\Web\Table\TemplatesTable;
+use Icinga\Module\Director\Web\Tabs\ObjectsTabs;
+use Icinga\Module\Director\Web\Tree\TemplateTreeRenderer;
+use gipfl\IcingaWeb2\Link;
+use Icinga\Module\Director\Web\Widget\AdditionalTableActions;
+use Icinga\Module\Director\Web\Widget\BranchedObjectsHint;
+use InvalidArgumentException;
+use Ramsey\Uuid\Uuid;
+
+abstract class ObjectsController extends ActionController
+{
+ use BranchHelper;
+
+ protected $isApified = true;
+
+ /** @var ObjectsTable */
+ protected $table;
+
+ protected function checkDirectorPermissions()
+ {
+ if ($this->getRequest()->getActionName() !== 'sets') {
+ $this->assertPermission('director/' . $this->getPluralBaseType());
+ }
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ */
+ protected function addObjectsTabs()
+ {
+ $tabName = $this->getRequest()->getActionName();
+ if (substr($this->getType(), -5) === 'Group') {
+ $tabName = 'groups';
+ }
+ $this->tabs(new ObjectsTabs(
+ $this->getBaseType(),
+ $this->Auth(),
+ $this->getBaseObjectUrl()
+ ))->activate($tabName);
+
+ return $this;
+ }
+
+ /**
+ * @return IcingaObjectsHandler
+ * @throws NotFoundError
+ */
+ protected function apiRequestHandler()
+ {
+ $request = $this->getRequest();
+ $table = $this->getTable();
+ if ($request->getControllerName() === 'services'
+ && $host = $this->params->get('host')
+ ) {
+ $host = IcingaHost::load($host, $this->db());
+ $table->getQuery()->where('o.host_id = ?', $host->get('id'));
+ }
+
+ if ($request->getActionName() === 'templates') {
+ $table->filterObjectType('template');
+ } elseif ($request->getActionName() === 'applyrules') {
+ $table->filterObjectType('apply');
+ }
+ $search = $this->params->get('q');
+ if ($search !== null && \strlen($search) > 0) {
+ $table->search($search);
+ }
+
+ return (new IcingaObjectsHandler(
+ $request,
+ $this->getResponse(),
+ $this->db()
+ ))->setTable($table);
+ }
+
+ /**
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws NotFoundError
+ */
+ public function indexAction()
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+
+ $type = $this->getType();
+ if ($this->params->get('format') === 'json') {
+ $filename = sprintf(
+ "director-${type}_%s.json",
+ date('YmdHis')
+ );
+ $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true);
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+
+ $this
+ ->addObjectsTabs()
+ ->setAutorefreshInterval(10)
+ ->addTitle($this->translate(ucfirst($this->getPluralType())))
+ ->actions(new ObjectsActionBar($this->getBaseObjectUrl(), $this->url()));
+
+ $this->content()->add(new BranchedObjectsHint($this->getBranch(), $this->Auth()));
+
+ if ($type === 'command' && $this->params->get('type') === 'external_object') {
+ $this->tabs()->activate('external');
+ }
+
+ // Hint: might be used in controllers extending this
+ $this->table = $this->eventuallyFilterCommand($this->getTable());
+
+ $this->table->renderTo($this);
+ (new AdditionalTableActions($this->getAuth(), $this->url(), $this->table))
+ ->appendTo($this->actions());
+ }
+
+ /**
+ * @return ObjectsTable
+ */
+ protected function getTable()
+ {
+ $table = ObjectsTable::create($this->getType(), $this->db())
+ ->setAuth($this->getAuth())
+ ->setBranchUuid($this->getBranchUuid())
+ ->setBaseObjectUrl($this->getBaseObjectUrl());
+
+ return $table;
+ }
+
+ /**
+ * @return ApplyRulesTable
+ * @throws NotFoundError
+ */
+ protected function getApplyRulesTable()
+ {
+ $table = new ApplyRulesTable($this->db());
+ $table->setType($this->getType())
+ ->setBaseObjectUrl($this->getBaseObjectUrl());
+ $this->eventuallyFilterCommand($table);
+
+ return $table;
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function edittemplatesAction()
+ {
+ $this->commonForEdit();
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function editAction()
+ {
+ $this->commonForEdit();
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function commonForEdit()
+ {
+ $type = ucfirst($this->getType());
+
+ if (empty($this->multiEdit)) {
+ throw new NotFoundError('Cannot edit multiple "%s" instances', $type);
+ }
+
+ $objects = $this->loadMultiObjectsFromParams();
+ if (empty($objects)) {
+ throw new NotFoundError('No "%s" instances have been loaded', $type);
+ }
+ $formName = 'icinga' . $type;
+ $form = IcingaMultiEditForm::load()
+ ->setBranch($this->getBranch())
+ ->setObjects($objects)
+ ->pickElementsFrom($this->loadForm($formName), $this->multiEdit);
+ if ($type === 'Service') {
+ $form->setListUrl('director/services');
+ } elseif ($type === 'Host') {
+ $form->setListUrl('director/hosts');
+ }
+
+ $form->handleRequest();
+
+ $this
+ ->addSingleTab($this->translate('Multiple objects'))
+ ->addTitle(
+ $this->translate('Modify %d objects'),
+ count($objects)
+ )->content()->add($form);
+ }
+
+ /**
+ * Loads the TemplatesTable or the TemplateTreeRenderer
+ *
+ * Passing render=tree switches to the tree view.
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws \Icinga\Security\SecurityException
+ * @throws NotFoundError
+ */
+ public function templatesAction()
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+ $type = $this->getType();
+
+ if ($this->params->get('format') === 'json') {
+ $filename = sprintf(
+ "director-${type}-templates_%s.json",
+ date('YmdHis')
+ );
+ $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true);
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+
+ $shortType = IcingaObject::createByType($type)->getShortTableName();
+ $this
+ ->assertPermission('director/admin')
+ ->addObjectsTabs()
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('All your %s Templates'),
+ $this->translate(ucfirst($type))
+ )
+ ->actions(new TemplateActionBar($shortType, $this->url()));
+
+ if ($this->params->get('render') === 'tree') {
+ TemplateTreeRenderer::showType($shortType, $this, $this->db());
+ } else {
+ $table = TemplatesTable::create($shortType, $this->db());
+ $this->eventuallyFilterCommand($table);
+ $table->renderTo($this);
+ (new AdditionalTableActions($this->getAuth(), $this->url(), $table))
+ ->appendTo($this->actions());
+ }
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Security\SecurityException
+ */
+ protected function assertApplyRulePermission()
+ {
+ return $this->assertPermission('director/admin');
+ }
+
+ /**
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws \Icinga\Security\SecurityException
+ * @throws NotFoundError
+ */
+ public function applyrulesAction()
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+
+ $type = $this->getType();
+
+ if ($this->params->get('format') === 'json') {
+ $filename = sprintf(
+ "director-${type}-applyrules_%s.json",
+ date('YmdHis')
+ );
+ $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true);
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+
+ $tType = $this->translate(ucfirst($type));
+ $this
+ ->assertApplyRulePermission()
+ ->addObjectsTabs()
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('All your %s Apply Rules'),
+ $tType
+ );
+ $baseUrl = 'director/' . $this->getBaseObjectUrl();
+ $this->actions()
+ //->add($this->getBackToDashboardLink())
+ ->add(
+ Link::create(
+ $this->translate('Add'),
+ "${baseUrl}/add",
+ ['type' => 'apply'],
+ [
+ 'title' => sprintf(
+ $this->translate('Create a new %s Apply Rule'),
+ $tType
+ ),
+ 'class' => 'icon-plus',
+ 'data-base-target' => '_next'
+ ]
+ )
+ );
+
+ $table = $this->getApplyRulesTable();
+ $table->renderTo($this);
+ (new AdditionalTableActions($this->getAuth(), $this->url(), $table))
+ ->appendTo($this->actions());
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function setsAction()
+ {
+ $type = $this->getType();
+ $tType = $this->translate(ucfirst($type));
+ $this
+ ->assertPermission('director/' . $this->getBaseType() . 'sets')
+ ->addObjectsTabs()
+ ->requireSupportFor('Sets')
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('Icinga %s Sets'),
+ $tType
+ );
+
+ $this->actions()->add(
+ Link::create(
+ $this->translate('Add'),
+ "director/${type}set/add",
+ null,
+ [
+ 'title' => sprintf(
+ $this->translate('Create a new %s Set'),
+ $tType
+ ),
+ 'class' => 'icon-plus',
+ 'data-base-target' => '_next'
+ ]
+ )
+ );
+
+ ObjectSetTable::create($type, $this->db(), $this->getAuth())
+ ->setBranch($this->getBranch())
+ ->renderTo($this);
+ }
+
+ /**
+ * @return array
+ * @throws NotFoundError
+ */
+ protected function loadMultiObjectsFromParams()
+ {
+ $filter = Filter::fromQueryString($this->params->toString());
+ $type = $this->getType();
+ $objects = array();
+ $db = $this->db();
+ $class = DbObjectTypeRegistry::classByType($type);
+ $table = DbObjectTypeRegistry::tableNameByType($type);
+ $store = new DbObjectStore($db, $this->getBranch());
+
+ /** @var $filter FilterChain */
+ foreach ($filter->filters() as $sub) {
+ /** @var $sub FilterChain */
+ foreach ($sub->filters() as $ex) {
+ /** @var $ex FilterChain|FilterExpression */
+ $col = $ex->getColumn();
+ if ($ex->isExpression()) {
+ if ($col === 'name') {
+ $name = $ex->getExpression();
+ if ($type === 'service') {
+ $key = [
+ 'object_type' => 'template',
+ 'object_name' => $name
+ ];
+ } else {
+ $key = $name;
+ }
+ $objects[$name] = $class::load($key, $db);
+ } elseif ($col === 'id') {
+ $name = $ex->getExpression();
+ $objects[$name] = $class::load($name, $db);
+ } elseif ($col === 'uuid') {
+ $object = $store->load($table, Uuid::fromString($ex->getExpression()));
+ $objects[$object->getObjectName()] = $object;
+ } else {
+ throw new InvalidArgumentException("'$col' is no a valid key component for '$type'");
+ }
+ }
+ }
+ }
+
+ return $objects;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return \Icinga\Module\Director\Web\Form\QuickForm
+ */
+ public function loadForm($name)
+ {
+ $form = FormLoader::load($name, $this->Module());
+ if ($this->getRequest()->isApiRequest()) {
+ // TODO: Ask form for API support?
+ $form->setApiRequest();
+ }
+
+ return $form;
+ }
+
+ /**
+ * @param ZfQueryBasedTable $table
+ * @return ZfQueryBasedTable
+ * @throws NotFoundError
+ */
+ protected function eventuallyFilterCommand(ZfQueryBasedTable $table)
+ {
+ if ($this->params->get('command')) {
+ $command = IcingaCommand::load($this->params->get('command'), $this->db());
+ switch ($this->getBaseType()) {
+ case 'host':
+ case 'service':
+ $table->getQuery()->where(
+ $this->db()->getDbAdapter()->quoteInto(
+ '(o.check_command_id = ? OR o.event_command_id = ?)',
+ $command->getAutoincId()
+ )
+ );
+ break;
+ case 'notification':
+ $table->getQuery()->where(
+ 'o.command_id = ?',
+ $command->getAutoincId()
+ );
+ break;
+ }
+ }
+
+ return $table;
+ }
+
+ /**
+ * @param $feature
+ * @return $this
+ * @throws NotFoundError
+ */
+ protected function requireSupportFor($feature)
+ {
+ if ($this->supports($feature) !== true) {
+ throw new NotFoundError(
+ '%s does not support %s',
+ $this->getType(),
+ $feature
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $feature
+ * @return bool
+ */
+ protected function supports($feature)
+ {
+ $func = "supports$feature";
+ return IcingaObject::createByType($this->getType())->$func();
+ }
+
+ /**
+ * @return string
+ */
+ protected function getBaseType()
+ {
+ $type = $this->getType();
+ if (substr($type, -5) === 'Group') {
+ return substr($type, 0, -5);
+ } else {
+ return $type;
+ }
+ }
+
+ protected function getBaseObjectUrl()
+ {
+ return $this->getType();
+ }
+
+ /**
+ * @return string
+ */
+ protected function getType()
+ {
+ // Strip final 's' and upcase an eventual 'group'
+ return preg_replace(
+ array('/group$/', '/period$/', '/argument$/', '/apiuser$/', '/dependencie$/', '/set$/'),
+ array('Group', 'Period', 'Argument', 'ApiUser', 'dependency', 'Set'),
+ str_replace(
+ 'template',
+ '',
+ substr($this->getRequest()->getControllerName(), 0, -1)
+ )
+ );
+ }
+
+ /**
+ * @return string
+ */
+ protected function getPluralType()
+ {
+ return preg_replace('/cys$/', 'cies', $this->getType() . 's');
+ }
+
+ /**
+ * @return string
+ */
+ protected function getPluralBaseType()
+ {
+ return preg_replace('/cys$/', 'cies', $this->getBaseType() . 's');
+ }
+}
diff --git a/library/Director/Web/Controller/TemplateController.php b/library/Director/Web/Controller/TemplateController.php
new file mode 100644
index 0000000..c368a82
--- /dev/null
+++ b/library/Director/Web/Controller/TemplateController.php
@@ -0,0 +1,243 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Controller\Extension\DirectorDb;
+use Icinga\Module\Director\Web\Table\ApplyRulesTable;
+use Icinga\Module\Director\Web\Table\ObjectsTable;
+use Icinga\Module\Director\Web\Table\TemplatesTable;
+use Icinga\Module\Director\Web\Table\TemplateUsageTable;
+use Icinga\Module\Director\Web\Tabs\ObjectTabs;
+use Icinga\Module\Director\Web\Widget\UnorderedList;
+use ipl\Html\FormattedString;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\CompatController;
+
+abstract class TemplateController extends CompatController
+{
+ use DirectorDb;
+
+ /** @var IcingaObject */
+ protected $template;
+
+ public function objectsAction()
+ {
+ $template = $this->requireTemplate();
+ $plural = $this->getTranslatedPluralType();
+ $this
+ ->addSingleTab($plural)
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('%s based on %s'),
+ $plural,
+ $template->getObjectName()
+ )->addBackToUsageLink($template);
+
+ ObjectsTable::create($this->getType(), $this->db())
+ ->setAuth($this->Auth())
+ ->setBaseObjectUrl($this->getBaseObjectUrl())
+ ->filterTemplate($template, $this->getInheritance())
+ ->renderTo($this);
+ }
+
+ public function applyrulesAction()
+ {
+ $type = $this->getType();
+ $template = $this->requireTemplate();
+ $this
+ ->addSingleTab(sprintf($this->translate('Applied %s'), $this->getTranslatedPluralType()))
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('Notification Apply Rules based on %s'),
+ $template->getObjectName()
+ )->addBackToUsageLink($template);
+
+ ApplyRulesTable::create($type, $this->db())
+ ->setBaseObjectUrl($this->getBaseObjectUrl())
+ ->filterTemplate($template, $this->params->get('inheritance', 'direct'))
+ ->renderTo($this);
+ }
+
+ public function templatesAction()
+ {
+ $template = $this->requireTemplate();
+ $typeName = $this->getTranslatedType();
+ $this
+ ->addSingleTab(sprintf($this->translate('%s Templates'), $typeName))
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('%s templates based on %s'),
+ $typeName,
+ $template->getObjectName()
+ )->addBackToUsageLink($template);
+
+ TemplatesTable::create($this->getType(), $this->db())
+ ->filterTemplate($template, $this->getInheritance())
+ ->renderTo($this);
+ }
+
+ protected function getInheritance()
+ {
+ return $this->params->get('inheritance', 'direct');
+ }
+
+ protected function addBackToUsageLink(IcingaObject $template)
+ {
+ $type = $this->getType();
+ $this->actions()->add(
+ Link::create(
+ $this->translate('Back'),
+ "director/${type}template/usage",
+ ['name' => $template->getObjectName()],
+ ['class' => 'icon-left-big']
+ )
+ );
+
+ return $this;
+ }
+
+ public function usageAction()
+ {
+ $template = $this->requireTemplate();
+ if (! $template->isTemplate() && $template instanceof IcingaCommand) {
+ $this->redirectNow($this->url()->setPath('director/command'));
+ }
+ $templateName = $template->getObjectName();
+
+ $type = $this->getType();
+ $this->tabs(new ObjectTabs($type, $this->Auth(), $template))->activate('modify');
+ $this
+ ->addTitle($this->translate('Template: %s'), $templateName)
+ ->setAutorefreshInterval(10);
+
+ $this->actions()->add([
+ Link::create(
+ $this->translate('Modify'),
+ "director/$type/edit",
+ ['uuid' => $template->getUniqueId()->toString()],
+ ['class' => 'icon-edit']
+ )
+ ]);
+ if ($template instanceof ExportInterface) {
+ $this->actions()->add(Link::create(
+ $this->translate('Add to Basket'),
+ 'director/basket/add',
+ [
+ 'type' => ucfirst($this->getType()) . 'Template',
+ 'names' => $template->getUniqueIdentifier()
+ ],
+ ['class' => 'icon-tag']
+ ));
+ }
+
+ $list = new UnorderedList([], [
+ 'class' => 'vertical-action-list'
+ ]);
+
+ $auth = $this->Auth();
+
+ if ($type !== 'notification') {
+ $list->addItem(new FormattedString(
+ $this->translate('Create a new %s inheriting from this template'),
+ [Link::create(
+ $this->translate('Object'),
+ "director/$type/add",
+ ['imports' => $templateName, 'type' => 'object']
+ )]
+ ));
+ }
+ if ($auth->hasPermission('director/admin')) {
+ $list->addItem(new FormattedString(
+ $this->translate('Create a new %s inheriting from this one'),
+ [Link::create(
+ $this->translate('Template'),
+ "director/$type/add",
+ ['imports' => $templateName, 'type' => 'template']
+ )]
+ ));
+ }
+ if ($template->supportsApplyRules()) {
+ $list->addItem(new FormattedString(
+ $this->translate('Create a new %s inheriting from this template'),
+ [Link::create(
+ $this->translate('Apply Rule'),
+ "director/$type/add",
+ ['imports' => $templateName, 'type' => 'apply']
+ )]
+ ));
+ }
+
+ $typeName = $this->getTranslatedType();
+ $this->content()->add(Html::sprintf(
+ $this->translate(
+ 'This is the "%s" %s Template. Based on this, you might want to:'
+ ),
+ $typeName,
+ $templateName
+ ))->add(
+ $list
+ )->add(
+ Html::tag('h2', null, $this->translate('Current Template Usage'))
+ );
+
+ try {
+ $this->content()->add(
+ TemplateUsageTable::forTemplate($template)
+ );
+ } catch (NestingError $e) {
+ $this->content()->add(Hint::error($e->getMessage()));
+ }
+ }
+
+ protected function getType()
+ {
+ return $this->template()->getShortTableName();
+ }
+
+ protected function getPluralType()
+ {
+ return preg_replace(
+ '/cys$/',
+ 'cies',
+ $this->template()->getShortTableName() . 's'
+ );
+ }
+
+ protected function getTranslatedType()
+ {
+ return $this->translate(ucfirst($this->getType()));
+ }
+
+ protected function getTranslatedPluralType()
+ {
+ return $this->translate(ucfirst($this->getPluralType()));
+ }
+
+ protected function getBaseObjectUrl()
+ {
+ return $this->getType();
+ }
+
+ /**
+ * @return IcingaObject
+ */
+ protected function template()
+ {
+ if ($this->template === null) {
+ $this->template = $this->requireTemplate();
+ }
+
+ return $this->template;
+ }
+
+ /**
+ * @return IcingaObject
+ */
+ abstract protected function requireTemplate();
+}