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