1<?php 2 3namespace Icinga\Module\Businessprocess\Controllers; 4 5use Icinga\Date\DateFormatter; 6use Icinga\Module\Businessprocess\BpConfig; 7use Icinga\Module\Businessprocess\BpNode; 8use Icinga\Module\Businessprocess\Node; 9use Icinga\Module\Businessprocess\Renderer\Breadcrumb; 10use Icinga\Module\Businessprocess\Renderer\Renderer; 11use Icinga\Module\Businessprocess\Renderer\TileRenderer; 12use Icinga\Module\Businessprocess\Renderer\TreeRenderer; 13use Icinga\Module\Businessprocess\Simulation; 14use Icinga\Module\Businessprocess\State\MonitoringState; 15use Icinga\Module\Businessprocess\Storage\ConfigDiff; 16use Icinga\Module\Businessprocess\Storage\LegacyConfigRenderer; 17use Icinga\Module\Businessprocess\Web\Component\ActionBar; 18use Icinga\Module\Businessprocess\Web\Component\RenderedProcessActionBar; 19use Icinga\Module\Businessprocess\Web\Component\Tabs; 20use Icinga\Module\Businessprocess\Web\Controller; 21use Icinga\Util\Json; 22use Icinga\Web\Notification; 23use Icinga\Web\Url; 24use Icinga\Web\Widget\Tabextension\DashboardAction; 25use Icinga\Web\Widget\Tabextension\OutputFormat; 26use ipl\Html\Html; 27use ipl\Html\HtmlString; 28 29class ProcessController extends Controller 30{ 31 /** @var Renderer */ 32 protected $renderer; 33 34 /** 35 * Create a new Business Process Configuration 36 */ 37 public function createAction() 38 { 39 $this->assertPermission('businessprocess/create'); 40 41 $title = $this->translate('Create a new Business Process'); 42 $this->setTitle($title); 43 $this->controls() 44 ->add($this->tabsForCreate()->activate('create')) 45 ->add(Html::tag('h1', null, $title)); 46 47 $this->content()->add( 48 $this->loadForm('bpConfig') 49 ->setStorage($this->storage()) 50 ->setSuccessUrl('businessprocess/process/show') 51 ->handleRequest() 52 ); 53 } 54 55 /** 56 * Upload an existing Business Process Configuration 57 */ 58 public function uploadAction() 59 { 60 $this->assertPermission('businessprocess/create'); 61 62 $title = $this->translate('Upload a Business Process Config file'); 63 $this->setTitle($title); 64 $this->controls() 65 ->add($this->tabsForCreate()->activate('upload')) 66 ->add(Html::tag('h1', null, $title)); 67 68 $this->content()->add( 69 $this->loadForm('BpUpload') 70 ->setStorage($this->storage()) 71 ->setSuccessUrl('businessprocess/process/show') 72 ->handleRequest() 73 ); 74 } 75 76 /** 77 * Show a business process 78 */ 79 public function showAction() 80 { 81 $bp = $this->loadModifiedBpConfig(); 82 $node = $this->getNode($bp); 83 84 MonitoringState::apply($bp); 85 $this->handleSimulations($bp); 86 87 $this->setTitle($this->translate('Business Process "%s"'), $bp->getTitle()); 88 89 $renderer = $this->prepareRenderer($bp, $node); 90 91 if (! $this->showFullscreen && ($node === null || ! $renderer->rendersImportedNode())) { 92 if ($this->params->get('unlocked')) { 93 $renderer->unlock(); 94 } 95 96 if ($bp->isEmpty() && $renderer->isLocked()) { 97 $this->redirectNow($this->url()->with('unlocked', true)); 98 } 99 } 100 101 $this->handleFormatRequest($bp, $node); 102 103 $this->prepareControls($bp, $renderer); 104 105 $this->tabs()->extend(new OutputFormat()); 106 107 $missing = $bp->getMissingChildren(); 108 if (! empty($missing)) { 109 if (($count = count($missing)) > 10) { 110 $missing = array_slice($missing, 0, 10); 111 $missing[] = '...'; 112 } 113 $bp->addError(sprintf('There are %d missing nodes: %s', $count, implode(', ', $missing))); 114 } 115 $this->content()->add($this->showHints($bp)); 116 $this->content()->add($this->showWarnings($bp)); 117 $this->content()->add($this->showErrors($bp)); 118 $this->content()->add($renderer); 119 $this->loadActionForm($bp, $node); 120 $this->setDynamicAutorefresh(); 121 } 122 123 protected function prepareControls($bp, $renderer) 124 { 125 $controls = $this->controls(); 126 127 if ($this->showFullscreen) { 128 $controls->getAttributes()->add('class', 'want-fullscreen'); 129 $controls->add(Html::tag( 130 'a', 131 [ 132 'href' => $this->url()->without('showFullscreen')->without('view'), 133 'title' => $this->translate('Leave full screen and switch back to normal mode'), 134 'style' => 'float: right' 135 ], 136 Html::tag('i', ['class' => 'icon icon-resize-small']) 137 )); 138 } 139 140 if (! ($this->showFullscreen || $this->view->compact)) { 141 $controls->add($this->getProcessTabs($bp, $renderer)); 142 $controls->getAttributes()->add('class', 'separated'); 143 } 144 145 $controls->add(Breadcrumb::create(clone $renderer)); 146 if (! $this->showFullscreen && ! $this->view->compact) { 147 $controls->add( 148 new RenderedProcessActionBar($bp, $renderer, $this->Auth(), $this->url()) 149 ); 150 } 151 } 152 153 protected function getNode(BpConfig $bp) 154 { 155 if ($nodeName = $this->params->get('node')) { 156 return $bp->getNode($nodeName); 157 } else { 158 return null; 159 } 160 } 161 162 protected function prepareRenderer($bp, $node) 163 { 164 if ($this->renderer === null) { 165 if ($this->params->get('mode') === 'tree') { 166 $renderer = new TreeRenderer($bp, $node); 167 } else { 168 $renderer = new TileRenderer($bp, $node); 169 } 170 $renderer->setUrl($this->url()) 171 ->setPath($this->params->getValues('path')); 172 173 $this->renderer = $renderer; 174 } 175 176 return $this->renderer; 177 } 178 179 protected function getProcessTabs(BpConfig $bp, Renderer $renderer) 180 { 181 $tabs = $this->singleTab($bp->getTitle()); 182 if ($renderer->isLocked()) { 183 $tabs->extend(new DashboardAction()); 184 } 185 186 return $tabs; 187 } 188 189 protected function handleSimulations(BpConfig $bp) 190 { 191 $simulation = Simulation::fromSession($this->session()); 192 193 if ($this->params->get('dismissSimulations')) { 194 Notification::success( 195 sprintf( 196 $this->translate('%d applied simulation(s) have been dropped'), 197 $simulation->count() 198 ) 199 ); 200 $simulation->clear(); 201 $this->redirectNow($this->url()->without('dismissSimulations')->without('unlocked')); 202 } 203 204 $bp->applySimulation($simulation); 205 } 206 207 protected function loadActionForm(BpConfig $bp, Node $node = null) 208 { 209 $action = $this->params->get('action'); 210 $form = null; 211 if ($this->showFullscreen) { 212 return; 213 } 214 215 $canEdit = $bp->getMetadata()->canModify(); 216 217 if ($action === 'add' && $canEdit) { 218 $form = $this->loadForm('AddNode') 219 ->setSuccessUrl(Url::fromRequest()->without('action')) 220 ->setStorage($this->storage()) 221 ->setProcess($bp) 222 ->setParentNode($node) 223 ->setSession($this->session()) 224 ->handleRequest(); 225 } elseif ($action === 'editmonitored' && $canEdit) { 226 $form = $this->loadForm('EditNode') 227 ->setSuccessUrl(Url::fromRequest()->without('action')) 228 ->setProcess($bp) 229 ->setNode($bp->getNode($this->params->get('editmonitorednode'))) 230 ->setParentNode($node) 231 ->setSession($this->session()) 232 ->handleRequest(); 233 } elseif ($action === 'delete' && $canEdit) { 234 $form = $this->loadForm('DeleteNode') 235 ->setSuccessUrl(Url::fromRequest()->without('action')) 236 ->setProcess($bp) 237 ->setNode($bp->getNode($this->params->get('deletenode'))) 238 ->setParentNode($node) 239 ->setSession($this->session()) 240 ->handleRequest(); 241 } elseif ($action === 'edit' && $canEdit) { 242 $form = $this->loadForm('Process') 243 ->setSuccessUrl(Url::fromRequest()->without('action')) 244 ->setProcess($bp) 245 ->setNode($bp->getNode($this->params->get('editnode'))) 246 ->setSession($this->session()) 247 ->handleRequest(); 248 } elseif ($action === 'simulation') { 249 $form = $this->loadForm('simulation') 250 ->setSuccessUrl(Url::fromRequest()->without('action')) 251 ->setNode($bp->getNode($this->params->get('simulationnode'))) 252 ->setSimulation(Simulation::fromSession($this->session())) 253 ->handleRequest(); 254 } elseif ($action === 'move') { 255 $form = $this->loadForm('MoveNode') 256 ->setProcess($bp) 257 ->setParentNode($node) 258 ->setSession($this->session()) 259 ->setNode($bp->getNode($this->params->get('movenode'))) 260 ->handleRequest(); 261 } 262 263 if ($form) { 264 $this->content()->prepend(HtmlString::create((string) $form)); 265 } 266 } 267 268 protected function setDynamicAutorefresh() 269 { 270 if (! $this->isXhr()) { 271 // This will trigger the very first XHR refresh immediately on page 272 // load. Please not that this may hammer the server in case we would 273 // decide to use autorefreshInterval for HTML meta-refreshes also. 274 $this->setAutorefreshInterval(1); 275 return; 276 } 277 278 if ($this->params->get('action')) { 279 $this->setAutorefreshInterval(45); 280 } else { 281 $this->setAutorefreshInterval(10); 282 } 283 } 284 285 protected function showWarnings(BpConfig $bp) 286 { 287 if ($bp->hasWarnings()) { 288 $ul = Html::tag('ul', array('class' => 'warning')); 289 foreach ($bp->getWarnings() as $warning) { 290 $ul->add(Html::tag('li')->setContent($warning)); 291 } 292 293 return $ul; 294 } else { 295 return null; 296 } 297 } 298 299 protected function showErrors(BpConfig $bp) 300 { 301 if ($bp->hasWarnings()) { 302 $ul = Html::tag('ul', array('class' => 'error')); 303 foreach ($bp->getErrors() as $msg) { 304 $ul->add(Html::tag('li')->setContent($msg)); 305 } 306 307 return $ul; 308 } else { 309 return null; 310 } 311 } 312 313 protected function showHints(BpConfig $bp) 314 { 315 $ul = Html::tag('ul', ['class' => 'error']); 316 foreach ($bp->getErrors() as $error) { 317 $ul->add(Html::tag('li')->setContent($error)); 318 } 319 if ($bp->hasChanges()) { 320 $li = Html::tag('li')->setSeparator(' '); 321 $li->add(sprintf( 322 $this->translate('This process has %d pending change(s).'), 323 $bp->countChanges() 324 ))->add(Html::tag( 325 'a', 326 [ 327 'href' => Url::fromPath('businessprocess/process/config') 328 ->setParams($this->getRequest()->getUrl()->getParams()) 329 ], 330 $this->translate('Store') 331 ))->add(Html::tag( 332 'a', 333 ['href' => $this->url()->with('dismissChanges', true)], 334 $this->translate('Dismiss') 335 )); 336 $ul->add($li); 337 } 338 339 if ($bp->hasSimulations()) { 340 $li = Html::tag('li')->setSeparator(' '); 341 $li->add(sprintf( 342 $this->translate('This process shows %d simulated state(s).'), 343 $bp->countSimulations() 344 ))->add(Html::tag( 345 'a', 346 ['href' => $this->url()->with('dismissSimulations', true)], 347 $this->translate('Dismiss') 348 )); 349 $ul->add($li); 350 } 351 352 if (! $ul->isEmpty()) { 353 return $ul; 354 } else { 355 return null; 356 } 357 } 358 359 /** 360 * Show the source code for a process 361 */ 362 public function sourceAction() 363 { 364 $this->assertPermission('businessprocess/modify'); 365 366 $bp = $this->loadModifiedBpConfig(); 367 $this->view->showDiff = $showDiff = (bool) $this->params->get('showDiff', false); 368 369 $this->view->source = LegacyConfigRenderer::renderConfig($bp); 370 if ($this->view->showDiff) { 371 $this->view->diff = ConfigDiff::create( 372 $this->storage()->getSource($this->view->configName), 373 $this->view->source 374 ); 375 $title = sprintf( 376 $this->translate('%s: Source Code Differences'), 377 $bp->getTitle() 378 ); 379 } else { 380 $title = sprintf( 381 $this->translate('%s: Source Code'), 382 $bp->getTitle() 383 ); 384 } 385 386 $this->setTitle($title); 387 $this->controls() 388 ->add($this->tabsForConfig($bp)->activate('source')) 389 ->add(Html::tag('h1', null, $title)) 390 ->add($this->createConfigActionBar($bp, $showDiff)); 391 392 $this->setViewScript('process/source'); 393 } 394 395 /** 396 * Download a process configuration file 397 */ 398 public function downloadAction() 399 { 400 $this->assertPermission('businessprocess/modify'); 401 402 $config = $this->loadModifiedBpConfig(); 403 $response = $this->getResponse(); 404 $response->setHeader( 405 'Content-Disposition', 406 sprintf( 407 'attachment; filename="%s.conf";', 408 $config->getName() 409 ) 410 ); 411 $response->setHeader('Content-Type', 'text/plain'); 412 413 echo LegacyConfigRenderer::renderConfig($config); 414 $this->doNotRender(); 415 } 416 417 /** 418 * Modify a business process configuration 419 */ 420 public function configAction() 421 { 422 $this->assertPermission('businessprocess/modify'); 423 424 $bp = $this->loadModifiedBpConfig(); 425 426 $title = sprintf( 427 $this->translate('%s: Configuration'), 428 $bp->getTitle() 429 ); 430 $this->setTitle($title); 431 $this->controls() 432 ->add($this->tabsForConfig($bp)->activate('config')) 433 ->add(Html::tag('h1', null, $title)) 434 ->add($this->createConfigActionBar($bp)); 435 436 $url = Url::fromPath('businessprocess/process/show') 437 ->setParams($this->getRequest()->getUrl()->getParams()); 438 $this->content()->add( 439 $this->loadForm('bpConfig') 440 ->setProcessConfig($bp) 441 ->setStorage($this->storage()) 442 ->setSuccessUrl($url) 443 ->handleRequest() 444 ); 445 } 446 447 protected function createConfigActionBar(BpConfig $config, $showDiff = false) 448 { 449 $actionBar = new ActionBar(); 450 451 if ($showDiff) { 452 $params = array('config' => $config->getName()); 453 $actionBar->add(Html::tag( 454 'a', 455 [ 456 'href' => Url::fromPath('businessprocess/process/source', $params), 457 'class' => 'icon-doc-text', 458 'title' => $this->translate('Show source code') 459 ], 460 $this->translate('Source') 461 )); 462 } else { 463 $params = array( 464 'config' => $config->getName(), 465 'showDiff' => true 466 ); 467 468 $actionBar->add(Html::tag( 469 'a', 470 [ 471 'href' => Url::fromPath('businessprocess/process/source', $params), 472 'class' => 'icon-flapping', 473 'title' => $this->translate('Highlight changes') 474 ], 475 $this->translate('Diff') 476 )); 477 } 478 479 $actionBar->add(Html::tag( 480 'a', 481 [ 482 'href' => Url::fromPath('businessprocess/process/download', ['config' => $config->getName()]), 483 'class' => 'icon-download', 484 'target' => '_blank', 485 'title' => $this->translate('Download process configuration') 486 ], 487 $this->translate('Download') 488 )); 489 490 return $actionBar; 491 } 492 493 protected function tabsForShow() 494 { 495 return $this->tabs()->add('show', array( 496 'label' => $this->translate('Business Process'), 497 'url' => $this->url() 498 )); 499 } 500 501 /** 502 * @return Tabs 503 */ 504 protected function tabsForCreate() 505 { 506 return $this->tabs()->add('create', array( 507 'label' => $this->translate('Create'), 508 'url' => 'businessprocess/process/create' 509 ))->add('upload', array( 510 'label' => $this->translate('Upload'), 511 'url' => 'businessprocess/process/upload' 512 )); 513 } 514 515 protected function tabsForConfig(BpConfig $config) 516 { 517 $params = array( 518 'config' => $config->getName() 519 ); 520 521 $tabs = $this->tabs()->add('config', array( 522 'label' => $this->translate('Process Configuration'), 523 'url' =>Url::fromPath('businessprocess/process/config', $params) 524 )); 525 526 if ($this->params->get('showDiff')) { 527 $params['showDiff'] = true; 528 } 529 530 $tabs->add('source', array( 531 'label' => $this->translate('Source'), 532 'url' =>Url::fromPath('businessprocess/process/source', $params) 533 )); 534 535 return $tabs; 536 } 537 538 protected function handleFormatRequest(BpConfig $bp, BpNode $node = null) 539 { 540 $desiredContentType = $this->getRequest()->getHeader('Accept'); 541 if ($desiredContentType === 'application/json') { 542 $desiredFormat = 'json'; 543 } elseif ($desiredContentType === 'text/csv') { 544 $desiredFormat = 'csv'; 545 } else { 546 $desiredFormat = strtolower($this->params->get('format', 'html')); 547 } 548 549 switch ($desiredFormat) { 550 case 'json': 551 $response = $this->getResponse(); 552 $response 553 ->setHeader('Content-Type', 'application/json') 554 ->setHeader('Cache-Control', 'no-store') 555 ->setHeader( 556 'Content-Disposition', 557 'inline; filename=' . $this->getRequest()->getActionName() . '.json' 558 ) 559 ->appendBody(Json::sanitize($node !== null ? $node->toArray() : $bp->toArray())) 560 ->sendResponse(); 561 exit; 562 case 'csv': 563 $csv = fopen('php://temp', 'w'); 564 565 fputcsv($csv, ['Path', 'Name', 'State', 'Since']); 566 567 foreach ($node !== null ? $node->toArray(null, true) : $bp->toArray(true) as $node) { 568 $data = [$node['path'], $node['name']]; 569 570 if (isset($node['state'])) { 571 $data[] = $node['state']; 572 } 573 574 if (isset($node['since'])) { 575 $data[] = DateFormatter::formatDateTime($node['since']); 576 } 577 578 fputcsv($csv, $data); 579 } 580 581 $response = $this->getResponse(); 582 $response 583 ->setHeader('Content-Type', 'text/csv') 584 ->setHeader('Cache-Control', 'no-store') 585 ->setHeader( 586 'Content-Disposition', 587 'attachment; filename=' . $this->getRequest()->getActionName() . '.csv' 588 ) 589 ->sendHeaders(); 590 591 rewind($csv); 592 593 fpassthru($csv); 594 595 exit; 596 } 597 } 598} 599