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