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