1package OpenXPKI::Client::UI::Certificate; 2use Moose; 3 4# Core modules 5use Data::Dumper; 6use Math::BigInt; 7 8# CPAN modules 9use URI::Escape; 10use DateTime; 11use Digest::SHA qw(sha1_base64); 12 13# Project modules 14use OpenXPKI::DN; 15use OpenXPKI::i18n qw( i18nGettext ); 16use OpenXPKI::Serialization::Simple; 17 18 19extends 'OpenXPKI::Client::UI::Result'; 20 21 22has __default_grid_head => ( 23 is => 'rw', 24 isa => 'ArrayRef', 25 lazy => 1, 26 27 default => sub { return [ 28 { sTitle => "I18N_OPENXPKI_UI_CERTIFICATE_SERIAL", sortkey => 'cert_key' }, 29 { sTitle => "I18N_OPENXPKI_UI_CERTIFICATE_SUBJECT", sortkey => 'subject' }, 30 { sTitle => "I18N_OPENXPKI_UI_CERTIFICATE_STATUS", format => 'certstatus', sortkey => 'status' }, 31 { sTitle => "I18N_OPENXPKI_UI_CERTIFICATE_NOTBEFORE", format => 'timestamp', sortkey => 'notbefore' }, 32 { sTitle => "I18N_OPENXPKI_UI_CERTIFICATE_NOTAFTER", format => 'timestamp', sortkey => 'notafter' }, 33 { sTitle => "I18N_OPENXPKI_UI_CERTIFICATE_ISSUER", sortkey => 'issuer_dn'}, 34 { sTitle => "I18N_OPENXPKI_UI_CERTIFICATE_IDENTIFIER", sortkey => 'identifier'}, 35 { sTitle => 'identifier', bVisible => 0 }, 36 { sTitle => "_className"}, 37 ]; } 38); 39 40has __default_grid_row => ( 41 is => 'rw', 42 isa => 'ArrayRef', 43 lazy => 1, 44 default => sub { return [ 45 { source => 'certificate', field => 'cert_key_hex' }, 46 { source => 'certificate', field => 'subject' }, 47 { source => 'certificate', field => 'status' }, 48 { source => 'certificate', field => 'notbefore' }, 49 { source => 'certificate', field => 'notafter' }, 50 { source => 'certificate', field => 'issuer_dn' }, 51 { source => 'certificate', field => 'identifier' }, 52 { source => 'certificate', field => 'identifier' }, 53 { source => 'certificate', field => 'status' }, 54 ]; } 55); 56 57has __validity_options => ( 58 is => 'rw', 59 isa => 'ArrayRef', 60 lazy => 1, 61 default => sub { return [ 62 { value => 'valid_at', label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY_VALID_AT' }, 63 { value => 'valid_before', label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY_NOTBEFORE_LT' }, 64 { value => 'valid_after', label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY_NOTBEFORE_GT' }, 65 { value => 'expires_before', label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY_NOTAFTER_LT' }, 66 { value => 'expires_after', label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY_NOTAFTER_GT' }, 67 { value => 'revoked_before', label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY_REVOKED_LT' }, 68 { value => 'revoked_after', label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY_REVOKED_GT' }, 69 { value => 'invalid_before', label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY_INVALID_LT' }, 70 { value => 'invalid_after', label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY_INVALID_GT' }, 71 ]; } 72); 73 74 75sub BUILD { 76 my $self = shift; 77} 78 79=head2 init_search 80 81Render the search form 82#TODO - preset parameters 83 84=cut 85sub init_search { 86 87 my $self = shift; 88 my $args = shift; 89 90 $self->_page({ 91 label => 'I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_LABEL', 92 description => '', 93 }); 94 95 my $profile = $self->send_command_v2( 'list_used_profiles' ); 96 97 # TODO Sorting / I18 98 99 my @profile_list = sort { $a->{label} cmp $b->{label} } @{$profile}; 100 101 my $issuer = $self->send_command_v2( 'list_used_issuers', { format => 'label' } ); 102 my @issuer_list = sort { $a->{label} cmp $b->{label} } @{$issuer}; 103 104 my @states = ( 105 { label => 'I18N_OPENXPKI_UI_CERT_STATUS_ISSUED', value => 'ISSUED'}, 106 { label => 'I18N_OPENXPKI_UI_CERT_STATUS_VALID', value => 'VALID'}, 107 { label => 'I18N_OPENXPKI_UI_CERT_STATUS_EXPIRED', value => 'EXPIRED'}, 108 { label => 'I18N_OPENXPKI_UI_CERT_STATUS_REVOKED', value => 'REVOKED'}, 109 { label => 'I18N_OPENXPKI_UI_CERT_STATUS_CRL_ISSUANCE_PENDING', value => 'CRL_ISSUANCE_PENDING'}, 110 ); 111 112 my $preset; 113 if ($args->{preset}) { 114 $preset = $args->{preset}; 115 } elsif (my $queryid = $self->param('query')) { 116 my $result = $self->_client->session()->param('query_cert_'.$queryid); 117 $preset = $result->{input}; 118 } else { 119 foreach my $key (('subject','san')) { 120 if (my $val = $self->param($key)) { 121 $preset->{$key} = $val; 122 } 123 } 124 } 125 126 my @fields = ( 127 { name => 'subject', label => 'I18N_OPENXPKI_UI_CERTIFICATE_SUBJECT', type => 'text', is_optional => 1, value => $preset->{subject} }, 128 { name => 'san', label => 'I18N_OPENXPKI_UI_CERTIFICATE_SAN', type => 'text', is_optional => 1, value => $preset->{san} }, 129 { name => 'status', label => 'I18N_OPENXPKI_UI_CERTIFICATE_STATUS', type => 'select', is_optional => 1, prompt => 'all', options => \@states, , value => $preset->{status} }, 130 { name => 'profile', label => 'I18N_OPENXPKI_UI_CERTIFICATE_PROFILE', type => 'select', is_optional => 1, prompt => 'all', options => \@profile_list, value => $preset->{profile} }, 131 { name => 'issuer_identifier', label => 'I18N_OPENXPKI_UI_CERTIFICATE_ISSUER', type => 'select', is_optional => 1, prompt => 'all', options => \@issuer_list, value => $preset->{issuer_identifier} }, 132 { name => 'validity', label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY', 'keys' => $self->__validity_options(), type => 'datetime', is_optional => 1, clonable => 1, value => $preset->{validity_options} || [ { key => 'valid_at', value => '' }], }, 133 ); 134 135 my $attributes = $self->_client->session()->param('certsearch')->{default}->{attributes}; 136 my @meta_description; 137 if (defined $attributes && (ref $attributes eq 'ARRAY')) { 138 my @attrib; 139 foreach my $item (@{$attributes}) { 140 push @attrib, { value => $item->{key}, label=> $item->{label} }; 141 if ($item->{description}) { 142 push @meta_description, { label=> $item->{label}, value => $item->{description}, format => 'raw' }; 143 } 144 } 145 push @fields, { 146 name => 'attributes', 147 label => 'I18N_OPENXPKI_UI_CERTIFICATE_METADATA', 148 'keys' => \@attrib, 149 type => 'text', 150 is_optional => 1, 151 'clonable' => 1, 152 'value' => $preset->{attributes} || [{ 'key' => $attrib[0]->{value}, value => ''}], 153 } if (@attrib); 154 } 155 156 $self->add_section({ 157 type => 'form', 158 action => 'certificate!search', 159 content => { 160 description => 'I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_DESC', 161 title => '', 162 submit_label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_SUBMIT_LABEL', 163 fields => \@fields 164 }}, 165 ); 166 167 $self->add_section({ 168 type => 'form', 169 action => 'certificate!find', 170 content => { 171 title => '', 172 description => 'I18N_OPENXPKI_UI_CERTIFICATE_BY_IDENTIFIER_OR_SERIAL', 173 submit_label => 'I18N_OPENXPKI_UI_WORKFLOW_SEARCH_SUBMIT_LABEL', 174 fields => [ 175 { name => 'cert_identifier', label => 'I18N_OPENXPKI_UI_CERTIFICATE_IDENTIFIER', type => 'text', is_optional => 1, value => $preset->{cert_identifier} }, 176 { name => 'cert_serial', label => 'I18N_OPENXPKI_UI_CERTIFICATE_SERIAL', type => 'text', is_optional => 1, value => $preset->{cert_serial} }, 177 ] 178 }}, 179 ); 180 181 $self->add_section({ 182 type => 'keyvalue', 183 content => { 184 label => 'I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_FIELD_HINT_LIST', 185 description => '', 186 data => [ 187 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_SUBJECT', value => 'I18N_OPENXPKI_UI_CERTIFICATE_SUBJECT_HINT', format => 'raw' }, 188 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_SAN', value => 'I18N_OPENXPKI_UI_CERTIFICATE_SAN_HINT', format => 'raw' }, 189 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_STATUS', value => 'I18N_OPENXPKI_UI_CERTIFICATE_STATUS_HINT', format => 'raw' }, 190 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_PROFILE', value => 'I18N_OPENXPKI_UI_CERTIFICATE_PROFILE_HINT', format => 'raw' }, 191 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_ISSUER', value => 'I18N_OPENXPKI_UI_CERTIFICATE_ISSUER_HINT', format => 'raw' }, 192 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY', value => 'I18N_OPENXPKI_UI_CERTIFICATE_VALIDITY_HINT', format => 'raw' }, 193 @meta_description 194 ] 195 } 196 }); 197 198 return $self; 199} 200 201=head2 init_result 202 203Load the result of a query, based on a query id and paging information 204 205=cut 206sub init_result { 207 208 my $self = shift; 209 my $args = shift; 210 211 my $queryid = $self->param('id'); 212 my $limit = $self->param('limit') || 25; 213 214 my $startat = $self->param('startat') || 0; 215 216 # Safety rule 217 if ($limit > 500) { $limit = 500; } 218 219 # Load query from session 220 my $result = $self->_client->session()->param('query_cert_'.$queryid); 221 222 # result expired or broken id 223 if (!$result || !$result->{count}) { 224 $self->set_status('I18N_OPENXPKI_UI_SEARCH_RESULT_EXPIRED_OR_EMPTY','error'); 225 return $self->init_search(); 226 } 227 228 # Add limits 229 my $query = $result->{query}; 230 $query->{limit} = $limit; 231 $query->{start} = $startat; 232 233 if (!$query->{order}) { 234 $query->{order} = 'notbefore'; 235 if (!defined $query->{reverse}) { 236 $query->{reverse} = 1; 237 } 238 } 239 240 $self->logger()->debug( "persisted query: " . Dumper $result) if $self->logger->is_debug; 241 242 my $search_result = $self->send_command_v2( 'search_cert', $query ); 243 244 $self->logger()->debug( "search result: " . Dumper $search_result) if $self->logger->is_debug; 245 246 my $criteria = '<br>' . (join ", ", @{$result->{criteria}}); 247 248 $self->_page({ 249 label => 'I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_RESULT_LABEL', 250 description => 'I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_RESULT_DESC' . $criteria , 251 breadcrumb => [ 252 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_LABEL', className => 'cert-search' }, 253 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_RESULT_TITLE', className => 'cert-search-result' } 254 ], 255 }); 256 257 my $pager = $self->__render_pager( $result, { limit => $limit, startat => $startat } ); 258 259 my $body = $result->{column}; 260 $body = $self->__default_grid_row() if(!$body); 261 262 my $header = $result->{header}; 263 $header = $self->__default_grid_head() if(!$header); 264 265 my @result = $self->__render_result_list( $search_result, $body ); 266 267 $self->logger()->trace( "dumper result: " . Dumper @result) if $self->logger->is_trace; 268 269 $self->add_section({ 270 type => 'grid', 271 className => 'certificate', 272 content => { 273 actions => [{ 274 path => 'certificate!detail!identifier!{identifier}', 275 label => 'I18N_OPENXPKI_UI_DOWNLOAD_LABEL', 276 icon => 'download', 277 target => 'popup' 278 }], 279 columns => $header, 280 data => \@result, 281 empty => 'I18N_OPENXPKI_UI_TASK_LIST_EMPTY_LABEL', 282 pager => $pager, 283 buttons => [ 284 { label => 'I18N_OPENXPKI_UI_SEARCH_RELOAD_FORM', 285 page => 'certificate!search!query!' .$queryid, 286 format => 'expected' 287 }, 288 { label => 'I18N_OPENXPKI_UI_SEARCH_REFRESH', 289 page => 'redirect!certificate!result!id!' .$queryid, 290 format => 'alternative' 291 }, 292 { label => 'I18N_OPENXPKI_UI_SEARCH_NEW_SEARCH', 293 page => 'certificate!search', 294 format => 'failure' 295 }, 296 { label => 'I18N_OPENXPKI_UI_SEARCH_EXPORT_RESULT', 297 href => $self->_client()->_config()->{'scripturl'} . '?page=certificate!export!id!'.$queryid, 298 target => '_blank', 299 format => 'optional' 300 }, 301 ] 302 } 303 }); 304 305 return $self; 306 307} 308 309 310=head2 init_export 311 312Like init_result but send the data as CSV download, default limit is 500! 313 314=cut 315sub init_export { 316 317 my $self = shift; 318 my $args = shift; 319 320 my $queryid = $self->param('id'); 321 322 my $limit = $self->param('limit') || 500; 323 my $startat = $self->param('startat') || 0; 324 325 # Safety rule 326 if ($limit > 500) { $limit = 500; } 327 328 329 # Load query from session 330 my $result = $self->_client->session()->param('query_cert_'.$queryid); 331 332 # result expired or broken id 333 if (!$result || !$result->{count}) { 334 $self->set_status('I18N_OPENXPKI_UI_SEARCH_RESULT_EXPIRED_OR_EMPTY','error'); 335 return $self->init_search(); 336 } 337 338 # Add limits 339 my $query = $result->{query}; 340 $query->{limit} = $limit; 341 $query->{start} = $startat; 342 343 if (!$query->{order}) { 344 $query->{order} = 'certificate.notbefore'; 345 if (!defined $query->{reverse}) { 346 $query->{reverse} = 1; 347 } 348 } 349 350 $self->logger()->trace( "persisted query: " . Dumper $result) if $self->logger->is_trace; 351 352 my $search_result = $self->send_command_v2( 'search_cert', $query ); 353 354 $self->logger()->trace( "search result: " . Dumper $search_result) if $self->logger->is_trace; 355 356 my $header = $result->{header}; 357 $header = $self->__default_grid_head() if(!$header); 358 359 my @head; 360 my @cols; 361 362 my $ii = 0; 363 foreach my $col (@{$header}) { 364 # skip hidden fields 365 if ((!defined $col->{bVisible} || $col->{bVisible}) && $col->{sTitle} !~ /\A_/) { 366 push @head, i18nGettext($col->{sTitle}); 367 push @cols, $ii; 368 } 369 $ii++; 370 } 371 372 my $buffer = join("\t", @head)."\n"; 373 374 my $body = $result->{column}; 375 $body = $self->__default_grid_row() if(!$body); 376 377 foreach my $item (@{$search_result}) { 378 379 $item->{status} = 'EXPIRED' if ($item->{status} eq 'ISSUED' && $item->{notafter} < time()); 380 381 my @line; 382 foreach my $cc (@cols) { 383 384 my $col = $body->[$cc]; 385 my $field = lc($col->{field}); # lowercase to ease migration from 1.0 syntax 386 if ($field eq 'status') { 387 push @line, i18nGettext('I18N_OPENXPKI_UI_CERT_STATUS_'.$item->{status}); 388 389 } elsif ($field =~ /(notafter|notbefore)/) { 390 push @line, DateTime->from_epoch( epoch => $item->{ $field } )->iso8601(); 391 392 } elsif ($field eq 'cert_key_hex') { 393 push @line, unpack('H*', Math::BigInt->new( $item->{cert_key})->to_bytes ); 394 395 396 } else { 397 push @line, $item->{ $field }; 398 } 399 } 400 $buffer .= join("\t", @line)."\n"; 401 } 402 403 if (scalar @{$search_result} == $limit) { 404 $buffer .= i18nGettext("I18N_OPENXPKI_UI_CERT_EXPORT_EXCEEDS_LIMIT")."\n"; 405 } 406 407 print $self->cgi()->header( 408 -type => 'text/tab-separated-values', 409 -expires => "1m", 410 -attachment => "certificate export " . DateTime->now()->iso8601() . ".txt" 411 ); 412 print $buffer; 413 exit; 414 415} 416 417=head2 init_pager 418 419Similar to init_result but returns only the data portion of the table as 420partial result. 421 422=cut 423 424sub init_pager { 425 426 my $self = shift; 427 my $args = shift; 428 429 my $queryid = $self->param('id'); 430 431 # Load query from session 432 my $result = $self->_client->session()->param('query_cert_'.$queryid); 433 434 # result expired or broken id 435 if (!$result || !$result->{count}) { 436 $self->set_status('I18N_OPENXPKI_UI_SEARCH_RESULT_EXPIRED_OR_EMPTY','error'); 437 return $self->init_search(); 438 } 439 440 # will be removed once inline paging works 441 my $startat = $self->param('startat'); 442 443 my $limit = $self->param('limit') || 25; 444 if ($limit > 500) { $limit = 500; } 445 446 $startat = int($startat / $limit) * $limit; 447 448 # Add limits 449 my $query = $result->{query}; 450 $query->{limit} = $limit; 451 $query->{start} = $startat; 452 453 if ($self->param('order')) { 454 $query->{order} = $self->param('order'); 455 } 456 457 if (defined $self->param('reverse')) { 458 $query->{reverse} = $self->param('reverse'); 459 } 460 461 $self->logger()->trace( "persisted query: " . Dumper $result) if $self->logger->is_trace; 462 $self->logger()->trace( "executed query: " . Dumper $query) if $self->logger->is_trace; 463 464 my $search_result = $self->send_command_v2( 'search_cert', $query ); 465 466 $self->logger()->trace( "search result: " . Dumper $search_result) if $self->logger->is_trace; 467 468 my $body = $result->{column}; 469 $body = $self->__default_grid_row() if(!$body); 470 471 my @result = $self->__render_result_list( $search_result, $body ); 472 473 $self->logger()->trace( "dumper result: " . Dumper @result) if $self->logger->is_trace; 474 475 $self->_result()->{_raw} = { 476 _returnType => 'partial', 477 data => \@result, 478 }; 479 480 return $self; 481} 482=head2 init_mine 483 484my certificates view, finds certificates based on the current logged in userid 485 486=cut 487sub init_mine { 488 489 my $self = shift; 490 my $args = shift; 491 492 my $limit = $self->param('limit') || 25; 493 494 # Safety rule 495 if ($limit > 500) { $limit = 500; } 496 497 # will be removed once inline paging works 498 my $startat = $self->param('startat') || 0; 499 500 my $query = { 501 cert_attributes => { 502 'system_cert_owner' => { '=', $self->_session->param('user')->{name} } 503 }, 504 order => 'notbefore', 505 reverse => 1, 506 $self->__tenant(), 507 }; 508 509 $self->logger()->trace( "search query: " . Dumper $query) if $self->logger->is_trace; 510 511 my $search_result = $self->send_command_v2( 'search_cert', { %$query, limit => $limit, start => $startat } ); 512 513 my $result_count = scalar @{$search_result}; 514 my $pager; 515 if ($result_count == $limit) { 516 my %count_query = %{$query}; 517 delete $count_query{order}; 518 delete $count_query{reverse}; 519 520 $result_count = $self->send_command_v2( 'search_cert_count', \%count_query ); 521 522 my $queryid = $self->__generate_uid(); 523 my $_query = { 524 'id' => $queryid, 525 'type' => 'certificate', 526 'count' => $result_count, 527 'query' => $query, 528 }; 529 $self->_client->session()->param('query_cert_'.$queryid, $_query ); 530 $pager = $self->__render_pager( $_query, { limit => $limit, startat => $startat } ) 531 532 } 533 534 $self->logger()->trace( "search result: " . Dumper $search_result) if $self->logger->is_trace; 535 536 $self->_page({ 537 label => 'I18N_OPENXPKI_UI_CERTIFICATE_MINE_LABEL', 538 description => 'I18N_OPENXPKI_UI_CERTIFICATE_MINE_DESC', 539 }); 540 541 my @result = $self->__render_result_list( $search_result, $self->__default_grid_row() ); 542 543 $self->logger()->trace( "dumper result: " . Dumper @result) if $self->logger->is_trace; 544 545 $self->add_section({ 546 type => 'grid', 547 className => 'certificate', 548 content => { 549 actions => [{ 550 path => 'certificate!detail!identifier!{identifier}', 551 label => 'I18N_OPENXPKI_UI_DOWNLOAD_LABEL', 552 icon => 'download', 553 target => 'popup' 554 }], 555 columns => $self->__default_grid_head(), 556 data => \@result, 557 empty => 'I18N_OPENXPKI_UI_TASK_LIST_EMPTY_LABEL', 558 pager => $pager, 559 } 560 }); 561 562 return $self; 563 564} 565 566=head2 init_detail 567 568Show details on the certificate, includes basic certificate information, 569status, issuer and links to download chains and related workflow. Designed to 570be shown in a modal popup. 571 572=cut 573 574sub init_detail { 575 576 my $self = shift; 577 my $args = shift; 578 579 my $cert_identifier = $self->param('identifier'); 580 581 # empty submission 582 if (!$cert_identifier) { 583 $self->redirect('certificate!search'); 584 return; 585 } 586 587 my $cert = $self->send_command_v2( 'get_cert', { 588 identifier => $cert_identifier, 589 format => 'DBINFO', 590 attribute => 'subject_alt_name' }, 1); 591 592 if (!$cert) { 593 $self->_page({ 594 label => 'I18N_OPENXPKI_UI_CERTIFICATE_DETAIL_LABEL', 595 shortlabel => 'I18N_OPENXPKI_UI_CERT_STATUS_UNKNOWN' 596 }); 597 598 $self->add_section({ 599 type => 'keyvalue', 600 content => { 601 label => '', 602 description => '', 603 data => [ 604 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_IDENTIFIER', value => $cert_identifier }, 605 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_STATUS', value => { label => 'I18N_OPENXPKI_UI_CERT_STATUS_UNKNOWN' , value => 'unknown' }, format => 'certstatus' }, 606 ], 607 }}, 608 ); 609 610 return; 611 } 612 613 $self->logger()->trace("result: " . Dumper $cert) if $self->logger->is_trace; 614 615 my $cert_attribute = $cert->{cert_attributes}; 616 $self->logger()->trace("result: " . Dumper $cert_attribute) if $self->logger->is_trace; 617 618 my %dn = OpenXPKI::DN->new( $cert->{subject} )->get_hashed_content(); 619 620 $self->_page({ 621 label => 'I18N_OPENXPKI_UI_CERTIFICATE_DETAIL_LABEL', 622 shortlabel => $dn{CN}[0] 623 }); 624 625 my @fields = ( { label => 'I18N_OPENXPKI_UI_CERTIFICATE_SUBJECT', format => 'link', value => { 626 page => 'certificate!search!subject!'.uri_escape_utf8($cert->{subject}), 627 label => $self-> __prepare_dn_for_display($cert->{subject}), 628 target => '_top', 629 tooltip => 'I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_SIMILAR_SUBJECT' 630 }}); 631 632 if ($cert_attribute && $cert_attribute->{subject_alt_name}) { 633 my @sanlist = map { 634 { page => 'certificate!search!san!'.uri_escape_utf8($_), label => $_, target => '_top', tooltip => 'I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_SIMILAR_SAN' } 635 } @{$cert_attribute->{subject_alt_name}}; 636 push @fields, { label => 'I18N_OPENXPKI_UI_CERTIFICATE_SAN', value => \@sanlist, 'format' => 'linklist' }; 637 } 638 639 # check if this is a entity certificate from the current realm 640 my $is_local_entity = 0; 641 if ($cert->{req_key} && $cert->{pki_realm} eq $self->_session->param('pki_realm')) { 642 $self->logger()->debug("cert is local entity"); 643 $is_local_entity = 1; 644 } 645 646 if ($is_local_entity) { 647 my $cert_profile = $self->send_command_v2( 'get_profile_for_cert', { identifier => $cert_identifier }, 1); 648 if ($cert_profile) { 649 push @fields, { label => 'I18N_OPENXPKI_UI_CERTIFICATE_PROFILE', value => $cert_profile }; 650 } 651 } 652 653 my $status_label = 'I18N_OPENXPKI_UI_CERT_STATUS_'.$cert->{status}; 654 my $status_tooltip = ''; 655 if ($cert->{revocation_time}) { 656 $status_tooltip = 'I18N_OPENXPKI_UI_CERT_STATUS_REVOKED_AT: '.DateTime->from_epoch( epoch => $cert->{revocation_time} )->iso8601(); 657 if ($cert->{revocation_id}) { 658 $status_tooltip .= sprintf(' (#0x%02x)',$cert->{revocation_id} ); 659 } 660 } 661 if ($cert->{reason_code} && $cert->{reason_code} ne 'unspecified') { 662 $status_label .= sprintf(' (I18N_OPENXPKI_UI_CERTIFICATE_REASON_CODE_%s)', uc($cert->{reason_code})); 663 } 664 665 #I18N_OPENXPKI_UI_CERTIFICATE_REASON_CODE_UNSPECIFIED 666 #I18N_OPENXPKI_UI_CERTIFICATE_REASON_CODE_KEYCOMPROMISE 667 #I18N_OPENXPKI_UI_CERTIFICATE_REASON_CODE_CACOMPROMISE 668 #I18N_OPENXPKI_UI_CERTIFICATE_REASON_CODE_AFFILIATIONCHANGED 669 #I18N_OPENXPKI_UI_CERTIFICATE_REASON_CODE_SUPERSEDED 670 #I18N_OPENXPKI_UI_CERTIFICATE_REASON_CODE_CESSATIONOFOPERATION 671 672 push @fields, ( 673 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_SERIAL', format => 'ullist', 674 value => [ $cert->{cert_key_hex}, $cert->{cert_key} ], 675 className => 'certserial', 676 }, 677 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_IDENTIFIER', value => $cert_identifier }, 678 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_NOTBEFORE', value => $cert->{notbefore}, format => 'timestamp' }, 679 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_NOTAFTER', value => $cert->{notafter}, format => 'timestamp' }, 680 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_STATUS', format => 'certstatus', value => { 681 label => $status_label, 682 value => $cert->{status}, 683 tooltip => $status_tooltip, 684 }}, 685 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_ISSUER', format => 'link', value => { 686 label => $self-> __prepare_dn_for_display($cert->{issuer_dn}), 687 page => 'certificate!chain!identifier!'. $cert_identifier, 688 tooltip => 'I18N_OPENXPKI_UI_CERTIFICATE_DETAIL_ISSUER_LINK', 689 }}, 690 ); 691 692 # certificate metadata - show only for certificates from the current or empty realm 693 sub { 694 my $metadata_config = $self->_client->session()->param('certdetails')->{metadata}; 695 return unless ($metadata_config); 696 return unless (!$cert->{pki_realm} || $cert->{pki_realm} eq $self->_session->param('pki_realm')); 697 my $cert_attrs = $self->send_command_v2( get_cert_attributes => { 698 identifier => $cert_identifier, 699 attribute => 'meta_%', 700 $self->__tenant() }, 1); 701 return unless $cert_attrs; 702 my @metadata_lines; 703 704 for my $cfg (@$metadata_config) { 705 my $line; 706 if ($cfg->{template}) { 707 $line = $self->send_command_v2( render_template => { 708 template => $cfg->{template}, 709 params => $cert_attrs, 710 }); 711 } 712 else { 713 if (defined $cert_attrs->{ $cfg->{field} }) { 714 $line = sprintf '%s: %s', $cfg->{label}, join(',', @{ $cert_attrs->{ $cfg->{field} } }) // '-'; 715 } 716 } 717 push @metadata_lines, $line if ($line); 718 } 719 720 push @fields, ( 721 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_METADATA', value => \@metadata_lines, format => "rawlist" }, 722 ) if (scalar @metadata_lines); 723 }->(); 724 725 # for i18n parser I18N_OPENXPKI_CERT_ISSUED CRL_ISSUANCE_PENDING I18N_OPENXPKI_CERT_REVOKED I18N_OPENXPKI_CERT_EXPIRED 726 727 # was in info, bullet list for downloads 728 my $base = $self->_client()->_config()->{'scripturl'} . "?page=certificate!download!identifier!$cert_identifier!format!"; 729 push @fields, { label => 'I18N_OPENXPKI_UI_DOWNLOAD_LABEL', value => [ 730 { page => "${base}pem", label => 'I18N_OPENXPKI_UI_DOWNLOAD_PEM', format => 'extlink' }, 731 { page => "${base}der", label => 'I18N_OPENXPKI_UI_DOWNLOAD_DER', format => 'extlink' }, 732 { page => "${base}pkcs7", label => 'I18N_OPENXPKI_UI_DOWNLOAD_PKCS7', format => 'extlink' }, 733 { page => "${base}pkcs7!root!true", label => 'I18N_OPENXPKI_UI_DOWNLOAD_PKCS7_WITH_ROOT', format => 'extlink' }, 734 { page => "${base}bundle", label => 'I18N_OPENXPKI_UI_DOWNLOAD_BUNDLE', format => 'extlink' }, 735 { page => "${base}install", label => 'I18N_OPENXPKI_UI_DOWNLOAD_INSTALL', format => 'extlink' }, 736 { page => "certificate!text!identifier!$cert_identifier", label => 'I18N_OPENXPKI_UI_DOWNLOAD_SHOW_PEM' }, 737 { page => "certificate!text!format!txtpem!identifier!$cert_identifier", label => 'I18N_OPENXPKI_UI_DOWNLOAD_SHOW_TEXT' }, 738 ], 739 format => 'linklist' 740 }; 741 742 if ($is_local_entity) { 743 744 my $baseurl = 'workflow!index!cert_identifier!'.$cert_identifier.'!wf_type!'; 745 746 my @actions; 747 my $reply = $self->send_command_v2 ( "get_cert_actions", { identifier => $cert_identifier }); 748 749 $self->logger()->trace("available actions for cert " . Dumper $reply) if $self->logger->is_trace; 750 751 if (defined $reply->{workflow} && ref $reply->{workflow} eq 'ARRAY') { 752 foreach my $item (@{$reply->{workflow}}) { 753 push @actions, { page => $baseurl.$item->{workflow}, label => $item->{label}, target => '_blank' }; 754 } 755 } 756 757 push @fields, { 758 label => 'I18N_OPENXPKI_UI_CERT_ACTION_LABEL', 759 value => \@actions, 760 format => 'linklist' 761 } if (@actions); 762 } 763 764 765 # hide the related link if there is no data to display or cert is not from this realm 766 if (($cert->{pki_realm} eq $self->_session->param('pki_realm')) && 767 ($self->send_command_v2 ( "get_cert_attributes", { 768 identifier => $cert_identifier, 769 attribute => "system_workflow%", 770 $self->__tenant() 771 }))) { 772 push @fields, { label => 'I18N_OPENXPKI_UI_CERT_RELATED_LABEL', format => 'link', value => { 773 page => 'certificate!related!identifier!'.$cert_identifier, 774 label => 'I18N_OPENXPKI_UI_CERT_RELATED_HINT' 775 }}; 776 } 777 778 $self->add_section({ 779 type => 'keyvalue', 780 content => { 781 label => '', 782 description => '', 783 data => \@fields, 784 }}, 785 ); 786 787} 788 789=head2 init_text 790 791Show the PEM block as text in a popup 792 793=cut 794 795sub init_text { 796 797 my $self = shift; 798 my $args = shift; 799 800 my $cert_identifier = $self->param('identifier'); 801 802 my $format = uc($self->param('format') || ''); 803 804 if ($format !~ /\A(TXT|PEM|TXTPEM)\z/) { 805 $format = 'PEM'; 806 } 807 808 my $pem = $self->send_command_v2 ( "get_cert", {'identifier' => $cert_identifier, 'format' => $format }); 809 810 $self->logger()->trace("Cert data: " . Dumper $pem) if $self->logger->is_trace; 811 812 $self->_page({ 813 label => 'I18N_OPENXPKI_UI_CERTIFICATE_DETAIL_LABEL', 814 shortlabel => $cert_identifier, 815 isLarge => ($format ne 'PEM') ? 1 : 0, 816 }); 817 818 $self->add_section({ 819 type => 'text', 820 content => { 821 label => '', 822 description => '<pre>' . $pem . '</pre>', 823 }}, 824 ); 825 826 return $self; 827 828 829} 830 831=head2 init_chain 832 833Show the full chain of a certificate (subjects only) with inline download 834options for PEM/DER or browser install for each item of the chain. 835 836=cut 837 838sub init_chain { 839 840 my $self = shift; 841 my $args = shift; 842 843 my $cert_identifier = $self->param('identifier'); 844 845 my $chain = $self->send_command_v2 ( "get_chain", { start_with => $cert_identifier, format => 'DBINFO', 'keeproot' => 1 }); 846 847 $self->_page({ 848 label => 'I18N_OPENXPKI_UI_CERTIFICATE_CHAIN_LABEL', 849 shortlabel => 'I18N_OPENXPKI_UI_CERTIFICATE_CHAIN_LABEL', 850 }); 851 852 # Download links 853 my $base = $self->_client()->_config()->{'scripturl'} . "?page=certificate!download!identifier!%s!format!%s"; 854 my $pattern = '<li><a href="'.$base.'" target="_blank">%s</a></li>'; 855 856 foreach my $cert (@{$chain->{certificates}}) { 857 858 my $dl = '<ul class="list-inline">'. 859 sprintf ($pattern, $cert->{identifier}, 'pem', 'I18N_OPENXPKI_UI_DOWNLOAD_SHORT_PEM'). 860 sprintf ($pattern, $cert->{identifier}, 'der', 'I18N_OPENXPKI_UI_DOWNLOAD_SHORT_DER'). 861 sprintf ($pattern, $cert->{identifier}, 'install', 'I18N_OPENXPKI_UI_DOWNLOAD_SHORT_INSTALL'). 862 '</ul>'; 863 864 $self->add_section({ 865 type => 'keyvalue', 866 content => { 867 label => '', 868 description => '', 869 data => [ 870 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_SUBJECT', format => 'link', 'value' => { 871 label => $cert->{subject}, page => 'certificate!detail!identifier!'.$cert->{identifier} } }, 872 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_NOTBEFORE', value => $cert->{notbefore}, format => 'timestamp' }, 873 { label => 'I18N_OPENXPKI_UI_CERTIFICATE_NOTAFTER', value => $cert->{notafter}, format => 'timestamp' }, 874 { label => 'I18N_OPENXPKI_UI_DOWNLOAD_LABEL', value => $dl, format => 'raw' }, 875 ], 876 }}, 877 ); 878 } 879 880 return $self; 881 882} 883 884=head2 init_related 885 886Show information related to the certificate, renders a key/value table with 887a list of related workflows, owner, and metadata 888 889=cut 890sub init_related { 891 892 893 my $self = shift; 894 my $args = shift; 895 896 my $cert_identifier = $self->param('identifier'); 897 898 my $cert = $self->send_command_v2( 'get_cert', { identifier => $cert_identifier, format => 'DBINFO', attribute => 'system_workflow%' }); 899 $self->logger()->trace("result: " . Dumper $cert) if $self->logger->is_trace; 900 901 my %dn = OpenXPKI::DN->new( $cert->{subject} )->get_hashed_content(); 902 903 $self->_page({ 904 label => 'I18N_OPENXPKI_UI_CERTIFICATE_RELATIONS_LABEL', 905 shortlabel => $dn{CN}[0] 906 }); 907 908 # run a workflow search using the given ids from the cert attributes 909 my @wfid = values %{$cert->{cert_attributes}}; 910 911 $self->logger()->trace("related workflows " . Dumper \@wfid) if $self->logger->is_trace; 912 913 my @result; 914 if (scalar @wfid) { 915 my $cert_workflows = $self->send_command_v2( 'search_workflow_instances', { id => \@wfid, check_acl => 1, $self->__tenant() }); 916 $self->logger()->trace("workflow results" . Dumper $cert_workflows) if ($self->logger()->is_trace());; 917 918 my $workflow_labels = $self->send_command_v2( 'get_workflow_instance_types'); 919 920 foreach my $line (@{$cert_workflows}) { 921 my $label = $workflow_labels->{$line->{'workflow_type'}}->{label}; 922 push @result, [ 923 $line->{'workflow_id'}, 924 $label || $line->{'workflow_type'}, 925 $line->{'workflow_state'}, 926 $line->{'workflow_id'}, 927 ]; 928 } 929 } 930 931 $self->add_section({ 932 type => 'grid', 933 className => 'workflow', 934 content => { 935 label => 'I18N_OPENXPKI_UI_CERTIFICATE_RELATED_WORKFLOW_LABEL', 936 actions => [{ 937 path => 'workflow!load!wf_id!{serial}', 938 label => 'I18N_OPENXPKI_UI_WORKFLOW_OPEN_WORKFLOW_LABEL', 939 icon => 'view', 940 target => 'tab', 941 }], 942 columns => [ 943 { sTitle => "I18N_OPENXPKI_UI_WORKFLOW_SEARCH_SERIAL_LABEL" }, 944 { sTitle => "I18N_OPENXPKI_UI_WORKFLOW_TYPE_LABEL"}, 945 { sTitle => "I18N_OPENXPKI_UI_WORKFLOW_STATE_LABEL"}, 946 { sTitle => "serial", bVisible => 0 }, 947 ], 948 data => \@result, 949 empty => 'I18N_OPENXPKI_UI_TASK_LIST_EMPTY_LABEL', 950 } 951 }); 952 return $self; 953 954 955} 956 957=head2 init_download 958 959Handle download requests, required the cert_identifier and the expected format. 960Redirects to init_detail if no format is given. 961 962=cut 963sub init_download { 964 965 my $self = shift; 966 my $args = shift; 967 968 my $cert_identifier = $self->param('identifier'); 969 my $format = $self->param('format'); 970 971 # No format, call detail 972 if (!$format) { 973 return $self->init_detail(); 974 } 975 976 my $cert_info = $self->send_command_v2 ( "get_cert", {'identifier' => $cert_identifier, 'format' => 'DBINFO' }); 977 if (!$cert_info) { 978 $self->redirect('certificate!search'); 979 return; 980 } 981 982 $self->logger()->trace("cert info " . Dumper $cert_info ) if $self->logger->is_trace; 983 my %dn = OpenXPKI::DN->new( $cert_info->{subject} )->get_hashed_content(); 984 my $filename = $dn{CN}[0] || $dn{emailAddress}[0] || $cert_info->{identifier}; 985 986 my $content_type = 'application/octet-string'; 987 my $output = ''; 988 989 if ($format eq 'pkcs7') { 990 991 my $keeproot = $self->param('root') ? 1 : 0; 992 $output = $self->send_command_v2 ( "get_chain", { start_with => $cert_identifier, bundle => 1, keeproot => $keeproot }); 993 994 $filename .= ".p7b"; 995 $content_type = 'application/x-pkcs7-certificates'; 996 997 } elsif ($format eq 'bundle') { 998 999 my $chain = $self->send_command_v2 ( "get_chain", { start_with => $cert_identifier, format => 'PEM', 'keeproot' => 1 }); 1000 $self->logger()->trace("chain info " . Dumper $chain ) if $self->logger->is_trace; 1001 1002 for (my $i=0;$i<@{$chain->{certificates}};$i++) { 1003 $output .= $chain->{subject}->[$i]. "\n". $chain->{certificates}->[$i]."\n\n"; 1004 } 1005 1006 $filename .= ".bundle"; 1007 1008 } else { 1009 1010 my $cert_format = 'DER'; 1011 1012 if ($format eq 'txt') { 1013 $content_type = 'text/plain'; 1014 $cert_format = 'TXT'; 1015 $filename .= '.txt'; 1016 } elsif ($format eq 'pem') { 1017 $filename .= '.crt'; 1018 $cert_format = 'PEM'; 1019 } elsif ($format eq 'der') { 1020 $filename .= '.cer'; 1021 } else { 1022 # Default is to send the certifcate for install in binary / der form 1023 $filename .= '.cer'; 1024 if ($cert_info->{issuer_identifier} eq $cert_info->{identifier}) { 1025 $content_type = 'application/x-x509-ca-cert'; 1026 } else { 1027 $content_type = 'application/x-x509-user-cert'; 1028 } 1029 } 1030 1031 $output = $self->send_command_v2 ( "get_cert", {'identifier' => $cert_identifier, 'format' => $cert_format}); 1032 1033 } 1034 1035 print $self->cgi()->header( -type => $content_type, -expires => "1m", -attachment => $filename ); 1036 print $output; 1037 exit; 1038 1039} 1040 1041=head2 init_parse 1042 1043not implemented 1044 1045receive a PEM encoded x509/pkcs10/pkcs7 block and output information. 1046 1047=cut 1048sub init_parse { 1049 1050 my $self = shift; 1051 my $args = shift; 1052 1053 my $pem = $self->param('body'); 1054 1055 my @fields = ({ 1056 label => 'Body', 1057 value => $pem 1058 }); 1059 1060 $self->_page({ 1061 label => '', 1062 description => '' 1063 }); 1064 1065 $self->add_section({ 1066 type => 'keyvalue', 1067 content => { 1068 label => 'Parsed Content', 1069 description => '', 1070 data => \@fields, 1071 }}, 1072 ); 1073 1074 return $self; 1075 1076} 1077 1078=head2 action_autocomplete 1079 1080Handle searches via autocomplete, shows only entity certificates 1081 1082=cut 1083 1084sub action_autocomplete { 1085 1086 my $self = shift; 1087 my $args = shift; 1088 1089 my $term = $self->param('cert_identifier') || ''; 1090 my $params = $self->fetch_autocomplete_params; # from OpenXPKI::Client::UI::Result 1091 1092 $self->logger()->trace( "autocomplete query: $term") if $self->logger->is_trace; 1093 1094 1095 my @result; 1096 # If we see a string with length of 25 to 27 with only base64 chars 1097 # we assume it is a cert identifier - this might fail in few cases 1098 # Note - we replace + and / by - and _ in our base64 strings! 1099 if ($term =~ /[a-zA-Z0-9-_]{25,27}/) { 1100 $self->logger()->debug( "search for identifier: $term "); 1101 my $search_result = $self->send_command_v2( 'get_cert', { 1102 identifier => $term, 1103 format => 'DBINFO', 1104 }); 1105 1106 if (!$search_result) { 1107 1108 } elsif ($search_result->{pki_realm} ne $self->_session->param('pki_realm')) { 1109 # silently swallow this result and stop searching 1110 $term = ""; 1111 } else { 1112 push @result, { 1113 value => $search_result->{identifier}, 1114 label => $self->_escape($search_result->{subject}), 1115 notbefore => $search_result->{notbefore}, 1116 notafter => $search_result->{notafter} 1117 }; 1118 } 1119 } 1120 1121 # do not search with less then 3 letters 1122 if (!@result && (length($term) >= 3)) { 1123 my $search_result = $self->send_command_v2( 'search_cert', { 1124 subject => "%$term%", 1125 valid_before => time(), 1126 expires_after => time(), 1127 status => 'ISSUED', 1128 entity_only => 1, 1129 %$params, 1130 $self->__tenant(), 1131 }); 1132 1133 foreach my $item (@{$search_result}) { 1134 push @result, { 1135 value => $item->{identifier}, 1136 label => $self->_escape($item->{subject}), 1137 notbefore => $item->{notbefore}, 1138 notafter => $item->{notafter} 1139 }; 1140 } 1141 } 1142 1143 $self->logger()->trace( "search result: " . Dumper \@result) if $self->logger->is_trace; 1144 1145 $self->_result()->{_raw} = \@result; 1146 1147 return $self; 1148 1149} 1150 1151=head2 action_find 1152 1153Handle search requests for a single certificate by its identifier 1154 1155=cut 1156 1157sub action_find { 1158 1159 my $self = shift; 1160 my $args = shift; 1161 1162 my $cert_identifier = $self->param('cert_identifier'); 1163 if ($cert_identifier) { 1164 my $cert = $self->send_command_v2( 'get_cert', { identifier => $cert_identifier, format => 'DBINFO' }); 1165 if (!$cert) { 1166 $self->set_status('I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_NO_SUCH_IDENTIFIER','error'); 1167 return $self->init_search(); 1168 } 1169 } elsif (my $serial = $self->param('cert_serial')) { 1170 1171 if ($serial =~ /[a-f]/i && substr($serial,0,2) ne '0x') { 1172 $serial =~ s/://g; 1173 $serial = '0x' . $serial; 1174 } 1175 if (substr($serial,0,2) eq '0x') { 1176 # strip whitespace 1177 $serial =~ s/\s//g; 1178 my $sn = Math::BigInt->new( $serial ); 1179 $serial = $sn->bstr(); 1180 } 1181 my $search_result = $self->send_command_v2( 'search_cert', { 1182 return_columns => 'identifier', 1183 cert_serial => $serial, 1184 entity_only => 1, 1185 $self->__tenant(), 1186 }); 1187 if (!$search_result || @{$search_result} == 0) { 1188 $self->set_status('I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_NO_SUCH_SERIAL','error'); 1189 return $self->init_search(); 1190 1191 } elsif (scalar @{$search_result} == 1) { 1192 $cert_identifier = $search_result->[0]->{"identifier"}; 1193 1194 } else { 1195 # found more than one item with serial 1196 # this is a legal use case when using external CAs 1197 my $queryid = $self->__generate_uid(); 1198 my $spec = $self->_client->session()->param('certsearch')->{default}; 1199 $self->_client->session()->param('query_cert_'.$queryid, { 1200 'id' => $queryid, 1201 'type' => 'certificate', 1202 'count' => scalar @{$search_result}, 1203 'query' => { cert_serial => $serial, entity_only => 1, $self->__tenant() }, 1204 'input' => { cert_serial => scalar $self->param('cert_serial') }, 1205 'header' => $self->__default_grid_head, 1206 'column' => $self->__default_grid_row, 1207 'pager' => {}, 1208 'criteria' => [ sprintf '<nobr><b>I18N_OPENXPKI_UI_CERTIFICATE_SERIAL:</b> <i>%s</i></nobr>', $self->param('cert_serial') ] 1209 }); 1210 1211 return $self->redirect( 'certificate!result!id!'.$queryid ); 1212 } 1213 } else { 1214 $self->set_status('I18N_OPENXPKI_UI_CERTIFICATE_SEARCH_MUST_PROIVDE_IDENTIFIER_OR_SERIAL','error'); 1215 return $self->init_search(); 1216 } 1217 1218 $self->redirect( 'certificate!detail!identifier!'.$cert_identifier ); 1219 1220} 1221 1222=head2 action_search 1223 1224Handle search requests and display the result as grid 1225 1226=cut 1227 1228sub action_search { 1229 1230 1231 my $self = shift; 1232 my $args = shift; 1233 1234 1235 $self->logger()->trace("input params: " . Dumper $self->cgi()->param()) if $self->logger->is_trace; 1236 1237 my $query = { entity_only => 1, $self->__tenant() }; 1238 my $input = {}; # store the input data the reopen the form later 1239 my $verbose = {}; 1240 foreach my $key (qw(subject issuer_dn)) { 1241 my $val = $self->param($key); 1242 $self->logger()->trace("$key: " . ($val//'')); 1243 if (defined $val && $val ne '') { 1244 $query->{$key} = '%'.$val.'%'; 1245 $input->{$key} = $val; 1246 $verbose->{$key} = $val; 1247 } 1248 } 1249 1250 foreach my $key (qw(profile issuer_identifier)) { 1251 my $val = $self->param($key); 1252 if (defined $val && $val ne '') { 1253 $input->{$key} = $val; 1254 $query->{$key} = $val; 1255 if ($key eq 'profile') { 1256 $verbose->{$key} = $self->send_command_v2( 'render_template', { 1257 template => '[% USE Profile %][% Profile.name(value) %]', params => { value => $val } 1258 }); 1259 } elsif ($key eq 'issuer_identifier') { 1260 $verbose->{$key} = $self->send_command_v2( 'render_template', { 1261 template => '[% USE Certificate %][% Certificate.body(value, "subject") %]', params => { value => $val } 1262 }); 1263 } 1264 } 1265 } 1266 1267 if (my $status = $self->param('status')) { 1268 $input->{'status'} = $status; 1269 $verbose->{'status'} = 'I18N_OPENXPKI_UI_CERT_STATUS_'.uc($status); 1270 $query->{status} = $status; 1271 } 1272 1273 # Validity 1274 $input->{validity_options} = []; 1275 foreach my $key (qw(valid_before valid_after expires_before expires_after 1276 revoked_before revoked_after invalid_before invalid_after valid_at)) { 1277 my $val = $self->param($key); 1278 next unless ($val); 1279 if ($val =~ /[^0-9]/) { 1280 $self->logger()->warn('skipping non-numeric value for validity option ' .$key); 1281 next; 1282 } 1283 push @{$input->{validity_options}}, { key => $key, value => $val }; 1284 1285 $verbose->{$key} = DateTime->from_epoch( epoch => $val )->iso8601(); 1286 1287 if ($key eq 'valid_at') { 1288 if (!$query->{valid_before} || $query->{valid_before} > $val) { 1289 $query->{valid_before} = $val; 1290 } 1291 if (!$query->{expires_after} || $query->{expires_after} < $val) { 1292 $query->{expires_after} = $val; 1293 } 1294 } else { 1295 $query->{$key} = $val; 1296 } 1297 } 1298 1299 # Read the query pattern for extra attributes from the session 1300 my $spec = $self->_client->session()->param('certsearch')->{default}; 1301 my $attr = $self->__build_attribute_subquery( $spec->{attributes} ); 1302 1303 if ($attr) { 1304 $input->{attributes} = $self->__build_attribute_preset( $spec->{attributes} ); 1305 } 1306 1307 # Add san search to attributes 1308 if (my $val = $self->param('san')) { 1309 $input->{'san'} = $val; 1310 # The serialization format was extended in v3.5 from a simple join 1311 # to use OXI::Serialize - currently this is used only for dirName 1312 # search needs to be fixed to find dirName items, see #755 1313 # if the san type was given by the user, strip it 1314 my $type = '%'; 1315 if ($val =~ m{\A(\w+):(.*)}) { 1316 $type = $1; 1317 $val = $2; 1318 } 1319 $val =~ s{\*}{%}g; 1320 $val =~ s{\?}{_}g; 1321 $attr->{subject_alt_name} = { -like => "$type:$val" }; 1322 } 1323 1324 if ($attr) { 1325 $query->{cert_attributes} = $attr; 1326 } 1327 1328 $self->logger()->debug("query : " . Dumper $query) if $self->logger->is_debug; 1329 1330 1331 my $result_count = $self->send_command_v2( 'search_cert_count', $query ); 1332 1333 if (not defined $result_count) { 1334 return $self; 1335 } 1336 1337 # No results founds 1338 if (!$result_count) { 1339 $self->set_status('I18N_OPENXPKI_UI_SEARCH_HAS_NO_MATCHES','error'); 1340 return $self->init_search({ preset => $input }); 1341 } 1342 1343 # check if there is a custom column set defined 1344 my ($header, $body, $cols); 1345 if ($spec->{cols} && ref $spec->{cols} eq 'ARRAY') { 1346 ($header, $body, $cols) = $self->__render_list_spec( $spec->{cols} ); 1347 } else { 1348 $body = $self->__default_grid_row; 1349 $header = $self->__default_grid_head; 1350 } 1351 1352 if ($cols) { 1353 $query->{return_attributes} = $cols; 1354 } 1355 1356 my %rcols; 1357 foreach my $ff (@{$body}) { 1358 next unless ($ff->{source} eq 'certificate'); 1359 if ($ff->{field} eq 'statusclass') { 1360 $rcols{'status'} = 1; 1361 } elsif ($ff->{field} eq 'cert_key_hex') { 1362 $rcols{'cert_key'} = 1; 1363 } else { 1364 $rcols{ $ff->{field} } = 1; 1365 } 1366 } 1367 $query->{return_columns} = [ keys %rcols ]; 1368 1369 my @criteria; 1370 foreach my $item (( 1371 { name => 'subject', label => 'I18N_OPENXPKI_UI_CERTIFICATE_SUBJECT' }, 1372 { name => 'san', label => 'I18N_OPENXPKI_UI_CERTIFICATE_SAN' }, 1373 { name => 'status', label => 'I18N_OPENXPKI_UI_CERTIFICATE_STATUS' }, 1374 { name => 'profile', label => 'I18N_OPENXPKI_UI_CERTIFICATE_PROFILE' }, 1375 { name => 'issuer_identifier', label => 'I18N_OPENXPKI_UI_CERTIFICATE_ISSUER' } 1376 )) { 1377 1378 my $val = $verbose->{ $item->{name} }; 1379 next unless ($val); 1380 $val =~ s/[^\w\s*\,\-\=]//g; 1381 push @criteria, sprintf '<nobr><b>%s:</b> <i>%s</i></nobr>', $item->{label}, $val; 1382 } 1383 1384 foreach my $item (@{$self->__validity_options()}) { 1385 my $val = $verbose->{ $item->{value} }; 1386 next unless ($val); 1387 push @criteria, sprintf '<nobr><b>%s:</b> <i>%s</i></nobr>', $item->{label}, $val; 1388 } 1389 1390 my $queryid = $self->__generate_uid(); 1391 $self->_client->session()->param('query_cert_'.$queryid, { 1392 'id' => $queryid, 1393 'type' => 'certificate', 1394 'count' => $result_count, 1395 'query' => $query, 1396 'input' => $input, 1397 'header' => $header, 1398 'column' => $body, 1399 'pager' => $spec->{pager} || {}, 1400 'criteria' => \@criteria 1401 }); 1402 1403 $self->redirect( 'certificate!result!id!'.$queryid ); 1404 1405 return $self; 1406 1407} 1408 1409=head2 __render_result_list 1410 1411Helper to render the output result list from a sql query result. 1412 1413 1414=cut 1415sub __render_result_list { 1416 1417 my $self = shift; 1418 my $search_result = shift; 1419 my $colums = shift; 1420 1421 my @result; 1422 foreach my $item (@{$search_result}) { 1423 1424 $item->{status} = 'EXPIRED' if ($item->{status} eq 'ISSUED' && $item->{notafter} < time()); 1425 1426 # if you add patterns you also need to add those in init_export! 1427 my @line; 1428 foreach my $col (@{$colums}) { 1429 if ($col->{field} eq 'status') { 1430 push @line, { label => 'I18N_OPENXPKI_UI_CERT_STATUS_'.$item->{status} , value => $item->{status} }; 1431 } elsif ($col->{field} eq 'statusclass') { 1432 push @line, lc($item->{status}); 1433 } elsif ($col->{field} eq 'cert_key_hex') { 1434 push @line, unpack('H*', Math::BigInt->new( $item->{cert_key})->to_bytes ); 1435 } else { 1436 push @line, $item->{ $col->{field} }; 1437 } 1438 } 1439 push @result, \@line; 1440 1441 } 1442 1443 return @result; 1444 1445} 1446 1447 1448=head2 __render_list_spec 1449 1450Create array to pass to UI from specification in config file 1451 1452=cut 1453 1454sub __render_list_spec { 1455 1456 my $self = shift; 1457 my $cols = shift; 1458 1459 my @header; 1460 my @column; 1461 my @attrib; 1462 1463 for (my $ii = 0; $ii < scalar @{$cols}; $ii++) { 1464 1465 # we must create a copy as we change the hash in the session info otherwise 1466 my %col = %{$cols->[$ii]}; 1467 my $head = { sTitle => $col{label} }; 1468 if ($col{sortkey}) { 1469 $head->{sortkey} = $col{sortkey}; 1470 } 1471 if ($col{format}) { 1472 $head->{format} = $col{format}; 1473 } 1474 push @header, $head; 1475 1476 if ($col{template}) { 1477 1478 } elsif ($col{field} =~ m{\A (csr|attribute)\.(\S+) }xi) { 1479 # we use this later to avoid the pattern match 1480 $col{source} = $1; 1481 $col{field} = $2; 1482 1483 push @attrib, $2 if ($1 eq 'attribute'); 1484 } else { 1485 $col{source} = 'certificate'; 1486 $col{field} = $col{field} 1487 1488 } 1489 push @column, \%col; 1490 } 1491 1492 push @header, { sTitle => 'identifier', bVisible => 0 }; 1493 push @header, { sTitle => "_className"}; 1494 1495 push @column, { source => 'certificate', field => 'identifier' }; 1496 push @column, { source => 'certificate', field => 'statusclass' }; 1497 1498 return ( \@header, \@column, \@attrib ); 1499} 1500 1501sub __prepare_dn_for_display { 1502 1503 my $self = shift; 1504 my $dn = shift; 1505 my @dn = OpenXPKI::DN->new( $dn )->get_rdns(); 1506 for (my $ii=1; $ii < @dn; $ii++ ) { 1507 $dn[$ii-1] .= ','; 1508 } 1509 return \@dn; 1510} 1511 1512 15131; 1514