1package OpenXPKI::Client::UI::Workflow;
2use Moose;
3
4extends 'OpenXPKI::Client::UI::Result';
5
6# Core modules
7use DateTime;
8use POSIX;
9use Data::Dumper;
10use Cache::LRU;
11use Digest::SHA qw(sha1_hex);
12
13# CPAN modules
14use Log::Log4perl::MDC;
15use Date::Parse;
16use YAML::Loader;
17use Try::Tiny;
18use MIME::Base64;
19use Crypt::JWT qw( encode_jwt );
20use Crypt::PRNG;
21
22# Project modules
23use OpenXPKI::DateTime;
24use OpenXPKI::Debug;
25use OpenXPKI::i18n qw( i18nTokenizer i18nGettext );
26
27
28# used to cache static patterns like the creator lookup
29my $template_cache = Cache::LRU->new( size => 256 );
30
31
32has __default_grid_head => (
33    is => 'rw',
34    isa => 'ArrayRef',
35    lazy => 1,
36
37    default => sub { return [
38        { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_SERIAL_LABEL', sortkey => 'workflow_id' },
39        { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_UPDATED_LABEL', sortkey => 'workflow_last_update' },
40        { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_TYPE_LABEL', sortkey => 'workflow_type'},
41        { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_STATE_LABEL', sortkey => 'workflow_state' },
42        { sTitle => 'serial', bVisible => 0 },
43        { sTitle => "_className"},
44    ]; }
45);
46
47has __default_grid_row => (
48    is => 'rw',
49    isa => 'ArrayRef',
50    lazy => 1,
51    default => sub { return [
52        { source => 'workflow', field => 'workflow_id' },
53        { source => 'workflow', field => 'workflow_last_update' },
54        { source => 'workflow', field => 'workflow_type' },
55        { source => 'workflow', field => 'workflow_state' },
56        { source => 'workflow', field => 'workflow_id' }
57    ]; }
58);
59
60has __default_wfdetails => (
61    is => 'rw',
62    isa => 'ArrayRef',
63    lazy => 1,
64    default => sub { return [
65        {
66            label => 'I18N_OPENXPKI_UI_WORKFLOW_ID_LABEL',
67            field => 'id',
68            link => {
69                page => 'workflow!load!wf_id![% id %]',
70                target => '_blank',
71            },
72        },
73        {
74            label => 'I18N_OPENXPKI_UI_WORKFLOW_TYPE_LABEL',
75            field => 'type',
76        },
77        {
78            label => 'I18N_OPENXPKI_UI_WORKFLOW_CREATOR_LABEL',
79            field => 'creator',
80        },
81        {
82            label => 'I18N_OPENXPKI_UI_WORKFLOW_STATE_LABEL',
83            template => "[% IF state == 'SUCCESS' %]<b>Success</b>[% ELSE %][% state %][% END %]",
84            format => "raw",
85        },
86        {
87            label => 'I18N_OPENXPKI_UI_WORKFLOW_PROC_STATE_LABEL',
88            field => 'proc_state',
89        },
90    ] },
91);
92
93has __validity_options => (
94    is => 'rw',
95    isa => 'ArrayRef',
96    lazy => 1,
97    default => sub { return [
98        { value => 'last_update_before', label => 'I18N_OPENXPKI_UI_WORKFLOW_LAST_UPDATE_BEFORE_LABEL' },
99        { value => 'last_update_after', label => 'I18N_OPENXPKI_UI_WORKFLOW_LAST_UPDATE_AFTER_LABEL' },
100
101    ];}
102);
103
104has __proc_state_i18n => (
105    is => 'ro', isa => 'HashRef', lazy => 1, init_arg => undef,
106    default => sub { return {
107        # label = short label
108        #   - heading for workflow info popup (workflow search results)
109        #   - search dropdown
110        #   - technical info block on workflow page
111        #
112        # desc = description
113        #   - workflow info popup (workflow search results)
114        running => {
115            label => 'I18N_OPENXPKI_UI_WORKFLOW_INFO_RUNNING_LABEL',
116            desc =>  'I18N_OPENXPKI_UI_WORKFLOW_INFO_RUNNING_DESC',
117        },
118        manual => {
119            label => 'I18N_OPENXPKI_UI_WORKFLOW_INFO_MANUAL_LABEL',
120            desc =>  'I18N_OPENXPKI_UI_WORKFLOW_INFO_MANUAL_DESC',
121        },
122        finished => {
123            label => 'I18N_OPENXPKI_UI_WORKFLOW_INFO_FINISHED_LABEL',
124            desc =>  'I18N_OPENXPKI_UI_WORKFLOW_INFO_FINISHED_DESC',
125        },
126        pause => {
127            label => 'I18N_OPENXPKI_UI_WORKFLOW_INFO_PAUSE_LABEL',
128            desc =>  'I18N_OPENXPKI_UI_WORKFLOW_INFO_PAUSE_DESC',
129        },
130        exception => {
131            label => 'I18N_OPENXPKI_UI_WORKFLOW_INFO_EXCEPTION_LABEL',
132            desc =>  'I18N_OPENXPKI_UI_WORKFLOW_INFO_EXCEPTION_DESC',
133        },
134        retry_exceeded => {
135            label => 'I18N_OPENXPKI_UI_WORKFLOW_INFO_RETRY_EXCEEDED_LABEL',
136            desc =>  'I18N_OPENXPKI_UI_WORKFLOW_INFO_RETRY_EXCEEDED_DESC',
137        },
138        archived => {
139            label => 'I18N_OPENXPKI_UI_WORKFLOW_INFO_ARCHIVED_LABEL',
140            desc =>  'I18N_OPENXPKI_UI_WORKFLOW_INFO_ARCHIVED_DESC',
141        },
142        failed => {
143            label => 'I18N_OPENXPKI_UI_WORKFLOW_INFO_FAILED_LABEL',
144            desc =>  'I18N_OPENXPKI_UI_WORKFLOW_INFO_FAILED_DESC',
145        },
146    } },
147);
148
149=head1 OpenXPKI::Client::UI::Workflow
150
151Generic UI handler class to render a workflow into gui elements.
152It first present a description of the workflow generated from the initial
153states description and a start button which creates the instance. Due to the
154workflow internals we are unable to fetch the field info from the initial
155state and therefore a workflow must not require any input fields at the
156time of creation. A brief description is given at the end of this document.
157
158=cut
159
160sub BUILD {
161    my $self = shift;
162}
163
164=head1 UI Methods
165
166=head2 init_index
167
168Requires parameter I<wf_type> and shows the intro page of the workflow.
169The headline is the value of type followed by an intro text as given
170as workflow description. At the end of the page a button names "start"
171is shown.
172
173This is usually used to start a workflow from the menu or link, e.g.
174
175    workflow!index!wf_type!change_metadata
176
177=cut
178
179sub init_index {
180
181    my $self = shift;
182    my $args = shift;
183
184    my $wf_info = $self->send_command_v2( 'get_workflow_base_info', {
185        type => scalar $self->param('wf_type')
186    });
187
188    if (!$wf_info) {
189        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_UNABLE_TO_LOAD_WORKFLOW_INFORMATION','error');
190        return $self;
191    }
192
193    # Pass the initial activity so we get the form right away
194    my $wf_action = $self->__get_next_auto_action($wf_info);
195
196    $self->__render_from_workflow({ wf_info => $wf_info, wf_action => $wf_action });
197    return $self;
198
199}
200
201=head2 init_start
202
203Same as init_index but directly creates the workflow and displays the result
204of the initial action. Normal workflows will result in a redirect using the
205workflow id, volatile workflows are displayed directly. This works only with
206workflows that do not require any initial parameters.
207
208=cut
209sub init_start {
210
211    my $self = shift;
212    my $args = shift;
213
214    my $wf_info = $self->send_command_v2( 'create_workflow_instance', {
215       workflow => scalar $self->param('wf_type'), params => {}, ui_info => 1,
216       $self->__tenant(),
217    });
218
219    if (!$wf_info) {
220        # todo - handle errors
221        $self->logger()->error("Create workflow failed");
222        return $self;
223    }
224
225    $self->logger()->trace("wf info on create: " . Dumper $wf_info ) if $self->logger->is_trace;
226
227    $self->logger()->info(sprintf "Create new workflow %s, got id %01d",  $wf_info->{workflow}->{type}, $wf_info->{workflow}->{id} );
228
229    # this duplicates code from action_index
230    if ($wf_info->{workflow}->{id} > 0) {
231
232        my $redirect = 'workflow!load!wf_id!'.$wf_info->{workflow}->{id};
233        my @activity = keys %{$wf_info->{activity}};
234        if (scalar @activity == 1) {
235            $redirect .= '!wf_action!'.$activity[0];
236        }
237        $self->redirect($redirect);
238
239    } else {
240        # one shot workflow
241        $self->__render_from_workflow({ wf_info => $wf_info });
242    }
243
244    return $self;
245
246}
247
248=head2 init_load
249
250Requires parameter I<wf_id> which is the id of an existing workflow.
251It loads the workflow at the current state and tries to render it
252using the __render_from_workflow method. In states with multiple actions
253I<wf_action> can be set to select one of them. If those arguments are not
254set from the CGI environment, they can be passed as method arguments.
255
256=cut
257
258sub init_load {
259
260    my $self = shift;
261    my $args = shift;
262
263    # re-instance existing workflow
264    my $id = $self->param('wf_id') || $args->{wf_id} || 0;
265    $id =~ s/[^\d]//g;
266
267    my $wf_action = $self->param('wf_action') || $args->{wf_action} || '';
268    my $view = $self->param('view') || '';
269
270    my $wf_info = $self->send_command_v2( 'get_workflow_info',  {
271        id => $id,
272        with_ui_info => 1,
273    });
274
275    if (!$wf_info) {
276        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_UNABLE_TO_LOAD_WORKFLOW_INFORMATION','error') unless($self->_status());
277        return $self->init_search({ preset => { wf_id => $id } });
278    }
279
280    # Set single action if no special view is requested and only single action is avail
281    if (!$view && !$wf_action && $wf_info->{workflow}->{proc_state} eq 'manual') {
282        $wf_action = $self->__get_next_auto_action($wf_info);
283    }
284
285    $self->__render_from_workflow({ wf_info => $wf_info, wf_action => $wf_action, view => $view });
286
287    return $self;
288
289}
290
291=head2 init_context
292
293Requires parameter I<wf_id> which is the id of an existing workflow.
294Shows the context as plain key/value pairs - usually called in a popup.
295
296=cut
297
298sub init_context {
299
300    my $self = shift;
301
302    # re-instance existing workflow
303    my $id = $self->param('wf_id');
304    my $view = $self->param('view') || '';
305
306
307    my $wf_info = $self->send_command_v2( 'get_workflow_info',  {
308        id => $id,
309        with_ui_info => 1,
310    });
311
312    if (!$wf_info) {
313        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_UNABLE_TO_LOAD_WORKFLOW_INFORMATION','error') unless($self->_status());
314        return $self;
315    }
316
317    $self->_page({
318        label => 'I18N_OPENXPKI_UI_WORKFLOW_CONTEXT_LABEL #' . $wf_info->{workflow}->{id},
319        isLarge => 1,
320    });
321
322    my %buttons;
323    %buttons = ( buttons => [{
324        page => 'workflow!info!wf_id!'.$wf_info->{workflow}->{id},
325        label => 'I18N_OPENXPKI_UI_WORKFLOW_BACK_TO_INFO_LABEL',
326        format => "primary",
327    }]) if ($view eq 'result');
328
329    $self->add_section({
330        type => 'keyvalue',
331        content => {
332            label => '',
333            data => $self->__render_fields( $wf_info, 'context'),
334            %buttons
335    }});
336
337    return $self;
338
339}
340
341
342=head2 init_attribute
343
344Requires parameter I<wf_id> which is the id of an existing workflow.
345Shows the assigned attributes as plain key/value pairs - usually called in a popup.
346
347=cut
348
349sub init_attribute {
350
351    my $self = shift;
352
353    # re-instance existing workflow
354    my $id = $self->param('wf_id');
355    my $view = $self->param('view') || '';
356
357    my $wf_info = $self->send_command_v2( 'get_workflow_info',  {
358        id => $id,
359        with_attributes => 1,
360        with_ui_info => 1,
361    });
362
363    if (!$wf_info) {
364        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_UNABLE_TO_LOAD_WORKFLOW_INFORMATION','error') unless($self->_status());
365        return $self;
366    }
367
368    $self->_page({
369        label => 'I18N_OPENXPKI_UI_WORKFLOW_ATTRIBUTE_LABEL #' . $wf_info->{workflow}->{id},
370        isLarge => 1,
371    });
372
373    my %buttons;
374    %buttons = ( buttons => [{
375        page => 'workflow!info!wf_id!'.$wf_info->{workflow}->{id},
376        label => 'I18N_OPENXPKI_UI_WORKFLOW_BACK_TO_INFO_LABEL',
377        format => "primary",
378    }]) if ($view eq 'result');
379
380    $self->add_section({
381        type => 'keyvalue',
382        content => {
383            label => '',
384            data => $self->__render_fields( $wf_info, 'attribute'),
385            %buttons
386    }});
387
388    return $self;
389
390}
391
392=head2 init_info
393
394Requires parameter I<wf_id> which is the id of an existing workflow.
395It loads the process information to be displayed in a modal popup, used
396mainly from the workflow search / result lists.
397
398=cut
399
400sub init_info {
401
402    my $self = shift;
403    my $args = shift;
404
405    # re-instance existing workflow
406    my $id = $self->param('wf_id') || $args->{wf_id} || 0;
407    $id =~ s/[^\d]//g;
408
409    my $wf_info = $self->send_command_v2( 'get_workflow_info',  {
410        id => $id,
411        with_ui_info => 1,
412    });
413
414    if (!$wf_info) {
415        $self->_page({
416            label => '',
417            shortlabel => '',
418            description => 'I18N_OPENXPKI_UI_WORKFLOW_UNABLE_TO_LOAD_WORKFLOW_INFORMATION',
419        });
420        $self->logger()->warn('Unable to load workflow info for id ' . $id);
421        return $self;
422    }
423
424    my $fields = $self->__render_workflow_info( $wf_info, $self->_client->session()->param('wfdetails') );
425
426    push @{$fields}, {
427        label => "I18N_OPENXPKI_UI_FIELD_ERROR_CODE",
428        name => "error_code",
429        value => $wf_info->{workflow}->{context}->{error_code},
430    } if ($wf_info->{workflow}->{context}->{error_code}
431        && $wf_info->{workflow}->{proc_state} =~ m{(manual|finished|failed)});
432
433    # The workflow info contains info about all control actions that
434    # can be done on the workflow -> render appropriate buttons.
435    my @buttons_handle = ({
436        href => '#/openxpki/redirect!workflow!load!wf_id!'.$wf_info->{workflow}->{id},
437        label => 'I18N_OPENXPKI_UI_WORKFLOW_OPEN_WORKFLOW_LABEL',
438        format => "primary",
439    });
440
441    # The workflow info contains info about all control actions that
442    # can be done on the workflow -> render appropriate buttons.
443    if ($wf_info->{handles} && ref $wf_info->{handles} eq 'ARRAY') {
444        my @handles = @{$wf_info->{handles}};
445        if (grep /context/, @handles) {
446            push @buttons_handle, {
447                'page' => 'workflow!context!view!result!wf_id!'.$wf_info->{workflow}->{id},
448                'label' => 'I18N_OPENXPKI_UI_WORKFLOW_CONTEXT_LABEL',
449            };
450        }
451
452        if (grep /attribute/, @handles) {
453            push @buttons_handle, {
454                'page' => 'workflow!attribute!view!result!wf_id!'.$wf_info->{workflow}->{id},
455                'label' => 'I18N_OPENXPKI_UI_WORKFLOW_ATTRIBUTE_LABEL',
456            };
457        }
458
459        if (grep /history/, @handles) {
460            push @buttons_handle, {
461                'page' => 'workflow!history!view!result!wf_id!'.$wf_info->{workflow}->{id},
462                'label' => 'I18N_OPENXPKI_UI_WORKFLOW_HISTORY_LABEL',
463            };
464        }
465
466        if (grep /techlog/, @handles) {
467            push @buttons_handle, {
468                'page' => 'workflow!log!view!result!wf_id!'.$wf_info->{workflow}->{id},
469                'label' => 'I18N_OPENXPKI_UI_WORKFLOW_LOG_LABEL',
470            };
471        }
472    }
473
474    my $label = sprintf("%s (#%01d)", ($wf_info->{workflow}->{title} || $wf_info->{workflow}->{label} || $wf_info->{workflow}->{type}), $wf_info->{workflow}->{id});
475    $self->_page({
476        label => $label,
477        shortlabel => $label,
478        description => '',
479        isLarge => 1,
480    });
481
482    my $proc_state = $wf_info->{workflow}->{proc_state};
483
484    $self->add_section({
485        type => 'keyvalue',
486        content => {
487            label => $self->__get_proc_state_label($proc_state),
488            description => $self->__get_proc_state_desc($proc_state),
489            data => $fields,
490            buttons => \@buttons_handle,
491    }});
492
493
494    return $self;
495
496}
497
498
499=head2
500
501Render form for the workflow search.
502#TODO: Preset parameters
503
504=cut
505
506sub init_search {
507
508    my $self = shift;
509    my $args = shift;
510
511    $self->_page({
512        label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_LABEL',
513        description => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_DESC',
514    });
515
516    my $workflows = $self->send_command_v2( 'get_workflow_instance_types' );
517    return $self unless(defined $workflows);
518
519    $self->logger()->trace('Workflows ' . Dumper $workflows) if $self->logger->is_trace;
520
521    my $preset = $self->_session->param('wfsearch')->{default}->{preset} || {};
522    # convert preset for last_update
523    foreach my $key (qw(last_update_before last_update_after)) {
524        next unless ($preset->{$key});
525        $preset->{last_update} = {
526            key => $key,
527            value => OpenXPKI::DateTime::get_validity({
528                VALIDITY => $preset->{$key},
529                VALIDITYFORMAT => 'detect'
530            })->epoch()
531        };
532    }
533
534    if ($args->{preset}) {
535        $preset = $args->{preset};
536
537    } elsif (my $queryid = $self->param('query')) {
538        my $result = $self->_client->session()->param('query_wfl_'.$queryid);
539        $preset = $result->{input};
540    }
541
542    $self->logger()->trace('Preset ' . Dumper $preset) if $self->logger->is_trace;
543
544    # TODO Sorting / I18
545    my @wf_names = keys %{$workflows};
546    my @wfl_list = map { {'value' => $_, 'label' => $workflows->{$_}->{label}} } @wf_names ;
547    @wfl_list = sort { lc($a->{'label'}) cmp lc($b->{'label'}) } @wfl_list;
548
549    my $proc_states = [
550        sort { $a->{label} cmp $b->{label} }
551        map { { label => i18nGettext($self->__get_proc_state_label($_)), value => $_} }
552        grep { $_ ne 'running' }
553        keys %{ $self->__proc_state_i18n }
554    ];
555
556    my @fields = (
557        { name => 'wf_type',
558          label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_TYPE_LABEL',
559          type => 'select',
560          is_optional => 1,
561          options => \@wfl_list,
562          value => $preset->{wf_type}
563
564        },
565        { name => 'wf_proc_state',
566          label => 'I18N_OPENXPKI_UI_WORKFLOW_PROC_STATE_LABEL',
567
568          type => 'select',
569          is_optional => 1,
570          prompt => '',
571          options => $proc_states,
572          value => $preset->{wf_proc_state}
573        },
574        { name => 'wf_state',
575          label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_STATE_LABEL',
576          type => 'text',
577          is_optional => 1,
578          prompt => '',
579          value => $preset->{wf_state}
580        },
581        { name => 'wf_creator',
582          label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_CREATOR_LABEL',
583          type => 'text',
584          is_optional => 1,
585          value => $preset->{wf_creator}
586        },
587        { name => 'last_update',
588          label => 'I18N_OPENXPKI_UI_WORKFLOW_LAST_UPDATE_LABEL',
589          'keys' => $self->__validity_options(),
590          type => 'datetime',
591          is_optional => 1,
592          value => $preset->{last_update},
593        }
594    );
595
596    # Searchable attributes are read from the menu bootstrap
597    my $attributes = $self->_session->param('wfsearch')->{default}->{attributes};
598    if ($attributes && (ref $attributes eq 'ARRAY')) {
599        my @attrib;
600        foreach my $item (@{$attributes}) {
601            push @attrib, { value => $item->{key}, label=> $item->{label} };
602        }
603        push @fields, {
604            name => 'attributes',
605            label => 'Metadata',
606            'keys' => \@attrib,
607            type => 'text',
608            is_optional => 1,
609            'clonable' => 1,
610            'value' => $preset->{attributes} || [],
611        } if (@attrib);
612
613    }
614
615    $self->add_section({
616        type => 'form',
617        action => 'workflow!load',
618        content => {
619            title => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_SEARCH_BY_ID_TITLE',
620            submit_label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_SUBMIT_LABEL',
621            fields => [{
622                name => 'wf_id',
623                label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_SERIAL_LABEL',
624                type => 'text',
625                value => $preset->{wf_id} || '',
626            }]
627    }});
628
629    $self->add_section({
630        type => 'form',
631        action => 'workflow!search',
632        content => {
633            title => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_SEARCH_DATABASE_TITLE',
634            submit_label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_SUBMIT_LABEL',
635            fields => \@fields
636
637        }
638    });
639
640    return $self;
641}
642
643=head2 init_result
644
645Load the result of a query, based on a query id and paging information
646
647=cut
648sub init_result {
649
650    my $self = shift;
651    my $args = shift;
652
653    my $queryid = $self->param('id');
654
655    # will be removed once inline paging works
656    my $startat = $self->param('startat') || 0;
657
658    my $limit = $self->param('limit') || 0;
659
660    if ($limit > 500) {  $limit = 500; }
661
662    # Load query from session
663    my $result = $self->_client->session()->param('query_wfl_'.$queryid);
664
665    # result expired or broken id
666    if (!$result || !$result->{count}) {
667
668        $self->set_status('I18N_OPENXPKI_UI_SEARCH_RESULT_EXPIRED_OR_EMPTY','error');
669        return $self->init_search();
670
671    }
672
673    # Add limits
674    my $query = $result->{query};
675
676    if ($limit) {
677        $query->{limit} = $limit;
678    } elsif (!$query->{limit}) {
679        $query->{limit} = 25;
680    }
681
682    $query->{start} = $startat;
683
684    if (!$query->{order}) {
685        $query->{order} = 'workflow_id';
686        if (!defined $query->{reverse}) {
687            $query->{reverse} = 1;
688        }
689    }
690
691    $self->logger()->trace( "persisted query: " . Dumper $result) if $self->logger->is_trace;
692
693    my $search_result = $self->send_command_v2( 'search_workflow_instances', $query );
694
695    $self->logger()->trace( "search result: " . Dumper $search_result) if $self->logger->is_trace;
696
697    # Add page header from result - optional
698    if ($result->{page} && ref $result->{page} ne 'HASH') {
699        $self->_page($result->{page});
700    } else {
701        my $criteria = $result->{criteria} ? '<br>' . (join ", ", @{$result->{criteria}}) : '';
702        $self->_page({
703            label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_RESULTS_LABEL',
704            description => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_RESULTS_DESCRIPTION' . $criteria ,
705            breadcrumb => [
706                { label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_LABEL', className => 'workflow-search' },
707                { label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_RESULTS_TITLE', className => 'workflow-search-result' }
708            ],
709        });
710    }
711
712    my $pager_args = $result->{pager} || {};
713
714    my $pager = $self->__render_pager( $result, { %$pager_args, limit => $query->{limit}, startat => $query->{start} } );
715
716    my $body = $result->{column};
717    $body = $self->__default_grid_row() if(!$body);
718
719    my @lines = $self->__render_result_list( $search_result, $body );
720
721    $self->logger()->trace( "dumper result: " . Dumper \@lines) if $self->logger->is_trace;
722
723    my $header = $result->{header};
724    $header = $self->__default_grid_head() if(!$header);
725
726    # buttons - from result (used in bulk) or default
727    my @buttons;
728    if ($result->{button} && ref $result->{button} eq 'ARRAY') {
729        @buttons = @{$result->{button}};
730    } else {
731
732        push @buttons, { label => 'I18N_OPENXPKI_UI_SEARCH_REFRESH',
733            page => 'redirect!workflow!result!id!' .$queryid,
734            format => 'expected' };
735
736        push @buttons, {
737            label => 'I18N_OPENXPKI_UI_SEARCH_RELOAD_FORM',
738            page => 'workflow!search!query!' .$queryid,
739            format => 'alternative',
740        } if ($result->{input});
741
742        push @buttons,{ label => 'I18N_OPENXPKI_UI_SEARCH_NEW_SEARCH',
743            page => 'workflow!search',
744            format => 'failure'};
745
746        push @buttons, { label => 'I18N_OPENXPKI_UI_SEARCH_EXPORT_RESULT',
747            href => $self->_client()->_config()->{'scripturl'} . '?page=workflow!export!id!'.$queryid,
748            target => '_blank',
749            format => 'optional'
750            };
751    }
752
753    $self->add_section({
754        type => 'grid',
755        className => 'workflow',
756        content => {
757            actions => [{
758                path => 'workflow!info!wf_id!{serial}',
759                label => 'I18N_OPENXPKI_UI_WORKFLOW_OPEN_WORKFLOW_LABEL',
760                icon => 'view',
761                target => 'popup',
762            }],
763            columns => $header,
764            data => \@lines,
765            empty => 'I18N_OPENXPKI_UI_TASK_LIST_EMPTY_LABEL',
766            pager => $pager,
767            buttons => \@buttons
768        }
769    });
770
771    return $self;
772
773}
774
775=head2 init_export
776
777Like init_result but send the data as CSV download, default limit is 500!
778
779=cut
780
781sub init_export {
782
783    my $self = shift;
784    my $args = shift;
785
786    my $queryid = $self->param('id');
787
788    my $limit = $self->param('limit') || 500;
789    my $startat = $self->param('startat') || 0;
790
791    # Safety rule
792    if ($limit > 500) {  $limit = 500; }
793
794    # Load query from session
795    my $result = $self->_client->session()->param('query_wfl_'.$queryid);
796
797    # result expired or broken id
798    if (!$result || !$result->{count}) {
799        $self->set_status('I18N_OPENXPKI_UI_SEARCH_RESULT_EXPIRED_OR_EMPTY','error');
800        return $self->init_search();
801    }
802
803    # Add limits
804    my $query = $result->{query};
805    $query->{limit} = $limit;
806    $query->{start} = $startat;
807
808    if (!$query->{order}) {
809        $query->{order} = 'workflow_id';
810        if (!defined $query->{reverse}) {
811            $query->{reverse} = 1;
812        }
813    }
814
815    $self->logger()->trace( "persisted query: " . Dumper $result) if $self->logger->is_trace;
816
817    my $search_result = $self->send_command_v2( 'search_workflow_instances', $query );
818
819    $self->logger()->trace( "search result: " . Dumper $search_result) if $self->logger->is_trace;
820
821    my $header = $result->{header};
822    $header = $self->__default_grid_head() if(!$header);
823
824    my @head;
825    my @cols;
826
827    my $ii = 0;
828    foreach my $col (@{$header}) {
829        # skip hidden fields
830        if ((!defined $col->{bVisible} || $col->{bVisible}) && $col->{sTitle} !~ /\A_/)  {
831            push @head, $col->{sTitle};
832            push @cols, $ii;
833        }
834        $ii++;
835    }
836
837    my $buffer = join("\t", @head)."\n";
838
839    my $body = $result->{column};
840    $body = $self->__default_grid_row() if(!$body);
841
842    my @lines = $self->__render_result_list( $search_result, $body );
843    my $colcnt = scalar @head - 1;
844    foreach my $line (@lines) {
845        my @t = @{$line};
846        # this hides invisible fields (assumes that hidden fields are always at the end)
847        $buffer .= join("\t", @t[0..$colcnt])."\n"
848    }
849
850    if (scalar @{$search_result} == $limit) {
851        $buffer .= "I18N_OPENXPKI_UI_CERT_EXPORT_EXCEEDS_LIMIT"."\n";
852    }
853
854    print $self->cgi()->header(
855        -type => 'text/tab-separated-values',
856        -expires => "1m",
857        -attachment => "workflow export " . DateTime->now()->iso8601() .  ".txt"
858    );
859
860    print i18nTokenizer($buffer);
861    exit;
862
863}
864
865=head2 init_pager
866
867Similar to init_result but returns only the data portion of the table as
868partial result.
869
870=cut
871
872sub init_pager {
873
874    my $self = shift;
875    my $args = shift;
876
877    my $queryid = $self->param('id');
878
879    # Load query from session
880    my $result = $self->_client->session()->param('query_wfl_'.$queryid);
881
882    # result expired or broken id
883    if (!$result || !$result->{count}) {
884        $self->set_status('Search result expired or empty!','error');
885        return $self->init_search();
886    }
887
888    my $startat = $self->param('startat');
889
890    my $limit = $self->param('limit') || 25;
891
892    if ($limit > 500) {  $limit = 500; }
893
894    # align startat to limit window
895    $startat = int($startat / $limit) * $limit;
896
897    # Add limits
898    my $query = $result->{query};
899    $query->{limit} = $limit;
900    $query->{start} = $startat;
901
902    if ($self->param('order')) {
903        $query->{order} = uc($self->param('order'));
904    }
905
906    if (defined $self->param('reverse')) {
907        $query->{reverse} = $self->param('reverse');
908    }
909
910    $self->logger()->trace( "persisted query: " . Dumper $result) if $self->logger->is_trace;
911    $self->logger()->trace( "executed query: " . Dumper $query) if $self->logger->is_trace;
912
913    my $search_result = $self->send_command_v2( 'search_workflow_instances', $query );
914
915    $self->logger()->trace( "search result: " . Dumper $search_result) if $self->logger->is_trace;
916
917
918    my $body = $result->{column};
919    $body = $self->__default_grid_row() if(!$body);
920
921    my @result = $self->__render_result_list( $search_result, $body );
922
923    $self->logger()->trace( "dumper result: " . Dumper @result) if $self->logger->is_trace;
924
925    $self->_result()->{_raw} = {
926        _returnType => 'partial',
927        data => \@result,
928    };
929
930    return $self;
931}
932
933=head2 init_history
934
935Render the history as grid view (state/action/user/time)
936
937=cut
938
939sub init_history {
940
941    my $self = shift;
942    my $args = shift;
943
944    my $id = $self->param('wf_id');
945    my $view = $self->param('view') || '';
946
947    $self->_page({
948        label => 'I18N_OPENXPKI_UI_WORKFLOW_HISTORY_TITLE',
949        description => 'I18N_OPENXPKI_UI_WORKFLOW_HISTORY_DESCRIPTION',
950        isLarge => 1,
951    });
952
953    my $workflow_history = $self->send_command_v2( 'get_workflow_history', { id => $id } );
954
955    my %buttons;
956    %buttons = ( buttons => [{
957        page => 'workflow!info!wf_id!'.$id,
958        label => 'I18N_OPENXPKI_UI_WORKFLOW_BACK_TO_INFO_LABEL',
959        format => "primary",
960    }]) if ($view eq 'result');
961
962    $self->logger()->trace( "dumper result: " . Dumper $workflow_history) if $self->logger->is_trace;
963
964    my $i = 1;
965    my @result;
966    foreach my $item (@{$workflow_history}) {
967        push @result, [
968            $item->{'workflow_history_date'},
969            $item->{'workflow_state'},
970            $item->{'workflow_action'},
971            $item->{'workflow_description'},
972            $item->{'workflow_user'},
973            $item->{'workflow_node'},
974        ]
975    }
976
977    $self->logger()->trace( "dumper result: " . Dumper $workflow_history) if $self->logger->is_trace;
978
979    $self->add_section({
980        type => 'grid',
981        className => 'workflow',
982        content => {
983            columns => [
984                { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_HISTORY_EXEC_TIME_LABEL' }, #, format => 'datetime'},
985                { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_HISTORY_STATE_LABEL' },
986                { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_HISTORY_ACTION_LABEL' },
987                { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_HISTORY_DESCRIPTION_LABEL' },
988                { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_HISTORY_USER_LABEL' },
989                { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_HISTORY_NODE_LABEL' },
990            ],
991            data => \@result,
992            %buttons,
993        },
994    });
995
996    return $self;
997
998}
999
1000=head2 init_mine
1001
1002Filter workflows where the current user is the creator, similar to workflow
1003search.
1004
1005=cut
1006
1007sub init_mine {
1008
1009    my $self = shift;
1010    my $args = shift;
1011
1012    $self->_page({
1013        label => 'I18N_OPENXPKI_UI_MY_WORKFLOW_TITLE',
1014        description => 'I18N_OPENXPKI_UI_MY_WORKFLOW_DESCRIPTION',
1015    });
1016
1017    my $tasklist = $self->_client->session()->param('tasklist')->{mine};
1018
1019    my $default = {
1020        query => {
1021            attribute => { 'creator' => $self->_session->param('user')->{name} },
1022            order => 'workflow_id',
1023            reverse => 1,
1024        },
1025        actions => [{
1026            path => 'workflow!info!wf_id!{serial}',
1027            label => 'I18N_OPENXPKI_UI_WORKFLOW_OPEN_WORKFLOW_LABEL',
1028            icon => 'view',
1029            target => 'popup',
1030        }]
1031    };
1032
1033    if (!$tasklist || ref $tasklist ne 'ARRAY') {
1034        $self->__render_task_list($default);
1035    } else {
1036        foreach my $item (@$tasklist) {
1037            if ($item->{query}) {
1038                $item->{query} = { %{$default->{query}}, %{$item->{query}} };
1039            } else {
1040                $item->{query} = $default->{query};
1041            }
1042            $item->{actions} = $default->{actions} unless($item->{actions});
1043
1044            $self->__render_task_list($item);
1045        }
1046    }
1047
1048    return $self;
1049
1050}
1051
1052=head2 init_task
1053
1054Outstanding tasks, filter definitions are read from the uicontrol file
1055
1056=cut
1057
1058sub init_task {
1059
1060    my $self = shift;
1061    my $args = shift;
1062
1063    $self->_page({
1064        label => 'I18N_OPENXPKI_UI_WORKFLOW_OUTSTANDING_TASKS_LABEL'
1065    });
1066
1067    my $tasklist = $self->_client->session()->param('tasklist')->{default};
1068
1069    if (!@$tasklist) {
1070        return $self->redirect('home');
1071    }
1072
1073    $self->logger()->trace( "got tasklist: " . Dumper $tasklist) if $self->logger->is_trace;
1074
1075    foreach my $item (@$tasklist) {
1076        $self->__render_task_list($item);
1077    }
1078
1079    return $self;
1080}
1081
1082
1083=head2 init_log
1084
1085Load and display the technical log file of the workflow
1086
1087=cut
1088
1089sub init_log {
1090
1091    my $self = shift;
1092    my $args = shift;
1093
1094    my $id = $self->param('wf_id');
1095    my $view = $self->param('view') || '';
1096
1097    $self->_page({
1098        label => 'I18N_OPENXPKI_UI_WORKFLOW_LOG',
1099        isLarge => 1,
1100    });
1101
1102    my $result = $self->send_command_v2( 'get_workflow_log', { id => $id } );
1103
1104    my %buttons;
1105    %buttons = ( buttons => [{
1106        page => 'workflow!info!wf_id!'.$id,
1107        label => 'I18N_OPENXPKI_UI_WORKFLOW_BACK_TO_INFO_LABEL',
1108        format => "primary",
1109    }]) if ($view eq 'result');
1110
1111    $result = [] unless($result);
1112
1113    $self->logger()->trace( "dumper result: " . Dumper $result) if $self->logger->is_trace;
1114
1115    $self->add_section({
1116        type => 'grid',
1117        className => 'workflow',
1118        content => {
1119            columns => [
1120                { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_LOG_TIMESTAMP_LABEL', format => 'timestamp'},
1121                { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_LOG_PRIORITY_LABEL'},
1122                { sTitle => 'I18N_OPENXPKI_UI_WORKFLOW_LOG_MESSAGE_LABEL'},
1123            ],
1124            data => $result,
1125            empty => 'I18N_OPENXPKI_UI_TASK_LIST_EMPTY_LABEL',
1126            %buttons,
1127        }
1128    });
1129
1130}
1131
1132=head2 action_index
1133
1134=head3 instance creation
1135
1136If you pass I<wf_type>, a new workflow instance of this type is created,
1137the inital action is executed and the resulting state is passed to
1138__render_from_workflow.
1139
1140=head3 generic action
1141
1142The generic action is the default when sending a workflow generated form back
1143to the server. You need to setup the handler from the rendering step, direct
1144posting is not allowed. The cgi environment must present the key I<wf_token>
1145which is a reference to a session based config hash. The config can be created
1146using __register_wf_token, recognized keys are:
1147
1148=over
1149
1150=item wf_fields
1151
1152An arrayref of fields, that are accepted by the handler. This is usually a copy
1153of the field list send to the browser but also allows to specify additional
1154validators. At minimum, each field must be a hashref with the name of the field:
1155
1156    [{ name => fieldname1 }, { name => fieldname2 }]
1157
1158Each input field is mapped to the contextvalue of the same name. Keys ending
1159with empty square brackets C<fieldname[]> are considered to form an array,
1160keys having curly brackets C<fieldname{subname}> are merged into a hash.
1161Non scalar values are serialized before they are submitted.
1162
1163=item wf_action
1164
1165The name of the workflow action that should be executed with the input
1166parameters.
1167
1168=item wf_handler
1169
1170Can hold the full name of a method which is called to handle the current
1171request instead of running the generic handler. See the __delegate_call
1172method for details.
1173
1174=back
1175
1176If there are errors, an error message is send back to the browser, if the
1177workflow execution succeeds, the new workflow state is rendered using
1178__render_from_workflow.
1179
1180=cut
1181
1182sub action_index {
1183
1184    my $self = shift;
1185    my $args = shift;
1186
1187    my $wf_token = $self->param('wf_token') || '';
1188
1189    my $wf_info;
1190    # wf_token found, so its a real action
1191    if (!$wf_token) {
1192        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_INVALID_REQUEST_ACTION_WITHOUT_TOKEN!','error');
1193        return $self;
1194    }
1195
1196    my $wf_args = $self->__fetch_wf_token( $wf_token );
1197
1198    $self->logger()->trace( "wf args: " . Dumper $wf_args) if $self->logger->is_trace;
1199
1200    # check for delegation
1201    if ($wf_args->{wf_handler}) {
1202        return $self->__delegate_call($wf_args->{wf_handler}, $args);
1203    }
1204
1205    my %wf_param;
1206    if ($wf_args->{wf_fields}) {
1207        %wf_param = %{$self->param_from_fields( $wf_args->{wf_fields} )};
1208        $self->logger()->trace( "wf fields: " . Dumper \%wf_param ) if $self->logger->is_trace;
1209    }
1210
1211    # take over params from token, if any
1212    if($wf_args->{wf_param}) {
1213        %wf_param = (%wf_param, %{$wf_args->{wf_param}});
1214    }
1215
1216    $self->logger()->trace( "wf params: " . Dumper \%wf_param ) if $self->logger->is_trace;
1217    ##! 64: "wf params: " . Dumper \%wf_param
1218
1219    if ($wf_args->{wf_id}) {
1220
1221        if (!$wf_args->{wf_action}) {
1222            $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_INVALID_REQUEST_NO_ACTION!','error');
1223            return $self;
1224        }
1225        Log::Log4perl::MDC->put('wfid', $wf_args->{wf_id});
1226        $self->logger()->info(sprintf "Run %s on workflow #%01d", $wf_args->{wf_action}, $wf_args->{wf_id} );
1227
1228        # send input data to workflow
1229        $wf_info = $self->send_command_v2( 'execute_workflow_activity', {
1230            id       => $wf_args->{wf_id},
1231            activity => $wf_args->{wf_action},
1232            params   => \%wf_param,
1233            ui_info => 1
1234        });
1235
1236        if (!$wf_info) {
1237
1238            if ($self->__check_for_validation_error()) {
1239                return $self;
1240            }
1241
1242            $self->logger()->error("workflow acton failed!");
1243            my $extra = { wf_id => $wf_args->{wf_id}, wf_action => $wf_args->{wf_action} };
1244            $self->init_load($extra);
1245            return $self;
1246        }
1247
1248        $self->logger()->trace("wf info after execute: " . Dumper $wf_info ) if $self->logger->is_trace;
1249        # purge the workflow token
1250        $self->__purge_wf_token( $wf_token );
1251
1252    } elsif ($wf_args->{wf_type}) {
1253
1254
1255        $wf_info = $self->send_command_v2( 'create_workflow_instance', {
1256            workflow => $wf_args->{wf_type}, params => \%wf_param, ui_info => 1,
1257            $self->__tenant(),
1258        });
1259        if (!$wf_info) {
1260
1261            if ($self->__check_for_validation_error()) {
1262                return $self;
1263            }
1264
1265            $self->logger()->error("Create workflow failed");
1266            # pass required arguments via extra and reload init page
1267
1268            my $extra = { wf_type => $wf_args->{wf_type} };
1269            $self->init_index($extra);
1270            return $self;
1271        }
1272        $self->logger()->trace("wf info on create: " . Dumper $wf_info ) if $self->logger->is_trace;
1273
1274        $self->logger()->info(sprintf "Create new workflow %s, got id %01d",  $wf_args->{wf_type}, $wf_info->{workflow}->{id} );
1275
1276        # purge the workflow token
1277        $self->__purge_wf_token( $wf_token );
1278
1279        # always redirect after create to have the url pointing to the created workflow
1280        # do not redirect for "one shot workflows" or workflows already in a final state
1281        # as they might hold volatile data (e.g. key download)
1282        my $proc_state = $wf_info->{workflow}->{proc_state};
1283
1284        $wf_args->{redirect} = (
1285            $wf_info->{workflow}->{id} > 0
1286            and $proc_state ne 'finished'
1287            and $proc_state ne 'archived'
1288        );
1289
1290    } else {
1291        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_INVALID_REQUEST_NO_ACTION!','error');
1292        return $self;
1293    }
1294
1295
1296    # Check if we can auto-load the next available action
1297    my $wf_action;
1298    if ($wf_info->{state}->{autoselect}) {
1299        $wf_action = $wf_info->{state}->{autoselect};
1300        $self->logger()->debug("Autoselect set: $wf_action");
1301    } else {
1302        $wf_action = $self->__get_next_auto_action($wf_info);
1303    }
1304
1305    # If we call the token action from within a result list we want
1306    # to "break out" and set the new url instead rendering the result inline
1307    if ($wf_args->{redirect}) {
1308        # Check if we can auto-load the next available action
1309        my $redirect = 'workflow!load!wf_id!'.$wf_info->{workflow}->{id};
1310        if ($wf_action) {
1311            $redirect .= '!wf_action!'.$wf_action;
1312        }
1313        $self->redirect($redirect);
1314        return $self;
1315    }
1316
1317    if ($wf_action) {
1318        $self->__render_from_workflow({ wf_info => $wf_info, wf_action => $wf_action });
1319    } else {
1320        $self->__render_from_workflow({ wf_info => $wf_info });
1321    }
1322
1323    return $self;
1324
1325}
1326
1327=head2 action_handle
1328
1329Execute a workflow internal action (fail, resume, wakeup, archive). Requires
1330the workflow and action to be set in the wf_token info.
1331
1332=cut
1333
1334sub action_handle {
1335
1336    my $self = shift;
1337    my $args = shift;
1338
1339    my $wf_token = $self->param('wf_token') || '';
1340
1341    my $wf_info;
1342    # wf_token found, so its a real action
1343    if (!$wf_token) {
1344        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_INVALID_REQUEST_ACTION_WITHOUT_TOKEN!','error');
1345        return $self;
1346    }
1347
1348    my $wf_args = $self->__fetch_wf_token( $wf_token );
1349
1350    if (!$wf_args->{wf_id}) {
1351        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_INVALID_REQUEST_HANDLE_WITHOUT_ID!','error');
1352        return $self;
1353    }
1354
1355    my $handle = $wf_args->{wf_handle};
1356
1357    if (!$wf_args->{wf_handle}) {
1358        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_INVALID_REQUEST_HANDLE_WITHOUT_ACTION!','error');
1359        return $self;
1360    }
1361
1362    Log::Log4perl::MDC->put('wfid', $wf_args->{wf_id});
1363
1364
1365    if ('fail' eq $handle) {
1366        $self->logger()->info(sprintf "Workflow %01d set to failure by operator", $wf_args->{wf_id} );
1367
1368        $wf_info = $self->send_command_v2( 'fail_workflow', {
1369            id => $wf_args->{wf_id},
1370        });
1371    } elsif ('wakeup' eq $handle) {
1372        $self->logger()->info(sprintf "Workflow %01d trigger wakeup", $wf_args->{wf_id} );
1373        $wf_info = $self->send_command_v2( 'wakeup_workflow', {
1374            id => $wf_args->{wf_id}, async => 1, wait => 1
1375        });
1376    } elsif ('resume' eq $handle) {
1377        $self->logger()->info(sprintf "Workflow %01d trigger resume", $wf_args->{wf_id} );
1378        $wf_info = $self->send_command_v2( 'resume_workflow', {
1379            id => $wf_args->{wf_id}, async => 1, wait => 1
1380        });
1381    } elsif ('reset' eq $handle) {
1382        $self->logger()->info(sprintf "Workflow %01d trigger reset", $wf_args->{wf_id} );
1383        $wf_info = $self->send_command_v2( 'reset_workflow', {
1384            id => $wf_args->{wf_id}
1385        });
1386    } elsif ('archive' eq $handle) {
1387        $self->logger()->info(sprintf "Workflow %01d trigger archive", $wf_args->{wf_id} );
1388        $wf_info = $self->send_command_v2( 'archive_workflow', {
1389            id => $wf_args->{wf_id}
1390        });
1391    }
1392
1393    $self->__render_from_workflow({ wf_info => $wf_info });
1394
1395    return $self;
1396
1397}
1398
1399=head2 action_load
1400
1401Load a workflow given by wf_id, redirects to init_load
1402
1403=cut
1404
1405sub action_load {
1406
1407    my $self = shift;
1408    my $args = shift;
1409
1410    $self->redirect('workflow!load!wf_id!'.$self->param('wf_id').'!_seed!'.time() );
1411    return $self;
1412
1413}
1414
1415=head2 action_select
1416
1417Handle requests to states that have more than one action.
1418Needs to reference an exisiting workflow either via C<wf_token> or C<wf_id> and
1419the action to choose with C<wf_action>. If the selected action does not require
1420any input parameters (has no fields) and does not have an ui override set, the
1421action is executed immediately and the resulting state is used. Otherwise,
1422the selected action is preset and the current state is passed to the
1423__render_from_workflow method.
1424
1425=cut
1426
1427sub action_select {
1428
1429    my $self = shift;
1430    my $args = shift;
1431
1432    my $wf_action =  $self->param('wf_action');
1433    $self->logger()->debug('activity select ' . $wf_action);
1434
1435    # can be either token or id
1436    my $wf_id = $self->param('wf_id');
1437    if (!$wf_id) {
1438        my $wf_token = $self->param('wf_token');
1439        my $wf_args = $self->__fetch_wf_token( $wf_token );
1440        $wf_id = $wf_args->{wf_id};
1441        if (!$wf_id) {
1442            $self->logger()->error('No workflow id given');
1443            $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_UNABLE_TO_LOAD_WORKFLOW_INFORMATION','error');
1444            return $self;
1445        }
1446    }
1447
1448    Log::Log4perl::MDC->put('wfid', $wf_id);
1449    my $wf_info = $self->send_command_v2( 'get_workflow_info', {
1450        id => $wf_id,
1451        with_ui_info => 1,
1452    });
1453    $self->logger()->trace('wf_info ' . Dumper  $wf_info) if $self->logger->is_trace;
1454
1455    if (!$wf_info) {
1456        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_UNABLE_TO_LOAD_WORKFLOW_INFORMATION','error');
1457        return $self;
1458    }
1459
1460    # If the activity has no fields and no ui class we proceed immediately
1461    # FIXME - really a good idea - intentional stop items without fields?
1462    my $wf_action_info = $wf_info->{activity}->{$wf_action};
1463    $self->logger()->trace('wf_action_info ' . Dumper  $wf_action_info) if $self->logger->is_trace;
1464    if ((!$wf_action_info->{field} || (scalar @{$wf_action_info->{field}}) == 0) &&
1465        !$wf_action_info->{uihandle}) {
1466
1467        $self->logger()->debug('activity has no input - execute');
1468
1469        # send input data to workflow
1470        $wf_info = $self->send_command_v2( 'execute_workflow_activity', {
1471            id       => $wf_info->{workflow}->{id},
1472            activity => $wf_action,
1473            ui_info  => 1
1474        });
1475
1476        $args->{wf_action} = $self->__get_next_auto_action($wf_info);
1477
1478    } else {
1479
1480        $args->{wf_action} = $wf_action;
1481    }
1482
1483    $args->{wf_info} = $wf_info;
1484
1485    $self->__render_from_workflow( $args );
1486
1487    return $self;
1488}
1489
1490=head2 action_search
1491
1492Handler for the workflow search dialog, consumes the data from the
1493search form and displays the matches as a grid.
1494
1495=cut
1496
1497sub action_search {
1498
1499    my $self = shift;
1500    my $args = shift;
1501
1502    my $query = { $self->__tenant() };
1503    my $verbose = {};
1504    my $input;
1505
1506    if (my $type = $self->param('wf_type')) {
1507        $query->{type} = $type;
1508        $input->{wf_type} = $type;
1509        $verbose->{wf_type} = $type;
1510    }
1511
1512    if (my $state = $self->param('wf_state')) {
1513        $query->{state} = $state;
1514        $input->{wf_state} = $state;
1515        $verbose->{wf_state} = $state;
1516    }
1517
1518    if (my $proc_state = $self->param('wf_proc_state')) {
1519        $query->{proc_state} = $proc_state;
1520        $input->{wf_proc_state} = $proc_state;
1521        $verbose->{wf_proc_state} = $self->__get_proc_state_label($proc_state);
1522    }
1523
1524    if (my $last_update_before = $self->param('last_update_before')) {
1525        $query->{last_update_before} = $last_update_before;
1526        $input->{last_update} = { key => 'last_update_before', value => $last_update_before };
1527        $verbose->{last_update_before} = DateTime->from_epoch( epoch => $last_update_before )->iso8601();
1528    }
1529
1530    if (my $last_update_after = $self->param('last_update_after')) {
1531        $query->{last_update_after} = $last_update_after;
1532        $input->{last_update} = { key => 'last_update_after', value => $last_update_after };
1533        $verbose->{last_update_after} = DateTime->from_epoch( epoch => $last_update_after )->iso8601();
1534    }
1535
1536    # Read the query pattern for extra attributes from the session
1537    my $spec = $self->_session->param('wfsearch')->{default};
1538    my $attr = $self->__build_attribute_subquery( $spec->{attributes} );
1539
1540    if (my $wf_creator = $self->param('wf_creator')) {
1541        $input->{wf_creator} = $wf_creator;
1542        $attr->{'creator'} = scalar $wf_creator;
1543        $verbose->{wf_creator} = $wf_creator;
1544    }
1545
1546    if ($attr) {
1547        $input->{attributes} = $self->__build_attribute_preset(  $spec->{attributes} );
1548        $query->{attribute} = $attr;
1549    }
1550
1551    # check if there is a custom column set defined
1552    my ($header,  $body, $rattrib);
1553    if ($spec->{cols} && ref $spec->{cols} eq 'ARRAY') {
1554        ($header, $body, $rattrib) = $self->__render_list_spec( $spec->{cols} );
1555    } else {
1556        $body = $self->__default_grid_row;
1557        $header = $self->__default_grid_head;
1558    }
1559
1560    $query->{return_attributes} = $rattrib if ($rattrib);
1561
1562    $self->logger()->trace("query : " . Dumper $query) if $self->logger->is_trace;
1563
1564    my $result_count = $self->send_command_v2( 'search_workflow_instances_count', $query );
1565
1566    # No results founds
1567    if (!$result_count) {
1568        # if $result_count is undefined there was an error with the query
1569        # status was set to the error message from the run_command sub
1570        $self->set_status('I18N_OPENXPKI_UI_SEARCH_HAS_NO_MATCHES','error') if (defined $result_count);
1571        return $self->init_search({ preset => $input });
1572    }
1573
1574
1575    my @criteria;
1576    foreach my $item ((
1577        { name => 'wf_type', label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_TYPE_LABEL' },
1578        { name => 'wf_proc_state', label => 'I18N_OPENXPKI_UI_WORKFLOW_PROC_STATE_LABEL' },
1579        { name => 'wf_state', label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_STATE_LABEL' },
1580        { name => 'wf_creator', label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_CREATOR_LABEL'}
1581        )) {
1582        my $val = $verbose->{ $item->{name} };
1583        next unless ($val);
1584        $val =~ s/[^\w\s*\,]//g;
1585        push @criteria, sprintf '<nobr><b>%s:</b> <i>%s</i></nobr>', $item->{label}, $val;
1586    }
1587
1588    foreach my $item (@{$self->__validity_options()}) {
1589        my $val = $verbose->{ $item->{value} };
1590        next unless ($val);
1591        push @criteria, sprintf '<nobr><b>%s:</b> <i>%s</i></nobr>', $item->{label}, $val;
1592    }
1593
1594    my $queryid = $self->__generate_uid();
1595    $self->_client->session()->param('query_wfl_'.$queryid, {
1596        'id' => $queryid,
1597        'type' => 'workflow',
1598        'count' => $result_count,
1599        'query' => $query,
1600        'input' => $input,
1601        'header' => $header,
1602        'column' => $body,
1603        'pager'  => $spec->{pager} || {},
1604        'criteria' => \@criteria
1605    });
1606
1607    $self->redirect( 'workflow!result!id!'.$queryid  );
1608
1609    return $self;
1610
1611}
1612
1613=head2 action_bulk
1614
1615Receive a list of workflow serials (I<wf_id>) plus a workflow action
1616(I<wf_action>) to execute on those workflows. For each given serial the given
1617action is executed. The resulting state for each workflow is shown in a grid
1618table. Methods that require additional parameters are not supported yet.
1619
1620=cut
1621
1622sub action_bulk {
1623
1624    my $self = shift;
1625
1626    my $wf_token = $self->param('wf_token') || '';
1627    if (!$wf_token) {
1628        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_INVALID_REQUEST_ACTION_WITHOUT_TOKEN!','error');
1629        return $self;
1630    }
1631
1632    # token contains the name of the action to do and extra params
1633    my $wf_args = $self->__fetch_wf_token( $wf_token );
1634    if (!$wf_args->{wf_action}) {
1635        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_INVALID_REQUEST_HANDLE_WITHOUT_ACTION!','error');
1636        return $self;
1637    }
1638
1639    $self->logger()->trace('Doing bulk with arguments: '. Dumper $wf_args) if $self->logger->is_trace;
1640
1641    # wf_token is also used as name of the form field
1642    my @serials = $self->multi_param($wf_token);
1643
1644    my @success; # list of wf_info results
1645    my $errors; # hash with wf_id => error
1646
1647    my ($command, %params);
1648    if ($wf_args->{wf_action} =~ m{(fail|wakeup|resume|reset)}) {
1649        $command = $wf_args->{wf_action}.'_workflow';
1650        %params = %{$wf_args->{params}} if ($wf_args->{params});
1651    } elsif ($wf_args->{wf_action} =~ m{\w+_\w+}) {
1652        $command = 'execute_workflow_activity';
1653        $params{activity} = $wf_args->{wf_action};
1654        $params{params} = %{$wf_args->{params}} if ($wf_args->{params});
1655    }
1656    # run in background
1657    $params{async} = 1 if ($wf_args->{async});
1658
1659
1660    if (!$command) {
1661        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_INVALID_REQUEST_HANDLE_WITHOUT_ACTION!','error');
1662        return $self;
1663    }
1664
1665    $self->logger()->debug("Run command $command on workflows " . join(", ", @serials));
1666
1667    $self->logger()->trace('Execute parameters ' . Dumper \%params) if ($self->logger()->is_trace);
1668
1669    foreach my $id (@serials) {
1670
1671        my $wf_info;
1672        eval {
1673            $wf_info = $self->send_command_v2( $command , { id => $id, %params } );
1674        };
1675
1676        # send_command returns undef if there is an error which usually means
1677        # that the action was not successful. We can slurp the verbose error
1678        # from the result status item and display it in the table
1679        if (!$wf_info) {
1680            $errors->{$id} = $self->_status()->{message} || 'I18N_OPENXPKI_UI_APPLICATION_ERROR';
1681        } else {
1682            push @success, $wf_info;
1683            $self->logger()->trace('Result on '.$id.': '. Dumper $wf_info) if $self->logger->is_trace;
1684        }
1685    }
1686
1687    $self->_page({
1688        label => 'I18N_OPENXPKI_UI_WORKFLOW_BULK_RESULT_LABEL',
1689        description => 'I18N_OPENXPKI_UI_WORKFLOW_BULK_RESULT_DESC',
1690    });
1691
1692    if ($errors) {
1693
1694        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_BULK_RESULT_HAS_FAILED_ITEMS_STATUS', 'error');
1695
1696        my @failed_id = keys %{$errors};
1697        my $failed_result = $self->send_command_v2( 'search_workflow_instances', { id => \@failed_id, $self->__tenant() } );
1698
1699        my @result_failed = $self->__render_result_list( $failed_result, $self->__default_grid_row );
1700
1701        # push the error to the result
1702        my $pos_serial = 4;
1703        my $pos_state = 3;
1704        map {
1705            my $serial = $_->[ $pos_serial ];
1706            $_->[ $pos_state ] = $errors->{$serial};
1707        } @result_failed;
1708
1709        $self->logger()->trace('Mangled failed result: '. Dumper \@result_failed) if $self->logger->is_trace;
1710
1711        my @fault_head = @{$self->__default_grid_head};
1712        $fault_head[$pos_state] = { sTitle => 'Error' };
1713
1714        $self->add_section({
1715            type => 'grid',
1716            className => 'workflow',
1717            content => {
1718                label => 'I18N_OPENXPKI_UI_WORKFLOW_BULK_RESULT_FAILED_ITEMS_LABEL',
1719                description => 'I18N_OPENXPKI_UI_WORKFLOW_BULK_RESULT_FAILED_ITEMS_DESC',
1720                actions => [{
1721                    path => 'workflow!info!wf_id!{serial}',
1722                    label => 'I18N_OPENXPKI_UI_WORKFLOW_OPEN_WORKFLOW_LABEL',
1723                    icon => 'view',
1724                    target => 'popup',
1725                }],
1726                columns => \@fault_head,
1727                data => \@result_failed,
1728                empty => 'I18N_OPENXPKI_UI_TASK_LIST_EMPTY_LABEL',
1729            }
1730        });
1731    } else {
1732        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_BULK_RESULT_ACTION_SUCCESS_STATUS', 'success');
1733    }
1734
1735    if (@success) {
1736
1737        my @result_done = $self->__render_result_list( \@success, $self->__default_grid_row );
1738
1739        $self->add_section({
1740            type => 'grid',
1741            className => 'workflow',
1742            content => {
1743                label => 'I18N_OPENXPKI_UI_WORKFLOW_BULK_RESULT_SUCCESS_ITEMS_LABEL',
1744                description => $params{async} ?
1745                    'I18N_OPENXPKI_UI_WORKFLOW_BULK_RESULT_ASYNC_ITEMS_DESC' :
1746                    'I18N_OPENXPKI_UI_WORKFLOW_BULK_RESULT_SUCCESS_ITEMS_DESC',
1747                actions => [{
1748                    path => 'workflow!info!wf_id!{serial}',
1749                    label => 'I18N_OPENXPKI_UI_WORKFLOW_OPEN_WORKFLOW_LABEL',
1750                    icon => 'view',
1751                    target => 'popup',
1752                }],
1753                columns => $self->__default_grid_head,
1754                data => \@result_done,
1755                empty => 'I18N_OPENXPKI_UI_TASK_LIST_EMPTY_LABEL',
1756            }
1757        });
1758    }
1759
1760    # persist the selected ids and add button to recheck the status
1761    my $queryid = $self->__generate_uid();
1762    $self->_client->session()->param('query_wfl_'.$queryid, {
1763        'id' => $queryid,
1764        'type' => 'workflow',
1765        'count' => scalar @serials,
1766        'query' => { id => \@serials },
1767    });
1768
1769    $self->add_section({
1770        type => 'text',
1771        content => {
1772            buttons => [{
1773                label => 'I18N_OPENXPKI_UI_WORKFLOW_BULK_RECHECK_BUTTON',
1774                page => 'redirect!workflow!result!id!' .$queryid,
1775                format => 'expected',
1776            }]
1777        }
1778    });
1779
1780}
1781
1782=head1 internal methods
1783
1784=head2 __render_from_workflow ( { wf_id, wf_info, wf_action }  )
1785
1786Internal method that renders the ui components from the current workflow state.
1787The info about the current workflow can be passed as a workflow info hash as
1788returned by the get_workflow_info api method or simply the workflow
1789id. In states with multiple action, the wf_action parameter can tell
1790the method to proceed with this state.
1791
1792=head3 activity selection
1793
1794If a state has multiple available activities, and no activity is given via
1795wf_action, the page includes the content of the description tag of the state
1796(or the workflow) and a list of buttons rendered from the description of the
1797available actions. For actions without a description tag, the action name is
1798used. If a user clicks one of the buttons, the call gets dispatched to the
1799action_select method.
1800
1801=head3 activity rendering
1802
1803If the state has only one available activity or wf_action is given, the method
1804loads the list of input fields from the workflow definition and renders one
1805form field per parameter, exisiting context values are filled in.
1806
1807The type attribute tells how to render the field, accepted basic html types are
1808
1809    text, hidden, password, textarea, select, checkbox
1810
1811TODO: stuff below not implemented yet!
1812
1813For select and checkbox you need to pass suitable options using the source_list
1814or source_class attribute as described in the Workflow manual.
1815
1816TODO: Meta definitons, custom config
1817
1818=head3 custom handler
1819
1820You can override the default rendering by setting the uihandle attribute either
1821in the state or in the action defintion. A handler on the state level will
1822always be called regardless of the internal workflow state, a handler on the
1823action level gets called only if the action is selected by above means.
1824
1825=cut
1826
1827sub __render_from_workflow {
1828
1829    my $self = shift;
1830    my $args = shift;
1831
1832    $self->logger()->trace( "render args: " . Dumper $args) if $self->logger->is_trace;
1833
1834    my $wf_info = $args->{wf_info} || undef;
1835    my $view = $args->{view} || '';
1836
1837    if (!$wf_info && $args->{id}) {
1838        $wf_info = $self->send_command_v2( 'get_workflow_info', {
1839            id => $args->{id},
1840            with_ui_info => 1,
1841        });
1842        $args->{wf_info} = $wf_info;
1843    }
1844
1845    $self->logger()->trace( "wf_info: " . Dumper $wf_info) if $self->logger->is_trace;
1846    if (!$wf_info) {
1847        $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_UNABLE_TO_LOAD_WORKFLOW_INFORMATION','error');
1848        return $self;
1849    }
1850
1851    # delegate handling to custom class
1852    if ($wf_info->{state}->{uihandle}) {
1853        return $self->__delegate_call($wf_info->{state}->{uihandle}, $args);
1854    }
1855
1856    my $wf_action;
1857    if($args->{wf_action}) {
1858        if (!$wf_info->{activity}->{$args->{wf_action}}) {
1859            $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_REQUESTED_ACTION_NOT_AVAILABLE','warn');
1860        } else {
1861            $wf_action = $args->{wf_action};
1862        }
1863    }
1864
1865    my $wf_proc_state = $wf_info->{workflow}->{proc_state} || 'init';
1866
1867    # add buttons for manipulative handles (wakeup, fail, reset, resume)
1868    # to be added to the default button list
1869
1870    my @handles;
1871    my @buttons_handle;
1872    if ($wf_info->{handles} && ref $wf_info->{handles} eq 'ARRAY') {
1873        @handles = @{$wf_info->{handles}};
1874
1875        $self->logger()->debug('Adding global actions ' . join('/', @handles));
1876
1877        if (grep /\A wakeup \Z/x, @handles) {
1878            my $token = $self->__register_wf_token( $wf_info, { wf_handle => 'wakeup' } );
1879            push @buttons_handle, {
1880                label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_WAKEUP_BUTTON',
1881                action => 'workflow!handle!wf_token!'.$token->{value},
1882                format => 'exceptional'
1883            }
1884        }
1885
1886        if (grep /\A resume \Z/x, @handles) {
1887            my $token = $self->__register_wf_token( $wf_info, { wf_handle => 'resume' } );
1888            push @buttons_handle, {
1889                label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_RESUME_BUTTON',
1890                action => 'workflow!handle!wf_token!'.$token->{value},
1891                format => 'exceptional'
1892            };
1893        }
1894
1895        if (grep /\A reset \Z/x, @handles) {
1896            my $token = $self->__register_wf_token( $wf_info, { wf_handle => 'reset' } );
1897            push @buttons_handle, {
1898                label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_RESET_BUTTON',
1899                action => 'workflow!handle!wf_token!'.$token->{value},
1900                format => 'reset',
1901                confirm => {
1902                    label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_RESET_DIALOG_LABEL',
1903                    description => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_RESET_DIALOG_TEXT',
1904                    confirm_label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_RESET_DIALOG_CONFIRM_BUTTON',
1905                    cancel_label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_RESET_DIALOG_CANCEL_BUTTON',
1906                }
1907            };
1908        }
1909
1910        if (grep /\A fail \Z/x, @handles) {
1911            my $token = $self->__register_wf_token( $wf_info, { wf_handle => 'fail' } );
1912            push @buttons_handle, {
1913                label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_FAILURE_BUTTON',
1914                action => 'workflow!handle!wf_token!'.$token->{value},
1915                format => 'failure',
1916                confirm => {
1917                    label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_FAILURE_DIALOG_LABEL',
1918                    description => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_FAILURE_DIALOG_TEXT',
1919                    confirm_label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_FAILURE_DIALOG_CONFIRM_BUTTON',
1920                    cancel_label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_FAILURE_DIALOG_CANCEL_BUTTON',
1921                }
1922            };
1923        }
1924
1925        if (grep /\A archive \Z/x, @handles) {
1926            my $token = $self->__register_wf_token( $wf_info, { wf_handle => 'archive' } );
1927            push @buttons_handle, {
1928                label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_ARCHIVING_BUTTON',
1929                action => 'workflow!handle!wf_token!'.$token->{value},
1930                format => 'exceptional',
1931                confirm => {
1932                    label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_ARCHIVING_DIALOG_LABEL',
1933                    description => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_ARCHIVING_DIALOG_TEXT',
1934                    confirm_label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_ARCHIVING_DIALOG_CONFIRM_BUTTON',
1935                    cancel_label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_ARCHIVING_DIALOG_CANCEL_BUTTON',
1936                }
1937            };
1938        }
1939    }
1940
1941    # we set the breadcrumb only if the workflow has a title set
1942    # fallback to label if title is not DEFINED is done in the API
1943    # setting title to the empty string will suppress breadcrumbs
1944    my @breadcrumb;
1945    if ($wf_info->{workflow}->{title}) {
1946        if ($wf_info->{workflow}->{id}) {
1947            push @breadcrumb, {
1948                className => 'workflow-type' ,
1949                label => sprintf("%s (#%01d)", $wf_info->{workflow}->{title}, $wf_info->{workflow}->{id})
1950            };
1951        } elsif ($wf_info->{workflow}->{state} eq 'INITIAL') {
1952            push @breadcrumb, {
1953                className => 'workflow-type',
1954                label => sprintf("%s", $wf_info->{workflow}->{title})
1955            };
1956        }
1957    }
1958
1959    # helper sub to render the pages description text from state/action using a template
1960    my $templated_description = sub {
1961        my $page_def = shift;
1962        my $description;
1963        if ($page_def->{template}) {
1964            my $user = $self->_client->session()->param('user');
1965            $description = $self->send_command_v2( 'render_template', {
1966                template => $page_def->{template}, params => {
1967                    context => $wf_info->{workflow}->{context},
1968                    user => { name => $user->{name},  role => $user->{role} },
1969                },
1970            });
1971        }
1972        return  $description || $page_def->{description} || '';
1973    };
1974
1975    # show buttons to proceed with workflow if it's in "non-regular" state
1976    my %irregular = (
1977        running => 'I18N_OPENXPKI_UI_WORKFLOW_STATE_RUNNING_DESC',
1978        pause => 'I18N_OPENXPKI_UI_WORKFLOW_STATE_PAUSE_DESC',
1979        exception => 'I18N_OPENXPKI_UI_WORKFLOW_STATE_EXCEPTION_DESC',
1980        retry_exceeded => 'I18N_OPENXPKI_UI_WORKFLOW_STATE_RETRY_EXCEEDED_DESC',
1981    );
1982    if ($irregular{$wf_proc_state}) {
1983
1984        # same page head for all proc states
1985        my $wf_action = $wf_info->{workflow}->{context}->{wf_current_action};
1986        my $wf_action_info = $wf_info->{activity}->{ $wf_action };
1987
1988        if (@breadcrumb && $wf_info->{state}->{label}) {
1989            push @breadcrumb, { className => 'workflow-state', label => $wf_info->{state}->{label} };
1990        }
1991
1992        my $label = $self->__get_proc_state_label($wf_proc_state); # reuse labels from init_info popup
1993        my $desc = $irregular{$wf_proc_state};
1994
1995        $self->_page({
1996            label => $label,
1997            breadcrumb => \@breadcrumb,
1998            shortlabel => $wf_info->{workflow}->{id},
1999            description => $desc,
2000            className => 'workflow workflow-proc-state workflow-proc-'.$wf_proc_state,
2001            ($wf_info->{workflow}->{id} ? (canonical_uri => 'workflow!load!wf_id!'.$wf_info->{workflow}->{id}) : ()),
2002        });
2003
2004        my @buttons;
2005        my @fields;
2006        # Check if the workflow is in pause or exceeded
2007        if (grep /$wf_proc_state/, ('pause','retry_exceeded')) {
2008
2009            @fields = ({
2010                label => 'I18N_OPENXPKI_UI_WORKFLOW_LAST_UPDATE_LABEL',
2011                value => str2time($wf_info->{workflow}->{last_update}.' GMT'),
2012                'format' => 'timestamp'
2013            }, {
2014                label => 'I18N_OPENXPKI_UI_WORKFLOW_WAKEUP_AT_LABEL',
2015                value => $wf_info->{workflow}->{wake_up_at},
2016                'format' => 'timestamp'
2017            }, {
2018                label => 'I18N_OPENXPKI_UI_WORKFLOW_COUNT_TRY_LABEL',
2019                value => $wf_info->{workflow}->{count_try}
2020            }, {
2021                label => 'I18N_OPENXPKI_UI_WORKFLOW_PAUSE_REASON_LABEL',
2022                value => $wf_info->{workflow}->{context}->{wf_pause_msg}
2023            });
2024
2025            if ($wf_proc_state eq 'pause') {
2026
2027                # If wakeup is less than 300 seconds away, we schedule an
2028                # automated reload of the page
2029                my $to_sleep = $wf_info->{workflow}->{wake_up_at} - time();
2030                if ($to_sleep < 30) {
2031                    $self->refresh('workflow!load!wf_id!'.$wf_info->{workflow}->{id}, 30);
2032                    $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATE_WATCHDOG_PAUSED_30SEC','info');
2033                } elsif ($to_sleep < 300) {
2034                    $self->refresh('workflow!load!wf_id!'.$wf_info->{workflow}->{id}, $to_sleep + 30);
2035                    $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATE_WATCHDOG_PAUSED_5MIN','info');
2036                } else {
2037                    $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATE_WATCHDOG_PAUSED','info');
2038                }
2039
2040                @buttons = ({
2041                    page => 'redirect!workflow!load!wf_id!'.$wf_info->{workflow}->{id},
2042                    label => 'I18N_OPENXPKI_UI_WORKFLOW_STATE_WATCHDOG_PAUSED_RECHECK_BUTTON',
2043                    format => 'alternative'
2044                });
2045                push @fields, {
2046                    label => 'I18N_OPENXPKI_UI_WORKFLOW_PAUSED_ACTION_LABEL',
2047                    value => $wf_action_info->{label}
2048                };
2049            } else {
2050                $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATE_WATCHDOG_RETRY_EXCEEDED','error');
2051                push @fields, {
2052                    label => 'I18N_OPENXPKI_UI_WORKFLOW_EXCEPTION_FAILED_ACTION_LABEL',
2053                    value => $wf_action_info->{label}
2054                };
2055            }
2056
2057            # if there are output rules defined, we add them now
2058            if ( $wf_info->{state}->{output} ) {
2059                push @fields, @{$self->__render_fields( $wf_info, $view )};
2060            }
2061
2062        # if the workflow is currently runnig, show info without buttons
2063        } elsif ($wf_proc_state eq 'running') {
2064
2065            $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATE_RUNNING_LABEL','info');
2066
2067            @fields = ({
2068                    label => 'I18N_OPENXPKI_UI_WORKFLOW_LAST_UPDATE_LABEL',
2069                    value => str2time($wf_info->{workflow}->{last_update}.' GMT'),
2070                    format => 'timestamp'
2071                }, {
2072                    label => 'I18N_OPENXPKI_UI_WORKFLOW_ACTION_RUNNING_LABEL',
2073                    value => ($wf_info->{activity}->{$wf_action}->{label} || $wf_action)
2074            });
2075
2076            @buttons = ({
2077                page => 'redirect!workflow!load!wf_id!'.$wf_info->{workflow}->{id},
2078                label => 'I18N_OPENXPKI_UI_WORKFLOW_BULK_RECHECK_BUTTON',
2079                format => 'alternative'
2080            });
2081
2082            # we use the time elapsed to calculate the next update
2083            my $timeout = 15;
2084            if ( $wf_info->{workflow}->{last_update} ) {
2085                # elapsed time in MINUTES
2086                my $elapsed = (time() - str2time($wf_info->{workflow}->{last_update}.' GMT')) / 60;
2087                if ($elapsed > 240) {
2088                    $timeout = 15 * 60;
2089                } elsif ($elapsed > 1) {
2090                    # 4 hours = 15 min delay, 4 min = 1 min delay
2091                    $timeout = POSIX::floor(sqrt( $elapsed )) * 60;
2092                }
2093                $self->logger()->debug('Auto Refresh when running' . $elapsed .' / ' . $timeout );
2094            }
2095
2096            $self->refresh('workflow!load!wf_id!'.$wf_info->{workflow}->{id}, $timeout);
2097
2098        # workflow halted by exception
2099        } elsif ( $wf_proc_state eq 'exception') {
2100
2101            @fields = ({
2102                label => 'I18N_OPENXPKI_UI_WORKFLOW_LAST_UPDATE_LABEL',
2103                value => str2time($wf_info->{workflow}->{last_update}.' GMT'),
2104                'format' => 'timestamp'
2105            }, {
2106                label => 'I18N_OPENXPKI_UI_WORKFLOW_EXCEPTION_FAILED_ACTION_LABEL',
2107                value => $wf_action_info->{label}
2108            });
2109
2110            # add the exception text in case the user is allowed to see the context
2111            push @fields, {
2112                label => 'I18N_OPENXPKI_UI_WORKFLOW_EXCEPTION_MESSAGE_LABEL',
2113                value => $wf_info->{workflow}->{context}->{wf_exception},
2114            } if ((grep /context/, @handles) && $wf_info->{workflow}->{context}->{wf_exception});
2115
2116            # if there are output rules defined, we add them now
2117            if ( $wf_info->{state}->{output} ) {
2118                push @fields, @{$self->__render_fields( $wf_info, $view )};
2119            }
2120
2121            # if we come here from a failed action the status is set already
2122            if (!$self->_status()) {
2123                $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATE_EXCEPTION','error');
2124            }
2125
2126        } # end proc_state switch
2127
2128        $self->add_section({
2129            type => 'keyvalue',
2130            content => {
2131                data => \@fields,
2132                buttons => [ @buttons, @buttons_handle ]
2133        }});
2134
2135    # if there is one activity selected (or only one present), we render it now
2136    } elsif ($wf_action) {
2137
2138        my $wf_action_info = $wf_info->{activity}->{$wf_action};
2139        # if we fallback to the state label we dont want it in the 1
2140        my $label = $wf_action_info->{label};
2141        if ($label ne $wf_action) {
2142            if (@breadcrumb && $wf_info->{state}->{label}) {
2143                push @breadcrumb, { className => 'workflow-state', label => $wf_info->{state}->{label} };
2144            }
2145        } else {
2146            $label = $wf_info->{state}->{label};
2147        }
2148
2149        $self->_page({
2150            label => $label,
2151            breadcrumb => \@breadcrumb,
2152            shortlabel => $wf_info->{workflow}->{id},
2153            description =>  $templated_description->($wf_action_info),
2154            className => 'workflow workflow-action ' . ($wf_action_info->{uiclass} || ''),
2155            canonical_uri => sprintf('workflow!load!wf_id!%01d!wf_action!%s', $wf_info->{workflow}->{id}, $wf_action),
2156        });
2157
2158        # delegation based on activity
2159        if ($wf_action_info->{uihandle}) {
2160            return $self->__delegate_call($wf_action_info->{uihandle}, $args, $wf_action);
2161        }
2162
2163        $self->logger()->trace('activity info ' . Dumper $wf_action_info ) if $self->logger->is_trace;
2164
2165        # we allow prefill of the form if the workflow is started
2166        my $do_prefill = $wf_info->{workflow}->{state} eq 'INITIAL';
2167
2168        my $context = $wf_info->{workflow}->{context};
2169        my @fields;
2170        my @additional_fields;
2171        my @fielddesc;
2172
2173        foreach my $field (@{$wf_action_info->{field}}) {
2174
2175            my $name = $field->{name};
2176            next if ($name =~ m{ \A workflow_id }x);
2177            next if ($name =~ m{ \A wf_ }x);
2178            next if ($field->{type} && $field->{type} eq "server");
2179
2180            my $val = $self->param($name);
2181            if ($do_prefill && defined $val) {
2182                # XSS prevention - very rude, but if you need to pass something
2183                # more sophisticated use the wf_token technique
2184                $val =~ s/[^A-Za-z0-9_=,-\. ]//;
2185            } elsif (defined $context->{$name}) {
2186                $val = $context->{$name};
2187            } else {
2188                $val = undef;
2189            }
2190
2191            my ($item, @more_items) = $self->__render_input_field( $field, $val );
2192            next unless ($item);
2193
2194            push @fields, $item;
2195            push @additional_fields, @more_items;
2196
2197            # if the field has a description text, push it to the @fielddesc list
2198            my $descr = $field->{description};
2199            if ($descr && $descr !~ /^\s*$/ && $field->{type} ne 'hidden') {
2200                push @fielddesc, { label => $item->{label}, value => $descr, format => 'raw' };
2201            }
2202
2203        }
2204
2205        # Render the context values if there are no fields
2206        if (!scalar @fields) {
2207            $self->add_section({
2208                type => 'keyvalue',
2209                content => {
2210                    label => '',
2211                    description => '',
2212                    data => $self->__render_fields( $wf_info, $view ),
2213                    buttons => $self->__get_form_buttons( $wf_info ),
2214            }});
2215
2216        } else {
2217
2218            # record the workflow info in the session
2219            push @fields, $self->__register_wf_token( $wf_info, {
2220                wf_action => $wf_action,
2221                wf_fields => \@fields,
2222            });
2223
2224            $self->add_section({
2225                type => 'form',
2226                action => 'workflow',
2227                content => {
2228                    #label => $wf_action_info->{label},
2229                    #description => $wf_action_info->{description},
2230                    submit_label => $wf_action_info->{button} || 'I18N_OPENXPKI_UI_WORKFLOW_SUBMIT_BUTTON',
2231                    fields => [ @fields, @additional_fields ],
2232                    buttons => $self->__get_form_buttons( $wf_info ),
2233                }
2234            });
2235
2236            if (@fielddesc) {
2237                $self->add_section({
2238                    type => 'keyvalue',
2239                    content => {
2240                        label => 'I18N_OPENXPKI_UI_WORKFLOW_FIELD_HINT_LIST',
2241                        description => '',
2242                        data => \@fielddesc
2243                }});
2244            }
2245        }
2246    } else {
2247
2248        $self->_page({
2249            label => $wf_info->{state}->{label} || $wf_info->{workflow}->{title} || $wf_info->{workflow}->{label},
2250            breadcrumb => \@breadcrumb,
2251            shortlabel => $wf_info->{workflow}->{id},
2252            description =>  $templated_description->($wf_info->{state}),
2253            className => 'workflow workflow-page ' . ($wf_info->{state}->{uiclass} || ''),
2254            ($wf_info->{workflow}->{id} ? (canonical_uri => 'workflow!load!wf_id!'.$wf_info->{workflow}->{id}) : ()),
2255        });
2256
2257        # Set status decorator on final states (uses proc_state).
2258        # To finalize without status message use state name "NOSTATUS".
2259        # Some field types are able to override the status during render so
2260        # this might not be the final status line!
2261        if ( $wf_info->{state}->{status} && ref $wf_info->{state}->{status} eq 'HASH' ) {
2262            $self->_status( $wf_info->{state}->{status} );
2263
2264        # Finished workflow
2265        } elsif ('finished' eq $wf_proc_state) {
2266            # add special colors for success and failure
2267            my $state = $wf_info->{workflow}->{state};
2268            if ('SUCCESS' eq $state) {
2269                $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATUS_SUCCESS', 'success');
2270            }
2271            elsif ('FAILURE' eq $state) {
2272                $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATUS_FAILURE', 'error');
2273            }
2274            elsif ('CANCELED' eq $state) {
2275                $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATUS_CANCELED', 'warn');
2276            }
2277            elsif ('NOSTATUS' ne $state) {
2278                $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATUS_MISC_FINAL', 'warn');
2279            }
2280
2281        # Archived workflow
2282        } elsif ('archived' eq $wf_proc_state) {
2283            $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATE_ARCHIVED', 'info');
2284
2285        # Forcibly failed workflow
2286        } elsif ('failed' eq $wf_proc_state) {
2287            $self->set_status('I18N_OPENXPKI_UI_WORKFLOW_STATE_FAILED', 'error');
2288        }
2289
2290        my $fields = $self->__render_fields( $wf_info, $view );
2291
2292        $self->logger()->trace('Field data ' . Dumper $fields) if $self->logger->is_trace;
2293
2294        # Add action buttons
2295        my $buttons = $self->__get_action_buttons( $wf_info ) ;
2296
2297        if (!@$fields && $wf_info->{workflow}->{state} eq 'INITIAL') {
2298            # initial step of workflow without fields
2299            $self->add_section({
2300                type => 'text',
2301                content => {
2302                    label => '',
2303                    description => '',
2304                    buttons => $buttons,
2305                }
2306            });
2307
2308        } else {
2309
2310            # state manual but no buttons -> user is waiting for a third party
2311            # to continue the workflow and might want to reload the page
2312            if ($wf_proc_state eq 'manual' && @{$buttons} == 0) {
2313                $buttons = [{
2314                    page => 'redirect!workflow!load!wf_id!'.$wf_info->{workflow}->{id},
2315                    label => 'I18N_OPENXPKI_UI_WORKFLOW_STATE_MANUAL_RECHECK_BUTTON',
2316                    format => 'alternative'
2317                }];
2318            }
2319
2320            my @fields = @{$fields};
2321
2322            # if we have no fields at all in the output we need an empty
2323            # section to make the UI happy and to show the buttons, if any
2324            $self->add_section({
2325                type => 'text',
2326                content => {
2327                    buttons => $buttons,
2328            }}) unless (@fields);
2329
2330            my @section_fields;
2331            while (my $field = shift @fields) {
2332
2333                # check if this field is a grid or chart
2334                if ($field->{format} !~ m{(grid|chart)}) {
2335                    push @section_fields, $field;
2336                    next;
2337                }
2338
2339                # check if we have normal fields on the stack to output
2340                if (@section_fields) {
2341                    $self->add_section({
2342                        type => 'keyvalue',
2343                        content => {
2344                            label => '',
2345                            description => '',
2346                            data => [ @section_fields ],
2347                    }});
2348                    @section_fields  = ();
2349                }
2350
2351                if ($field->{format} eq 'grid') {
2352                    $self->logger()->trace('Adding grid ' . Dumper $field) if $self->logger->is_trace;
2353                    $self->add_section({
2354                        type => 'grid',
2355                        className => 'workflow',
2356                        content => {
2357                            actions => ($field->{action} ? [{
2358                                path => $field->{action},
2359                                label => '',
2360                                icon => 'view',
2361                                target => ($field->{target} ? $field->{target} : 'tab'),
2362                            }] : undef),
2363                            columns =>  $field->{header},
2364                            data => $field->{value},
2365                            empty => 'I18N_OPENXPKI_UI_TASK_LIST_EMPTY_LABEL',
2366                            buttons => (@fields ? [] : $buttons), # add buttons if its the last item
2367                        }
2368                    });
2369                } elsif ($field->{format} eq 'chart') {
2370
2371                    $self->logger()->trace('Adding chart ' . Dumper $field) if $self->logger->is_trace;
2372                    $self->add_section({
2373                        type => 'chart',
2374                        content => {
2375                            label => $field->{label} || '',
2376                            options => $field->{options},
2377                            data => $field->{value},
2378                            empty => 'I18N_OPENXPKI_UI_TASK_LIST_EMPTY_LABEL',
2379                            buttons => (@fields ? [] : $buttons), # add buttons if its the last item
2380                        }
2381                    });
2382                }
2383            }
2384            # no chart/grid in the last position => output items on the stack
2385
2386            $self->add_section({
2387                type => 'keyvalue',
2388                content => {
2389                    label => '',
2390                    description => '',
2391                    data => \@section_fields,
2392                    buttons => $buttons,
2393            }}) if (@section_fields);
2394        }
2395    }
2396
2397    #
2398    # Right block
2399    #
2400    if ($wf_info->{workflow}->{id}) {
2401
2402        my $wfdetails_config = $self->_client->session()->param('wfdetails');
2403        # undef = no right box
2404        if (defined $wfdetails_config) {
2405
2406            if ($view eq 'result' && $wf_info->{workflow}->{proc_state} !~ /(finished|failed|archived)/) {
2407                push @buttons_handle, {
2408                    href => '#/openxpki/redirect!workflow!load!wf_id!'.$wf_info->{workflow}->{id},
2409                    label => 'I18N_OPENXPKI_UI_WORKFLOW_OPEN_WORKFLOW_LABEL',
2410                    format => "primary",
2411                };
2412            }
2413
2414            # assemble infos
2415            my $data = $self->__render_workflow_info( $wf_info, $wfdetails_config );
2416
2417            # The workflow info contains info about all control actions that
2418            # can done on the workflow -> render appropriate buttons.
2419            my $extra_handles;
2420            if (@handles) {
2421
2422                my @extra_links;
2423                if (grep /context/, @handles) {
2424                    push @extra_links, {
2425                        'page' => 'workflow!context!wf_id!'.$wf_info->{workflow}->{id},
2426                        'label' => 'I18N_OPENXPKI_UI_WORKFLOW_CONTEXT_LABEL',
2427                    };
2428                }
2429
2430                if (grep /attribute/, @handles) {
2431                    push @extra_links, {
2432                        'page' => 'workflow!attribute!wf_id!'.$wf_info->{workflow}->{id},
2433                        'label' => 'I18N_OPENXPKI_UI_WORKFLOW_ATTRIBUTE_LABEL',
2434                    };
2435                }
2436
2437                if (grep /history/, @handles) {
2438                    push @extra_links, {
2439                        'page' => 'workflow!history!wf_id!'.$wf_info->{workflow}->{id},
2440                        'label' => 'I18N_OPENXPKI_UI_WORKFLOW_HISTORY_LABEL',
2441                    };
2442                }
2443
2444                if (grep /techlog/, @handles) {
2445                    push @extra_links, {
2446                        'page' => 'workflow!log!wf_id!'.$wf_info->{workflow}->{id},
2447                        'label' => 'I18N_OPENXPKI_UI_WORKFLOW_LOG_LABEL',
2448                    };
2449                }
2450
2451                push @{$data}, {
2452                    label => 'I18N_OPENXPKI_UI_WORKFLOW_EXTRA_INFO_LABEL',
2453                    format => 'linklist',
2454                    value => \@extra_links
2455                } if (scalar @extra_links);
2456
2457            }
2458
2459            $self->_result()->{right} = [{
2460                type => 'keyvalue',
2461                content => {
2462                    label => '',
2463                    description => '',
2464                    data => $data,
2465                    buttons => \@buttons_handle,
2466            }}];
2467        }
2468    }
2469
2470    return $self;
2471
2472}
2473
2474=head2 __get_action_buttons
2475
2476For states having multiple actions, this helper renders a set of buttons to
2477dispatch to the next action. It expects a workflow info structure as single
2478parameter and returns a ref to a list to be put in the buttons field.
2479
2480=cut
2481
2482sub __get_action_buttons {
2483
2484    my $self = shift;
2485    my $wf_info = shift;
2486
2487    # The text hints for the action is encoded in the state
2488    my $btnhint = $wf_info->{state}->{button} || {};
2489
2490    my @buttons;
2491
2492    if ($btnhint->{_head}) {
2493        push @buttons, { section => $btnhint->{_head} };
2494    }
2495
2496    foreach my $wf_action (@{$wf_info->{state}->{option}}) {
2497        my $wf_action_info = $wf_info->{activity}->{$wf_action};
2498
2499        my %button = (
2500            label => $wf_action_info->{label},
2501            action => sprintf ('workflow!select!wf_action!%s!wf_id!%01d', $wf_action, $wf_info->{workflow}->{id}),
2502        );
2503
2504        # buttons in workflow start = only one initial start button
2505        %button = (
2506            label => ($wf_action_info->{label} ne $wf_action) ? $wf_action_info->{label} : 'I18N_OPENXPKI_UI_WORKFLOW_START_BUTTON',
2507            page => 'workflow!start!wf_type!'. $wf_info->{workflow}->{type},
2508        ) if (!$wf_info->{workflow}->{id});
2509
2510        # TODO - we should add some configuration option for this
2511        if ($wf_action =~ /global_cancel/) {
2512            $button{confirm} = {
2513                label => 'I18N_OPENXPKI_UI_WORKFLOW_CONFIRM_CANCEL_LABEL',
2514                description => 'I18N_OPENXPKI_UI_WORKFLOW_CONFIRM_CANCEL_DESCRIPTION',
2515                confirm_label => 'I18N_OPENXPKI_UI_WORKFLOW_CONFIRM_DIALOG_CONFIRM_BUTTON',
2516                cancel_label => 'I18N_OPENXPKI_UI_WORKFLOW_CONFIRM_DIALOG_CANCEL_BUTTON',
2517            };
2518            $button{format} = 'failure';
2519        }
2520
2521        if ($btnhint->{$wf_action}) {
2522            my $hint = $btnhint->{$wf_action};
2523            # a label at the button overrides the default label from the action
2524            foreach my $key (qw(description tooltip label)) {
2525                if ($hint->{$key}) {
2526                    $button{$key} = $hint->{$key};
2527                }
2528            }
2529            if ($hint->{format}) {
2530                $button{format} = $hint->{format};
2531            }
2532            if ($hint->{confirm}) {
2533                $button{confirm} = {
2534                    label => $hint->{confirm}->{label} || 'I18N_OPENXPKI_UI_PLEASE_CONFIRM_TITLE',
2535                    description => $hint->{confirm}->{description} || 'I18N_OPENXPKI_UI_PLEASE_CONFIRM_DESC',
2536                    confirm_label => $hint->{confirm}->{confirm} || 'I18N_OPENXPKI_UI_WORKFLOW_CONFIRM_DIALOG_CONFIRM_BUTTON',
2537                    cancel_label => $hint->{confirm}->{cancel} ||  'I18N_OPENXPKI_UI_WORKFLOW_CONFIRM_DIALOG_CANCEL_BUTTON',
2538                }
2539            }
2540            if ($hint->{break} && $hint->{break} =~ /(before|after)/) {
2541                $button{'break_'. $hint->{break}} = 1;
2542            }
2543
2544        }
2545        push @buttons, \%button;
2546
2547    }
2548
2549    $self->logger()->trace('Buttons are ' . Dumper \@buttons) if $self->logger->is_trace;
2550
2551    return \@buttons;
2552}
2553
2554sub __get_form_buttons {
2555
2556    my $self = shift;
2557    my $wf_info = shift;
2558    my @buttons;
2559
2560    my $activity_count = scalar keys %{$wf_info->{activity}};
2561    if ($wf_info->{activity}->{global_cancel}) {
2562        push @buttons, {
2563            label => 'I18N_OPENXPKI_UI_WORKFLOW_CANCEL_BUTTON',
2564            action => 'workflow!select!wf_action!global_cancel!wf_id!'. $wf_info->{workflow}->{id},
2565            format => 'cancel',
2566            confirm => {
2567                description => 'I18N_OPENXPKI_UI_WORKFLOW_CONFIRM_CANCEL_DESCRIPTION',
2568                label => 'I18N_OPENXPKI_UI_WORKFLOW_CONFIRM_CANCEL_LABEL',
2569                confirm_label => 'I18N_OPENXPKI_UI_WORKFLOW_CONFIRM_DIALOG_CONFIRM_BUTTON',
2570                cancel_label => 'I18N_OPENXPKI_UI_WORKFLOW_CONFIRM_DIALOG_CANCEL_BUTTON',
2571        }};
2572        $activity_count--;
2573    }
2574
2575    # if there is another activity besides global_cancel, we add a go back button
2576    if ($activity_count > 1) {
2577        unshift @buttons, {
2578            label => 'I18N_OPENXPKI_UI_WORKFLOW_RESET_BUTTON',
2579            page => 'redirect!workflow!load!wf_id!'.$wf_info->{workflow}->{id},
2580            format => 'reset',
2581        };
2582    }
2583
2584    if ($wf_info->{handles} && ref $wf_info->{handles} eq 'ARRAY' && (grep /fail/, @{$wf_info->{handles}})) {
2585        my $token = $self->__register_wf_token( $wf_info, { wf_handle => 'fail' } );
2586        push @buttons, {
2587            label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_FAILURE_BUTTON',
2588            action => 'workflow!handle!wf_token!'.$token->{value},
2589            format => 'terminate',
2590            confirm => {
2591                label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_FAILURE_DIALOG_LABEL',
2592                description => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_FAILURE_DIALOG_TEXT',
2593                confirm_label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_FAILURE_DIALOG_CONFIRM_BUTTON',
2594                cancel_label => 'I18N_OPENXPKI_UI_WORKFLOW_FORCE_FAILURE_DIALOG_CANCEL_BUTTON',
2595            }
2596        };
2597    }
2598
2599    return \@buttons;
2600}
2601
2602
2603sub __get_next_auto_action {
2604
2605    my $self = shift;
2606    my $wf_info = shift;
2607    my $wf_action;
2608
2609    # no auto action if the state has output rules defined
2610    return if (ref $wf_info->{state}->{output} eq 'ARRAY' &&
2611        scalar(@{$wf_info->{state}->{output}}) > 0);
2612
2613
2614    my @activities = keys %{$wf_info->{activity}};
2615    # only one valid activity found, so use it
2616    if (scalar @activities == 1) {
2617        $wf_action = $activities[0];
2618
2619    # do not count global_cancel as alternative selection
2620    } elsif (scalar @activities == 2 && (grep /global_cancel/, @activities)) {
2621        $wf_action = ($activities[1] eq 'global_cancel') ? $activities[0] : $activities[1];
2622
2623    }
2624
2625    return unless ($wf_action);
2626
2627    # do not load activities that do not have fields or a uihandle class
2628    return unless ($wf_info->{activity}->{$wf_action}->{field} ||
2629        $wf_info->{activity}->{$wf_action}->{uihandle});
2630
2631    $self->logger()->debug('Implicit autoselect of action ' . $wf_action ) if($wf_action);
2632
2633    return $wf_action;
2634
2635}
2636
2637
2638
2639=head2 __render_input_field
2640
2641Render the UI code for a input field from the server sided definition.
2642Does translation of labels and mangles values for multi-valued componentes.
2643
2644This method might dynamically create additional "helper" fields on-the-fly
2645(usually of type I<hidden>) and may thus return a list with several field
2646definitions.
2647
2648The first returned item is always the one corresponding to the workflow field.
2649
2650=cut
2651
2652sub __render_input_field {
2653
2654    my $self = shift;
2655    my $field = shift;
2656    my $value = shift;
2657
2658    die "__render_input_field() must be called in list context: it may return more than one field definition\n"
2659      unless wantarray;
2660
2661    my $name = $field->{name};
2662    next if ($name =~ m{ \A workflow_id }x);
2663    next if ($name =~ m{ \A wf_ }x);
2664
2665    my $type = $field->{type} || 'text';
2666
2667    # fields to be filled only by server sided workflows
2668    return if ($type eq "server");
2669
2670    # common attributes for all field types
2671    my $item = {
2672        name => $name,
2673        label => $field->{label} || $name,
2674        type => $type
2675    };
2676    $item->{placeholder} = $field->{placeholder} if ($field->{placeholder});
2677    $item->{tooltip} = $field->{tooltip} if ($field->{tooltip});
2678    $item->{clonable} = 1 if $field->{clonable};
2679    $item->{is_optional} = 1 unless $field->{required};
2680
2681    # includes dynamically generated additional fields
2682    my @all_items = ($item);
2683
2684    # type 'select' - fill in options
2685    if ($type eq 'select' and $field->{option}) {
2686        $item->{options} = $field->{option};
2687    }
2688
2689    # type 'cert_identifier'
2690    if ($type eq 'cert_identifier') {
2691        # special handling of preset value
2692        if ($value) {
2693            $item->{type} = 'static';
2694        }
2695        else {
2696            $item->{type} = 'text';
2697            $item->{autocomplete_query} = {
2698                action => "certificate!autocomplete",
2699                params => {
2700                    cert_identifier => $item->{name},
2701                },
2702            };
2703        }
2704    }
2705
2706    # type 'uploadarea' - transform into 'textarea'
2707    if ($type eq 'uploadarea') {
2708        $item->{type} = 'textarea';
2709        $item->{allow_upload} = 1;
2710    }
2711
2712    # option 'autocomplete'
2713    if ($field->{autocomplete}) {
2714        my ($ac_query_params, $enc_field) = $self->make_autocomplete_query($field);
2715        # "autocomplete_query" to distinguish it from the wf config param
2716        $item->{autocomplete_query} = {
2717            action => $field->{autocomplete}->{action},
2718            params => $ac_query_params,
2719        };
2720        # additional field definition
2721        push @all_items, $enc_field;
2722    }
2723
2724    if (defined $value) {
2725        # clonables need array as value
2726        if ($item->{clonable}) {
2727            if (ref $value eq 'ARRAY') {
2728                $item->{value} = $value;
2729            } elsif (OpenXPKI::Serialization::Simple::is_serialized($value)) {
2730                $item->{value} = $self->serializer()->deserialize($value);
2731            } elsif ($value) {
2732                $item->{value} = [ $value ];
2733            }
2734        } else {
2735            $item->{value} = $value;
2736        }
2737    } elsif ($field->{default}) {
2738        $item->{value} = $field->{default};
2739    }
2740
2741    if ($item->{type} eq 'static' && $field->{template}) {
2742        if (OpenXPKI::Serialization::Simple::is_serialized($value)) {
2743            $item->{value} = $self->serializer()->deserialize($value);
2744        }
2745        $item->{verbose} = $self->send_command_v2( 'render_template', { template => $field->{template}, params => $item } );
2746    }
2747
2748    # type 'encrypted'
2749    for (@all_items) {
2750        $_->{value} = $self->_encrypt_jwt($_->{value}) if $_->{type} eq 'encrypted';
2751    }
2752
2753    return @all_items;
2754
2755}
2756
2757# encrypt given data
2758sub _encrypt_jwt {
2759    my ($self, $value) = @_;
2760
2761    die "Only values of type HashRef are supported for encrypted input fields\n"
2762      unless ref $value eq 'HASH';
2763
2764    my $key = $self->_session->param('jwt_encryption_key');
2765    if (not $key) {
2766        $key = Crypt::PRNG::random_bytes(32);
2767        $self->_session->param('jwt_encryption_key', $key);
2768    }
2769
2770    my $token = encode_jwt(
2771        payload => $value,
2772        enc => 'A256CBC-HS512',
2773        alg => 'PBES2-HS512+A256KW', # uses "HMAC-SHA512" as the PRF and "AES256-WRAP" for the encryption scheme
2774        key => $key, # can be any length for PBES2-HS512+A256KW
2775        extra_headers => {
2776            p2c => 8000, # PBES2 iteration count
2777            p2s => 32,   # PBES2 salt length
2778        },
2779    );
2780
2781    return $token
2782}
2783
2784=head2 __delegate_call
2785
2786Used to delegate the rendering to another class, requires the method
2787to dispatch to as string (class + method using the :: notation) and
2788a ref to the args to be passed. If called from within an action, the
2789name of the action is passed as additonal parameter.
2790
2791=cut
2792sub __delegate_call {
2793
2794    my $self = shift;
2795    my $call = shift;
2796    my $args = shift;
2797    my $wf_action = shift || '';
2798
2799    my ($class, $method, $n, $param) = $call =~ /([\w\:\_]+)::([\w\_]+)(!([!\w]+))?/;
2800    $self->logger()->debug("delegate render to $class, $method" );
2801    eval "use $class; 1;";
2802    if ($param) {
2803        $class->$method( $self, $args, $wf_action, $param );
2804    } else {
2805        $class->$method( $self, $args, $wf_action );
2806    }
2807    return $self;
2808
2809}
2810
2811=head2 __render_result_list
2812
2813Helper to render the output result list from a sql query result.
2814adds exception/paused label to the state column and status class based on
2815proc and wf state.
2816
2817=cut
2818
2819sub __render_result_list {
2820
2821    my $self = shift;
2822    my $search_result = shift;
2823    my $colums = shift;
2824
2825    $self->logger()->trace("search result " . Dumper $search_result) if $self->logger->is_trace;
2826
2827    my @result;
2828
2829    my $wf_labels = $self->send_command_v2( 'get_workflow_instance_types' );
2830
2831    foreach my $wf_item (@{$search_result}) {
2832
2833        my @line;
2834        my ($wf_info, $context, $attrib);
2835
2836        # if we received a list of wf_info structures, we need to translate
2837        # the workflow hash into the database table format
2838        if ($wf_item->{workflow} && ref $wf_item->{workflow} eq 'HASH') {
2839            $wf_info = $wf_item;
2840            $context = $wf_info->{workflow}->{context};
2841            $attrib = $wf_info->{workflow}->{attribute};
2842            $wf_item = {
2843                'workflow_last_update' => $wf_info->{workflow}->{last_update},
2844                'workflow_id' => $wf_info->{workflow}->{id},
2845                'workflow_type' => $wf_info->{workflow}->{type},
2846                'workflow_state' => $wf_info->{workflow}->{state},
2847                'workflow_proc_state' => $wf_info->{workflow}->{proc_state},
2848                'workflow_wakeup_at' => $wf_info->{workflow}->{wake_up_at},
2849            };
2850        }
2851
2852        $wf_item->{workflow_label} = $wf_labels->{$wf_item->{workflow_type}}->{label};
2853        $wf_item->{workflow_description} = $wf_labels->{$wf_item->{workflow_type}}->{description};
2854
2855        foreach my $col (@{$colums}) {
2856
2857            my $field = lc($col->{field} // ''); # migration helper, lowercase uicontrol input
2858            $field = 'workflow_id' if ($field eq 'workflow_serial');
2859
2860            # we need to load the wf info
2861            my $colsrc = $col->{source} || '';
2862            if (!$wf_info && ($col->{template} || $colsrc eq 'context')) {
2863                $wf_info = $self->send_command_v2( 'get_workflow_info',  {
2864                    id => $wf_item->{'workflow_id'},
2865                    with_attributes => 1,
2866                });
2867                $self->logger()->trace( "fetch wf info : " . Dumper $wf_info) if $self->logger->is_trace;
2868                $context = $wf_info->{workflow}->{context};
2869                $attrib = $wf_info->{workflow}->{attribute};
2870            }
2871
2872            if ($col->{template}) {
2873                my $out;
2874                my $ttp = {
2875                    context => $context,
2876                    attribute => $attrib,
2877                    workflow => $wf_info->{workflow}
2878                };
2879                push @line, $self->send_command_v2( 'render_template', { template => $col->{template}, params => $ttp } );
2880
2881            } elsif ($colsrc eq 'workflow') {
2882
2883                # Special handling of the state field
2884                if ($field eq "workflow_state") {
2885                    my $state = $wf_item->{'workflow_state'};
2886                    my $proc_state = $wf_item->{'workflow_proc_state'};
2887
2888                    if (grep /\A $proc_state \Z/x, qw( exception pause retry_exceeded failed )) {
2889                        $state .= sprintf(" (%s)", $self->__get_proc_state_label($proc_state));
2890                    };
2891                    push @line, $state;
2892                } else {
2893                    push @line, $wf_item->{ $field };
2894
2895                }
2896
2897            } elsif ($colsrc eq 'context') {
2898                push @line, $context->{ $col->{field} };
2899            } elsif ($colsrc eq 'attribute') {
2900                push @line, $wf_item->{ $col->{field} }
2901            } elsif ($col->{field} eq 'creator') {
2902                push @line, $self->__render_creator_tooltip($wf_item->{creator}, $col);
2903            } else {
2904                # hu ?
2905            }
2906        }
2907
2908        # special color for workflows in final failure
2909
2910        my $status = $wf_item->{'workflow_proc_state'};
2911
2912        if ($status eq 'finished' && $wf_item->{'workflow_state'} eq 'FAILURE') {
2913            $status  = 'failed';
2914        }
2915
2916        push @line, $status;
2917
2918        push @result, \@line;
2919
2920    }
2921
2922    return @result;
2923
2924}
2925
2926=head2 __render_list_spec
2927
2928Create array to pass to UI from specification in config file
2929
2930=cut
2931
2932sub __render_list_spec {
2933
2934    my $self = shift;
2935    my $cols = shift;
2936
2937    my @header;
2938    my @column;
2939    my %attrib;
2940
2941    for (my $ii = 0; $ii < scalar @{$cols}; $ii++) {
2942
2943        # we must create a copy as we change the hash in the session info otherwise
2944        my %col = %{ $cols->[$ii] };
2945        my $field = $col{field} // ''; # prevent "Use of uninitialized value $col{"field"} in string eq"
2946        my $head = { sTitle => $col{label} };
2947
2948        if ($field eq 'creator') {
2949            $attrib{'creator'} = 1;
2950            $col{format} = 'tooltip';
2951
2952        } elsif ($field =~ m{\A (attribute)\.(\S+) }xi) {
2953            $col{source} = $1;
2954            $col{field} = $2;
2955            $attrib{$2} = 1;
2956
2957        } elsif ($field =~ m{\A (context)\.(\S+) }xi) {
2958            # we use this later to avoid the pattern match
2959            $col{source} = $1;
2960            $col{field} = $2;
2961
2962        } elsif (!$col{template}) {
2963            $col{source} = 'workflow';
2964            $col{field} = uc($col{field})
2965
2966        }
2967        push @column, \%col;
2968
2969        if ($col{sortkey}) {
2970            $head->{sortkey} = $col{sortkey};
2971        }
2972        if ($col{format}) {
2973            $head->{format} = $col{format};
2974        }
2975        push @header, $head;
2976    }
2977
2978    push @header, { sTitle => 'serial', bVisible => 0 };
2979    push @header, { sTitle => "_className"};
2980
2981    push @column, { source => 'workflow', field => 'workflow_id' };
2982
2983    return ( \@header, \@column, [ keys(%attrib) ] );
2984}
2985
2986=head2 __render_fields
2987
2988=cut
2989
2990sub __render_fields {
2991
2992    my $self = shift;
2993    my $wf_info = shift;
2994    my $view = shift;
2995
2996    my @fields;
2997    my $context = $wf_info->{workflow}->{context};
2998
2999    # in case we have output format rules, we just show the defined fields
3000    # can be overriden with view = context
3001    my $output = $wf_info->{state}->{output};
3002    my @fields_to_render;
3003
3004    if ($view eq 'context' && (grep /context/, @{$wf_info->{handles}})) {
3005        foreach my $field (sort keys %{$context}) {
3006            push @fields_to_render, { name => $field };
3007        }
3008    } elsif ($view eq 'attribute' && (grep /attribute/, @{$wf_info->{handles}})) {
3009        my $attr = $wf_info->{workflow}->{attribute};
3010        foreach my $field (sort keys %{$attr }) {
3011            push @fields_to_render, { name => $field, value => $attr->{$field} };
3012        }
3013    } elsif ($output) {
3014        @fields_to_render = @$output;
3015        # strip array indicator [] from field name
3016        for (@fields_to_render) { $_->{name} =~ s/\[\]$// if ($_->{name}) }
3017        $self->logger()->trace('Render output rules: ' . Dumper  \@fields_to_render) if $self->logger->is_trace;
3018
3019    } else {
3020        foreach my $field (sort keys %{$context}) {
3021            next if ($field =~ m{ \A (wf_|_|workflow_id|sources) }x);
3022            push @fields_to_render, { name => $field };
3023        }
3024        $self->logger()->trace('No output rules, render plain context: ' . Dumper  \@fields_to_render) if $self->logger->is_trace;
3025    }
3026
3027    my $queued; # receives header items that depend on non-empty sections
3028    ##! 64: "Context: " . Dumper($context)
3029    FIELD: foreach my $field (@fields_to_render) {
3030
3031        my $key = $field->{name} || '';
3032        ##! 64: "Context value for field $key: " . (defined $context->{$key} ? Dumper($context->{$key}) : '')
3033        my $item = {
3034            name => $key,
3035            value => $field->{value} // (defined $context->{$key} ? $context->{$key} : ''),
3036            format =>  $field->{format} || ''
3037        };
3038
3039
3040        if ($field->{uiclass}) {
3041            $item->{className} = $field->{uiclass};
3042        }
3043
3044        if ($item->{format} eq 'spacer') {
3045            push @fields, { format => 'head', className => $item->{className}||'spacer' };
3046            next FIELD;
3047        }
3048
3049        # Suppress key material, exceptions are vollatile and download fields
3050        if ($item->{value} =~ /-----BEGIN[^-]*PRIVATE KEY-----/ && $item->{format} ne 'download' && substr($key,0,1) ne '_') {
3051            $item->{value} = 'I18N_OPENXPKI_UI_WORKFLOW_SENSITIVE_CONTENT_REMOVED_FROM_CONTEXT';
3052        }
3053
3054        # Label, Description, Tooltip
3055        foreach my $prop (qw(label description tooltip preamble)) {
3056            if ($field->{$prop}) {
3057                $item->{$prop} = $field->{$prop};
3058            }
3059        }
3060
3061        if (!$item->{label}) {
3062            $item->{label} = $key;
3063        }
3064
3065        my $field_type = $field->{type} || '';
3066
3067        # we have several formats that might have non-scalar values
3068        if (OpenXPKI::Serialization::Simple::is_serialized( $item->{value} ) ) {
3069            $item->{value} = $self->serializer()->deserialize( $item->{value} );
3070        }
3071
3072        # auto-assign format based on some assumptions if no format is set
3073        if (!$item->{format}) {
3074
3075            # create a link on cert_identifier fields
3076            if ( $key =~ m{ cert_identifier \z }x ||
3077                $field_type eq 'cert_identifier') {
3078                $item->{format} = 'cert_identifier';
3079            }
3080
3081            # Code format any PEM blocks
3082            if ( $key =~ m{ \A (pkcs10|pkcs7) \z }x  ||
3083                $item->{value} =~ m{ \A -----BEGIN([A-Z ]+)-----.*-----END([A-Z ]+)---- }xms) {
3084                $item->{format} = 'code';
3085            } elsif ($field_type eq 'textarea') {
3086                $item->{format} = 'nl2br';
3087            }
3088
3089            if (ref $item->{value}) {
3090                if (ref $item->{value} eq 'HASH') {
3091                    $item->{format} = 'deflist';
3092                } elsif (ref $item->{value} eq 'ARRAY') {
3093                    $item->{format} = 'ullist';
3094                }
3095            }
3096            ##! 64: 'Auto applied format: ' . $item->{format}
3097        }
3098
3099        # convert format cert_identifier into a link
3100        if ($item->{format} eq "cert_identifier") {
3101            $item->{format} = 'link';
3102
3103            # do not create if the field is empty
3104            if ($item->{value}) {
3105                my $label = $item->{value};
3106
3107                my $cert_identifier = $item->{value};
3108                $item->{value}  = {
3109                    label => $label,
3110                    page => 'certificate!detail!identifier!'.$cert_identifier,
3111                    target => 'popup',
3112                    # label is usually formated to a human readable string
3113                    # but we sometimes need the raw value in the UI for extras
3114                    value => $cert_identifier,
3115                };
3116            }
3117
3118            $self->logger()->trace( 'item ' . Dumper $item) if $self->logger->is_trace;
3119
3120        # open another workflow - performs ACL check
3121        } elsif ($item->{format} eq "workflow_id") {
3122
3123            my $workflow_id = $item->{value};
3124            next FIELD unless($workflow_id);
3125
3126            my $can_access = $self->send_command_v2( 'check_workflow_acl',
3127                    { id => $workflow_id  });
3128
3129            if ($can_access) {
3130                $item->{format} = 'link';
3131                $item->{value}  = {
3132                    label => $workflow_id,
3133                    page => 'workflow!load!wf_id!'.$workflow_id,
3134                    target => '_blank',
3135                    value => $workflow_id,
3136                };
3137            } else {
3138                $item->{format} = '';
3139            }
3140
3141            $self->logger()->trace( 'item ' . Dumper $item) if $self->logger->is_trace;
3142
3143        # add a redirect command to the page
3144        } elsif ($item->{format} eq "redirect") {
3145
3146            if (ref $item->{value}) {
3147                my $v = $item->{value};
3148                my $target = $v->{target} || 'workflow!load!wf_id!'.$wf_info->{workflow}->{id};
3149                my $pause = $v->{pause} || 1;
3150                $self->refresh($target, $pause);
3151                if ($v->{label}) {
3152                    $self->set_status($v->{label}, ($v->{level} || 'info'));
3153                }
3154            } else {
3155                $self->redirect($item->{value});
3156            }
3157            # we dont want this to show up in the result so we unset its value
3158            $item = undef;
3159
3160        # create a link to download the given filename
3161        } elsif ($item->{format} =~ m{ \A download(\/([\w_\/-]+))? }xms ) {
3162
3163            # legacy - format is "download/mime/type"
3164            my $mime = $2 || 'application/octect-stream';
3165            $item->{format} = 'download';
3166
3167            # value is empty
3168            next FIELD unless($item->{value});
3169
3170            # parameters given in the field definition
3171            my $param = $field->{param} || {};
3172
3173            # Arguments for the UI field
3174            # label => STR           # text above the download field
3175            # type => "plain" | "base64" | "link",  # optional, default: "plain"
3176            # data => STR,           # plain data, Base64 data or URL
3177            # mimetype => STR,       # optional: mimetype passed to browser
3178            # filename => STR,       # optional: filename, default: depends on data
3179            # autodownload => BOOL,  # optional: set to 1 to auto-start download
3180            # hide => BOOL,          # optional: set to 1 to hide input and buttons (requires autodownload)
3181
3182            my $vv = $item->{value};
3183            # scalar value
3184            if (!ref $vv) {
3185                # if an explicit filename is set, we assume it is v3.10 or
3186                # later so we assume the value is the data and config is in
3187                # the field parameters
3188                if ($param->{filename}) {
3189                    $vv = { filename => $param->{filename}, data => $vv };
3190                } else {
3191                    $vv = { filename => $vv, source => 'file:'.$vv };
3192                }
3193            }
3194
3195            # very old legacy format where file was given without source
3196            if ($vv->{file}) {
3197                $vv->{source} = "file:".$vv->{file};
3198                $vv->{filename} = $vv->{file} unless($vv->{filename});
3199                delete $vv->{file};
3200            }
3201
3202            # merge items from field param
3203            $self->logger()->info(Dumper [ $vv, $param ]);
3204            map { $vv->{$_} ||= $param->{$_}  } ('mime','label','binary','hide','auto','filename');
3205
3206            # guess filename from a file source
3207            if (!$vv->{filename} && $vv->{source} && $vv->{source} =~ m{ file:.*?([^\/]+(\.\w+)?) \z }xms) {
3208                $vv->{filename} = $1;
3209            }
3210
3211            # set mime to default / from format
3212            $vv->{mime} ||= $mime;
3213
3214            # we have an external source so we need a link
3215            if ($vv->{source}) {
3216                 my $target = $self->__persist_response({
3217                    source => $vv->{source},
3218                    attachment =>  $vv->{filename},
3219                    mime => $vv->{mime}
3220                });
3221                $item->{value}  = {
3222                    label => 'I18N_OPENXPKI_UI_CLICK_TO_DOWNLOAD',
3223                    type => 'link',
3224                    filename => $vv->{filename},
3225                    data => $self->_client()->_config()->{'scripturl'} . "?page=".$target,
3226                };
3227            } else {
3228                my $type;
3229                # payload is binary, so encode it and set type to base64
3230                if ($vv->{binary}) {
3231                    $type = 'base64';
3232                    $vv->{data} = encode_base64($vv->{data}, '');
3233                } elsif ($vv->{base64}) {
3234                    $type = 'base64';
3235                }
3236                $item->{value}  = {
3237                    label=> $vv->{label},
3238                    mimetype => $vv->{mime},
3239                    filename => $vv->{filename},
3240                    type => $type,
3241                    data => $vv->{data},
3242                };
3243            }
3244
3245            if ($vv->{hide}) {
3246                $item->{value}->{autodownload} = 1;
3247                $item->{value}->{hide} = 1;
3248            } elsif ($vv->{auto}) {
3249                $item->{value}->{autodownload} = 1;
3250            }
3251
3252        # format for cert_info block
3253        } elsif ($item->{format} eq "cert_info") {
3254            $item->{format} = 'deflist';
3255
3256            my $raw = $item->{value};
3257
3258            # this requires that we find the profile and subject in the context
3259            my @val;
3260            my $cert_profile = $context->{cert_profile};
3261            my $cert_subject_style = $context->{cert_subject_style};
3262            if ($cert_profile && $cert_subject_style) {
3263
3264                if (!$raw || ref $raw ne 'HASH') {
3265                    $raw = {};
3266                }
3267
3268                my $fields = $self->send_command_v2( 'get_field_definition',
3269                    { profile => $cert_profile, style => $cert_subject_style, 'section' =>  'info' });
3270                $self->logger()->trace( 'Profile fields' . Dumper $fields ) if $self->logger->is_trace;
3271
3272                foreach my $field (@$fields) {
3273                    # FIXME this still uses "old" syntax - adjust after API refactoring
3274                    my $key = $field->{id}; # Name of the context key
3275                    if ($raw->{$key}) {
3276                        push @val, { label => $field->{label}, value => $raw->{$key}, key => $key };
3277                    }
3278                }
3279            } else {
3280                # if nothing is found, transform raw values to a deflist
3281                my $kv = $item->{value} || {};
3282                @val = map { { key => $_, label => $_, value => $kv->{$_}} } sort keys %{$kv};
3283
3284            }
3285
3286            $item->{value} = \@val;
3287
3288        } elsif ($item->{format} eq "ullist" || $item->{format} eq "rawlist") {
3289            # nothing to do here
3290
3291        } elsif ($item->{format} eq "itemcnt") {
3292
3293            my $list = $item->{value};
3294
3295            if (ref $list eq 'ARRAY') {
3296                $item->{value} = scalar @{$list};
3297            } elsif (ref $list eq 'HASH') {
3298                $item->{value} = scalar keys %{$list};
3299            } else {
3300                $item->{value} = '??';
3301            }
3302            $item->{format} = '';
3303
3304        } elsif ($item->{format} eq "deflist") {
3305
3306            # Sort by label
3307            my @val;
3308            if ($item->{value} && (ref $item->{value} eq 'HASH')) {
3309                @val = map { { label => $_, value => $item->{value}->{$_}} } sort keys %{$item->{value}};
3310                $item->{value} = \@val;
3311            }
3312
3313        } elsif ($item->{format} eq "grid") {
3314
3315            my @head;
3316            # item value can be data or grid specification
3317            if (ref $item->{value} eq 'HASH') {
3318                my $hv = $item->{value};
3319                $item->{header} = [ map { { 'sTitle' => $_ } } @{$hv->{header}} ];
3320                $item->{value} = $hv->{value};
3321            } elsif ($field->{header}) {
3322                $item->{header} = [ @head = map { { 'sTitle' => $_ } } @{$field->{header}} ];
3323            } else {
3324                $item->{header} = [ @head = map { { 'sTitle' => '' } } @{$item->{value}->[0]} ];
3325            }
3326            $item->{action} = $field->{action};
3327            $item->{target} = $field->{target} ?  $field->{target} : 'tab';
3328
3329        } elsif ($item->{format} eq "chart") {
3330
3331            my @head;
3332
3333            my $param = $field->{param} || {};
3334
3335            $item->{options} = {
3336                type => 'line',
3337            };
3338
3339            # read options from the fields param method
3340            foreach my $key ('width','height','type','title') {
3341                $item->{options}->{$key} = $param->{$key} if (defined $param->{$key});
3342            }
3343
3344            # series can be a hash based on the datas keys or an array
3345            my $series = $param->{series};
3346            if (ref $series eq 'ARRAY') {
3347                $item->{options}->{series} = $series;
3348            }
3349
3350            my $start_at = 0;
3351            my $interval = 'months';
3352
3353            # item value can be data (array) or chart specification (hash)
3354            if (ref $item->{value} eq 'HASH') {
3355                # single data row chart with keys as groups
3356                my $hv = $item->{value};
3357                my @series;
3358                my @keys;
3359                if (ref $series eq 'HASH') {
3360                    # series contains label as key / value hash
3361                    @keys = sort keys %{$series};
3362                    map {
3363                        # series value can be a scalar (label) or a full hash
3364                        my $ll = $series->{$_};
3365                        push @series, (ref $ll ? $ll : { label => $ll });
3366                        $_;
3367                    } @keys;
3368
3369                } elsif (ref $series eq 'ARRAY') {
3370                    @keys = map {
3371                        my $kk = $_->{key};
3372                        delete $_->{key};
3373                        $kk;
3374                    } @{$series};
3375
3376                } else {
3377
3378                    @keys = grep { ref $hv->{$_} ne 'HASH' } sort keys %{$hv};
3379                    if (my $prefix = $param->{label}) {
3380                        # label is a prefix to be merged with the key names
3381                        @series = map { { label => $prefix.'_'.uc($_) } } @keys;
3382                    } else {
3383                        @series = map {  { label => $_ } } @keys;
3384                    }
3385                }
3386
3387                # check if we have a single row or multiple, we also assume
3388                # that all keys have the same value count so we just take the
3389                # first one
3390                if (ref $hv->{$keys[0]}) {
3391                    # get the number of items per row
3392                    my $ic = scalar @{$hv->{$keys[0]}};
3393
3394                    # if start_at is not set, we do a backward calculation
3395                    $start_at ||= DateTime->now()->subtract ( $interval => ($ic-1) );
3396                    my $val = [];
3397                    for (my $drw = 0; $drw < $ic; $drw++) {
3398                        my @row = (undef) x @keys;
3399                        unshift @row, $start_at->epoch();
3400                        $start_at->add( $interval => 1 );
3401                        $val->[$drw] = \@row;
3402                        for (my $idx = 0; $idx < @keys; $idx++) {
3403                            $val->[$drw]->[$idx+1] = $hv->{$keys[$idx]}->[$drw];
3404                        }
3405                    }
3406                    $item->{value} = $val;
3407
3408                } elsif ($item->{options}->{type} eq 'pie') {
3409
3410                    my $sum = 0;
3411                    my @val = map { $sum+=$hv->{$_}; $hv->{$_} || 0 } @keys;
3412                    if ($sum) {
3413                        my $divider = 100 / $sum;
3414
3415                        @val = map {  $_ * $divider } @val;
3416
3417                        unshift @val, '';
3418                        $item->{value} = [ \@val ];
3419                    }
3420
3421                } else {
3422                    # only one row so this is easy
3423                    my @val = map { $hv->{$_} || 0 } @keys;
3424                    unshift @val, '';
3425                    $item->{value} = [ \@val ];
3426                }
3427                $item->{options}->{series} = \@series if (@series);
3428
3429            } elsif (ref $item->{value} eq 'ARRAY' && @{$item->{value}}) {
3430                if (!ref $item->{value}->[0]) {
3431                    $item->{value} = [ $item->{value} ];
3432                }
3433            }
3434
3435        } elsif ($field_type eq 'select' && !$field->{template} && $field->{option} && ref $field->{option} eq 'ARRAY') {
3436            foreach my $option (@{$field->{option}}) {
3437                next unless (defined $option->{value});
3438                if ($item->{value} eq $option->{value}) {
3439                    $item->{value} = $option->{label};
3440                    last;
3441                }
3442            }
3443        }
3444
3445        if ($field->{template}) {
3446
3447            $self->logger()->trace('Render output using template on field '.$key.', '. $field->{template} . ', value:  ' . Dumper $item->{value}) if $self->logger->is_trace;
3448
3449            # Rendering target depends on value format
3450            # deflist: iterate over each label/value pair and render the value template
3451            if ($item->{format} eq "deflist") {
3452                $item->{value} = [
3453                    map { {
3454                        # $_ is a HashRef: { label => STR, key => STR, value => STR } where key is the field name (not needed here)
3455                        label => $_->{label},
3456                        value => $self->send_command_v2('render_template', { template => $field->{template}, params => $_ }),
3457                        format => 'raw',
3458                    } }
3459                    @{ $item->{value} }
3460                ];
3461
3462            # bullet list, put the full list to tt and split at the | as sep (as used in profile)
3463            } elsif ($item->{format} eq "ullist" || $item->{format} eq "rawlist") {
3464                my $out = $self->send_command_v2('render_template', {
3465                    template => $field->{template},
3466                    params => { value => $item->{value} },
3467                });
3468                $self->logger()->debug('Rendered template: ' . $out);
3469                if ($out) {
3470                    my @val = split /\s*\|\s*/, $out;
3471                    $self->logger()->trace('Split ' . Dumper \@val) if $self->logger->is_trace;
3472                    $item->{value} = \@val;
3473                } else {
3474                    $item->{value} = undef; # prevent pushing emtpy lists
3475                }
3476
3477            } elsif (ref $item->{value} eq 'HASH' && $item->{value}->{label}) {
3478                $item->{value}->{label} = $self->send_command_v2('render_template', {
3479                    template => $field->{template},
3480                    params => { value => $item->{value}->{label} },
3481                });
3482
3483            } else {
3484                $item->{value} = $self->send_command_v2('render_template', {
3485                    template => $field->{template},
3486                    params => { value => $item->{value} },
3487                });
3488            }
3489
3490        } elsif ($field->{yaml_template}) {
3491            ##! 64: 'Rendering value: ' . $item->{value}
3492            $self->logger->debug('Template value: ' . Dumper $item );
3493            my $structure = $self->send_command_v2('render_yaml_template', {
3494                template => $field->{yaml_template},
3495                params => { value => $item->{value} },
3496            });
3497            $self->logger->debug('Rendered YAML template: ' . Dumper $structure);
3498            ##! 64: 'Rendered YAML template: ' . $out
3499            if (defined $structure) {
3500                $item->{value} = $structure;
3501            } else {
3502                $item->{value} = undef; # prevent pushing emtpy lists
3503            }
3504        }
3505
3506        # do not push items that are empty
3507        if (!(defined $item->{value} &&
3508            ((ref $item->{value} eq 'HASH' && %{$item->{value}}) ||
3509            (ref $item->{value} eq 'ARRAY' && @{$item->{value}}) ||
3510            (ref $item->{value} eq '' && $item->{value} ne '')))) {
3511            #noop
3512        } elsif ($item->{format} eq 'head' && $item->{empty}) {
3513            # queue header element - we only add it (below) if a non-empty item follows
3514            $queued = $item;
3515        } else {
3516            # add queued element if any
3517            if ($queued) {
3518                push @fields, $queued;
3519                $queued = undef;
3520            }
3521            # push current field
3522            push @fields, $item;
3523        }
3524    }
3525
3526    return \@fields;
3527
3528}
3529
3530=head2 __render_workflow_info
3531
3532Render the technical info of a workflow (state, proc_state, etc). Expects a
3533wf_info structure and optional a wfdetail_config, will fallback to the
3534default display if this is not given.
3535
3536=cut
3537
3538sub __render_workflow_info {
3539
3540    my $self = shift;
3541    my $wf_info = shift;
3542    my $wfdetails_config = shift || [];
3543
3544    $wfdetails_config = $self->__default_wfdetails
3545        unless (@$wfdetails_config);
3546
3547    my $wfdetails_info;
3548    # if needed, fetch enhanced info incl. workflow attributes
3549    if (
3550        # if given info hash doesn't contain attribute data...
3551        not($wf_info->{workflow}->{attribute}) and (
3552            # ...but default wfdetails reference attribute.*
3553               grep { ($_->{field}//'') =~              / attribute\. /msx } @$wfdetails_config
3554            or grep { ($_->{template}//'') =~           / attribute\. /msx } @$wfdetails_config
3555            or grep { (($_->{link}//{})->{page}//'') =~ / attribute\. /msx } @$wfdetails_config
3556            or grep { ($_->{field}//'') =~              / \Acreator /msx } @$wfdetails_config
3557        )
3558    ) {
3559        $wfdetails_info = $self->send_command_v2( 'get_workflow_info',  {
3560            id => $wf_info->{workflow}->{id},
3561            with_attributes => 1,
3562        })->{workflow};
3563    }
3564    else {
3565        $wfdetails_info = $wf_info->{workflow};
3566    }
3567
3568    # assemble infos
3569    my @data;
3570    for my $cfg (@$wfdetails_config) {
3571        my $value;
3572
3573        my $field = $cfg->{field} // '';
3574        if ($field eq 'creator') {
3575            # we enforce tooltip, if you need something else use a template on attribute.creator
3576            if ($wfdetails_info->{attribute}->{creator} =~ m{certid:([\w-]+)}) {
3577                $cfg->{format} = 'link';
3578                # for a link the tooltip is on the top level and the value is a
3579                # scalar so we need to remap this
3580                $value = $self->__render_creator_tooltip($wfdetails_info->{attribute}->{creator}, $cfg);
3581                $value->{label} = $value->{value};
3582                $value->{page} = 'certificate!detail!identifier!'.$1;
3583            } else {
3584                $cfg->{format} = 'tooltip';
3585                $value = $self->__render_creator_tooltip($wfdetails_info->{attribute}->{creator}, $cfg);
3586            }
3587        } elsif ($cfg->{template}) {
3588            $value = $self->send_command_v2( render_template => {
3589                template => $cfg->{template},
3590                params => $wfdetails_info,
3591            });
3592        } elsif ($field =~ m{\A attribute\.(\S+) }xi) {
3593            $value = $wfdetails_info->{attribute}->{$1} // '-';
3594        } elsif ($field =~ m{\A context\.(\S+) }xi) {
3595            $value = $wfdetails_info->{context}->{$1} // '-';
3596        } elsif ($field eq 'proc_state') {
3597            $value = $self->__get_proc_state_label($wfdetails_info->{$field});
3598        } elsif ($field) {
3599            $value = $wfdetails_info->{$field} // '-';
3600        }
3601
3602        # if it's a link: render URL template ("page")
3603        if ($cfg->{link}) {
3604            $value = {
3605                label => $value,
3606                page => $self->send_command_v2( render_template => {
3607                    template => $cfg->{link}->{page},
3608                    params => $wfdetails_info,
3609                }),
3610                target => $cfg->{link}->{target} || 'popup',
3611            }
3612        }
3613
3614        push @data, {
3615            label => $cfg->{label} // '',
3616            value => $value,
3617            format => $cfg->{link} ? 'link' : ($cfg->{format} || 'text'),
3618            $cfg->{tooltip} ? ( tooltip => $cfg->{tooltip} ) : (),
3619        };
3620    }
3621
3622    return \@data;
3623
3624}
3625
3626=head2 __render_creator_tooltip
3627
3628Expects the userid of a creator and the field definition.
3629
3630If the field has I<yaml_template> set, the template is parsed with the
3631value of the creator given as key I<creator> in the parameter hash. If
3632the resulting data structure is a hash and has a non-empty key I<value>,
3633it is used as value for the field.
3634
3635If the field has I<template> set, the result of this template is used as
3636tooltip for the field, the literal value given as I<creator> is used as
3637the visible value. If the namespace of the item is I<certid>, the value
3638will be created as link pointing to the certificate details popup.
3639
3640If neither one is set, the C<creator()> method from the C<Metadata>
3641Plugin is used as default renderer.
3642
3643In case the template does not provide a usable value, the tooltip will
3644show an error message, depending on weather the creator string has a
3645namespace tag or not.
3646
3647=cut
3648
3649sub __render_creator_tooltip {
3650
3651    my $self = shift;
3652    my $creator = shift;
3653    my $field = shift;
3654
3655    my $cacheid;
3656    my $value = { value => $creator };
3657    if (!$field->{nocache}) {
3658        # Enable caching of the creator information
3659        # The key is made from the creator name and the template string
3660        # and bound to the user session to avoid information leakage in
3661        # case the template binds to the users role/permissions
3662        $cacheid = Digest::SHA->new()
3663            ->add($self->_session->id())
3664            ->add($field->{yaml_template} // $field->{template} // '')
3665            ->add($creator//'')->hexdigest;
3666
3667        $self->logger()->trace('creator tooltip cache id ' .  $cacheid);
3668        my $value = $template_cache->get($cacheid);
3669        return $value if($value);
3670
3671    }
3672
3673    # the field comes with a YAML template = render the field definiton from it
3674    if ($field->{yaml_template}) {
3675        $self->logger()->debug('render creator tooltip from yaml template');
3676        my $val = $self->send_command_v2( render_yaml_template => {
3677            template => $field->{yaml_template},
3678            params => { creator => $creator },
3679        });
3680        $value = $val if (ref $val eq 'HASH' && $val->{value});
3681
3682    # use template (or default template) to set username
3683    } else {
3684        $self->logger()->debug('render creator name from template');
3685        my $username = $self->send_command_v2( render_template => {
3686            template => $field->{template} || '[% USE Metadata; Metadata.creator(creator) %]',
3687            params => { creator => $creator },
3688        });
3689        if ($username) {
3690            $value->{tooltip} = (($username ne $creator) ? $username : '');
3691        }
3692    }
3693
3694    # creator has no namespace so there was nothing to resolve
3695    if (!defined $value->{tooltip} && $creator !~ m{\A\w+:}) {
3696        $value->{tooltip} = 'I18N_OPENXPKI_UI_WORKFLOW_CREATOR_NO_NAMESPACE';
3697    }
3698
3699    # still no result
3700    $value->{tooltip} //= 'I18N_OPENXPKI_UI_WORKFLOW_CREATOR_UNABLE_TO_RESOLVE';
3701
3702    $self->logger()->trace(Dumper { cacheid => $cacheid, value => $value} );
3703
3704    $template_cache->set($cacheid => $value) if($cacheid);
3705    return $value;
3706
3707}
3708
3709=head2 __render_task_list
3710
3711Expects a hash that defines a workflow query and output rules for a
3712tasklist as defined in the uicontrol section.
3713
3714=cut
3715
3716sub __render_task_list {
3717
3718    my $self = shift;
3719    my $item = shift;
3720
3721    my $query = $item->{query};
3722    my $limit = 25;
3723
3724    $query = { $self->__tenant(), %$query } unless($query->{tenant});
3725
3726    if ($query->{limit}) {
3727        $limit = $query->{limit};
3728        delete $query->{limit};
3729    }
3730
3731    if (!$query->{order}) {
3732        $query->{order} = 'workflow_id';
3733        if (!defined $query->{reverse}) {
3734            $query->{reverse} = 1;
3735        }
3736    }
3737
3738    my $pager_args = { limit => $limit };
3739    if ($item->{pager}) {
3740        $pager_args = $item->{pager};
3741    }
3742
3743    my @cols;
3744    if ($item->{cols}) {
3745        @cols = @{$item->{cols}};
3746    } else {
3747        @cols = (
3748            { label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_SERIAL_LABEL', field => 'workflow_id', sortkey => 'workflow_id' },
3749            { label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_UPDATED_LABEL', field => 'workflow_last_update', sortkey => 'workflow_last_update' },
3750            { label => 'I18N_OPENXPKI_UI_WORKFLOW_TYPE_LABEL', field => 'workflow_label' },
3751            { label => 'I18N_OPENXPKI_UI_WORKFLOW_STATE_LABEL', field => 'workflow_state' },
3752        );
3753    }
3754
3755    my $actions = $item->{actions} // [{ path => 'redirect!workflow!load!wf_id!{serial}', icon => 'view' }];
3756
3757    # create the header from the columns spec
3758    my ($header, $column, $rattrib) = $self->__render_list_spec( \@cols );
3759
3760    if ($rattrib) {
3761        $query->{return_attributes} = $rattrib;
3762    }
3763
3764    $self->logger()->trace( "columns : " . Dumper $column) if $self->logger->is_trace;
3765
3766    my $search_result = $self->send_command_v2( 'search_workflow_instances', { limit => $limit, %$query } );
3767
3768    # empty message
3769    my $empty = $item->{ifempty} || 'I18N_OPENXPKI_UI_TASK_LIST_EMPTY_LABEL';
3770
3771    my $pager;
3772    my @data;
3773    # No results
3774    if (!@$search_result) {
3775
3776        return if ($empty eq 'hide');
3777
3778    } else {
3779
3780        @data = $self->__render_result_list( $search_result, $column );
3781
3782        $self->logger()->trace( "dumper result: " . Dumper @data) if $self->logger->is_trace;
3783
3784        if ($limit == scalar @$search_result) {
3785            my %count_query = %{$query};
3786            delete $count_query{order};
3787            delete $count_query{reverse};
3788            my $result_count= $self->send_command_v2( 'search_workflow_instances_count', \%count_query  );
3789            my $queryid = $self->__generate_uid();
3790            my $_query = {
3791                'id' => $queryid,
3792                'type' => 'workflow',
3793                'count' => $result_count,
3794                'query' => $query,
3795                'column' => $column,
3796                'pager' => $pager_args,
3797            };
3798            $self->_client->session()->param('query_wfl_'.$queryid, $_query );
3799            $pager = $self->__render_pager( $_query, $pager_args );
3800        }
3801
3802    }
3803
3804    $self->add_section({
3805        type => 'grid',
3806        className => 'workflow',
3807        content => {
3808            label => $item->{label},
3809            description => $item->{description},
3810            actions => $actions,
3811            columns => $header,
3812            data => \@data,
3813            pager => $pager,
3814            empty => $empty,
3815
3816        }
3817    });
3818
3819    return \@data
3820}
3821
3822=head2 __check_for_validation_error
3823
3824Uses last_reply to check if there was a validation error. If a validation
3825error occured, the field_errors hash is returned and the status variable is
3826set to render the errors in the form view. Returns undef otherwise.
3827
3828=cut
3829
3830sub __check_for_validation_error {
3831
3832    my $self = shift;
3833    my $reply = $self->_last_reply();
3834    if ($reply->{'ERROR'}->{CLASS} eq 'OpenXPKI::Exception::InputValidator' &&
3835        $reply->{'ERROR'}->{ERRORS}) {
3836        my $validator_msg = $reply->{'ERROR'}->{LABEL};
3837        my $field_errors = $reply->{'ERROR'}->{ERRORS};
3838        if (ref $field_errors eq 'ARRAY') {
3839            $self->logger()->info('Input validation error on fields '.
3840                join(",", map { ref $_ ? $_->{name} : $_ } @{$field_errors}));
3841        } else {
3842            $self->logger()->info('Input validation error');
3843        }
3844        $self->_status({ level => 'error', message => $validator_msg, field_errors => $field_errors });
3845        $self->logger()->trace('validation details' . Dumper $field_errors ) if $self->logger->is_trace;
3846        return $field_errors;
3847    }
3848    return;
3849}
3850
3851sub __get_proc_state_label {
3852    my ($self, $proc_state) = @_;
3853    return $proc_state ? $self->__proc_state_i18n->{$proc_state}->{label} : '-';
3854}
3855
3856sub __get_proc_state_desc {
3857    my ($self, $proc_state) = @_;
3858    return $proc_state ? $self->__proc_state_i18n->{$proc_state}->{desc} : '-';
3859}
3860
3861=head1 example workflow config
3862
3863=head2 State with default rendering
3864
3865    <state name="DATA_LOADED">
3866        <description>I18N_OPENXPKI_WF_STATE_CHANGE_METADATA_LOADED</description>
3867        <action name="changemeta_update" resulting_state="DATA_UPDATE"/>
3868        <action name="changemeta_persist" resulting_state="SUCCESS"/>
3869    </state>
3870    ...
3871    <action name="changemeta_update"
3872        class="OpenXPKI::Server::Workflow::Activity::Noop"
3873        description="I18N_OPENXPKI_ACTION_UPDATE_METADATA">
3874        <field name="metadata_update" />
3875    </action>
3876    <action name="changemeta_persist"
3877        class="OpenXPKI::Server::Workflow::Activity::PersistData">
3878    </action>
3879
3880When reached first, a page with the text from the description tag and two
3881buttons will appear. The update button has I18N_OPENXPKI_ACTION_UPDATE_METADATA
3882as label an after pushing it, a form with one text field will be rendered.
3883The persist button has no description and will have the action name
3884changemeta_persist as label. As it has no input fields, the workflow will go
3885to the next state without further ui interaction.
3886
3887=head2 State with custom rendering
3888
3889    <state name="DATA_LOADED" uihandle="OpenXPKI::Client::UI::Workflow::Metadata::render_current_data">
3890    ....
3891    </state>
3892
3893Regardless of what the rest of the state looks like, as soon as the state is
3894reached, the render_current_data method is called.
3895
3896=head2 Action with custom rendering
3897
3898    <state name="DATA_LOADED">
3899        <description>I18N_OPENXPKI_WF_STATE_CHANGE_METADATA_LOADED</description>
3900        <action name="changemeta_update" resulting_state="DATA_UPDATE"/>
3901        <action name="changemeta_persist" resulting_state="SUCCESS"/>
3902    </state>
3903
3904    <action name="changemeta_update"
3905        class="OpenXPKI::Server::Workflow::Activity::Noop"
3906        uihandle="OpenXPKI::Client::UI::Workflow::Metadata::render_update_form"
3907        description="I18N_OPENXPKI_ACTION_UPDATE_METADATA_ACTION">
3908        <field name="metadata_update"/>
3909    </action>
3910
3911While no action is selected, this will behave as the default rendering and show
3912two buttons. After the changemeta_update button was clicked, it calls the
3913render_update_form method. Note: The uihandle does not affect the target of
3914the form submission so you either need to properly setup the environment to
3915use the default action (see action_index) or set the wf_handler to a custom
3916method for parsing the form data.
3917
3918=cut
3919
3920__PACKAGE__->meta->make_immutable;
3921