Bug 19768: DBRev 23.12.00.054
[koha.git] / about.pl
1 #!/usr/bin/perl
2
3 # Copyright Pat Eyler 2003
4 # Copyright Biblibre 2006
5 # Parts Copyright Liblime 2008
6 # Parts Copyright Chris Nighswonger 2010
7 #
8 # This file is part of Koha.
9 #
10 # Koha is free software; you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # Koha is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22
23 use Modern::Perl;
24
25 use CGI qw ( -utf8 );
26 use DateTime::TimeZone;
27 use File::Slurp qw( read_file );
28 use IPC::Cmd qw(can_run);
29 use List::MoreUtils qw( any );
30 use Module::Load::Conditional qw( can_load );
31 use Config qw( %Config );
32 use Search::Elasticsearch;
33 use Try::Tiny qw( catch try );
34 use YAML::XS;
35 use Encode;
36
37 use C4::Output qw( output_html_with_http_headers );
38 use C4::Auth qw( get_template_and_user get_user_subpermissions );
39 use C4::Context;
40 use C4::Installer;
41 use C4::Installer::PerlModules;
42
43 use Koha;
44 use Koha::DateUtils qw( dt_from_string output_pref );
45 use Koha::Acquisition::Currencies;
46 use Koha::Authorities;
47 use Koha::BackgroundJob;
48 use Koha::BiblioFrameworks;
49 use Koha::Biblios;
50 use Koha::Email;
51 use Koha::I18N;
52 use Koha::Patron::Categories;
53 use Koha::Patrons;
54 use Koha::Caches;
55 use Koha::Config::SysPrefs;
56 use Koha::ILL::Request::Config;
57 use Koha::SearchEngine::Elasticsearch;
58 use Koha::Logger;
59 use Koha::Filter::MARC::ViewPolicy;
60
61 use C4::Members::Statistics;
62
63 my $query = CGI->new;
64 my $params = $query->Vars();
65
66 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
67     {
68         template_name   => "about.tt",
69         query           => $query,
70         type            => "intranet",
71         flagsrequired   => { catalogue => 1 },
72     }
73 );
74
75 my $tab = $params->{tab} || 'about';
76
77 $template->param(
78     tab => $tab,
79 );
80
81 my $docdir;
82 if ( defined C4::Context->config('docdir') ) {
83     $docdir = C4::Context->config('docdir');
84 } else {
85
86     # if no <docdir> is defined in koha-conf.xml, use the default location
87     # this is a work-around to stop breakage on upgraded Kohas, bug 8911
88     $docdir = C4::Context->config('intranetdir') . '/docs';
89 }
90
91 my %versions = C4::Context::get_versions();
92
93 if ( $tab eq 'about' ) {
94
95     my $config_timezone = C4::Context->config('timezone') // '';
96     my $config_invalid  = !DateTime::TimeZone->is_valid_name( $config_timezone );
97     my $env_timezone    = $ENV{TZ} // '';
98     my $env_invalid     = !DateTime::TimeZone->is_valid_name( $env_timezone );
99     my $actual_bad_tz_fallback = 0;
100
101     if ( $config_timezone ne '' &&
102         $config_invalid ) {
103         # Bad config
104         $actual_bad_tz_fallback = 1;
105     }
106     elsif ( $config_timezone eq '' &&
107             $env_timezone    ne '' &&
108             $env_invalid ) {
109         # No config, but bad ENV{TZ}
110         $actual_bad_tz_fallback = 1;
111     }
112
113     my $time_zone = {
114         actual                 => C4::Context->tz->name,
115         actual_bad_tz_fallback => $actual_bad_tz_fallback,
116         config                 => $config_timezone,
117         config_invalid         => $config_invalid,
118         environment            => $env_timezone,
119         environment_invalid    => $env_invalid
120     };
121
122     { # Logger checks
123         my $log4perl_config = C4::Context->config("log4perl_conf");
124         my @log4perl_errors;
125         if ( ! $log4perl_config ) {
126             push @log4perl_errors, 'missing_config_entry'
127         }
128         else {
129             my @lines = read_file($log4perl_config) or push @log4perl_errors, 'cannot_read_config_file';
130             for my $line ( @lines ) {
131                 next unless $line =~ m|log4perl.appender.\w+.filename=(.*)|;
132                 push @log4perl_errors, 'logfile_not_writable' unless -w $1;
133             }
134         }
135         eval {Koha::Logger->get};
136         push @log4perl_errors, 'cannot_init_module' and warn $@ if $@;
137         $template->param( log4perl_errors => @log4perl_errors );
138     }
139
140     $template->param(
141         time_zone              => $time_zone,
142         current_date_and_time  => output_pref({ dt => dt_from_string(), dateformat => 'iso' })
143     );
144
145     my $perl_path = $^X;
146     if ($^O ne 'VMS') {
147         $perl_path .= $Config{_exe} unless $perl_path =~ m/$Config{_exe}$/i;
148     }
149
150     my $zebraVersion = `zebraidx -V`;
151
152     # Check running PSGI env
153     if ( C4::Context->psgi_env ) {
154         $template->param(
155             is_psgi => 1,
156             psgi_server => ($ENV{ PLACK_ENV }) ? "Plack ($ENV{PLACK_ENV})" :
157                         ($ENV{ MOD_PERL })  ? "mod_perl ($ENV{MOD_PERL})" :
158                                                 'Unknown'
159         );
160     }
161
162     # Memcached configuration
163     my $memcached_servers   = $ENV{MEMCACHED_SERVERS} || C4::Context->config('memcached_servers');
164     my $memcached_namespace = $ENV{MEMCACHED_NAMESPACE} || C4::Context->config('memcached_namespace') // 'koha';
165
166     my $cache = Koha::Caches->get_instance;
167     my $effective_caching_method = ref($cache->cache);
168     # Memcached may have been running when plack has been initialized but could have been stopped since
169     # FIXME What are the consequences of that??
170     my $is_memcached_still_active = $cache->set_in_cache('test_for_about_page', "just a simple value");
171
172     my $where_is_memcached_config = 'nowhere';
173     if ( $ENV{MEMCACHED_SERVERS} and C4::Context->config('memcached_servers') ) {
174         $where_is_memcached_config = 'both';
175     } elsif ( $ENV{MEMCACHED_SERVERS} and not C4::Context->config('memcached_servers') ) {
176         $where_is_memcached_config = 'ENV_only';
177     } elsif ( C4::Context->config('memcached_servers') ) {
178         $where_is_memcached_config = 'config_only';
179     }
180
181     $template->param(
182         effective_caching_method => $effective_caching_method,
183         memcached_servers   => $memcached_servers,
184         memcached_namespace => $memcached_namespace,
185         is_memcached_still_active => $is_memcached_still_active,
186         where_is_memcached_config => $where_is_memcached_config,
187         perlPath      => $perl_path,
188         zebraVersion  => $zebraVersion,
189         kohaVersion   => $versions{'kohaVersion'},
190         osVersion     => $versions{'osVersion'},
191         perlVersion   => $versions{'perlVersion'},
192         perlIncPath   => [ map { perlinc => $_ }, @INC ],
193         mysqlVersion  => $versions{'mysqlVersion'},
194         apacheVersion => $versions{'apacheVersion'},
195         memcached_running   => Koha::Caches->get_instance->memcached_cache,
196     );
197
198 }
199
200 if($tab eq 'sysinfo') {
201
202     # Additional system information for warnings
203
204     my $warnStatisticsFieldsError;
205     my $prefStatisticsFields = C4::Context->preference('StatisticsFields');
206     if ($prefStatisticsFields) {
207         $warnStatisticsFieldsError = $prefStatisticsFields
208             unless ( $prefStatisticsFields eq C4::Members::Statistics->get_fields() );
209     }
210
211     my $prefAutoCreateAuthorities = C4::Context->preference('AutoCreateAuthorities');
212     my $prefRequireChoosingExistingAuthority = C4::Context->preference('RequireChoosingExistingAuthority');
213     my $warnPrefRequireChoosingExistingAuthority = ( !$prefAutoCreateAuthorities && ( !$prefRequireChoosingExistingAuthority) );
214
215     my $prefEasyAnalyticalRecords  = C4::Context->preference('EasyAnalyticalRecords');
216     my $prefUseControlNumber  = C4::Context->preference('UseControlNumber');
217     my $warnPrefEasyAnalyticalRecords  = ( $prefEasyAnalyticalRecords  && $prefUseControlNumber );
218
219     my $AnonymousPatron = C4::Context->preference('AnonymousPatron');
220     my $warnPrefAnonymousPatronOPACPrivacy = (
221         C4::Context->preference('OPACPrivacy')
222             and not $AnonymousPatron
223     );
224     my $warnPrefAnonymousPatronAnonSuggestions = (
225         C4::Context->preference('AnonSuggestions')
226             and not $AnonymousPatron
227     );
228
229     my $anonymous_patron = Koha::Patrons->find( $AnonymousPatron );
230     my $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist = ( $AnonymousPatron && C4::Context->preference('AnonSuggestions') && not $anonymous_patron );
231
232     my $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist = ( not $anonymous_patron and Koha::Patrons->search({ privacy => 2 })->count );
233
234     my $warnPrefKohaAdminEmailAddress = !Koha::Email->is_valid(C4::Context->preference('KohaAdminEmailAddress'));
235
236     my $c = Koha::Items->filter_by_visible_in_opac->count;
237     my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
238     my $warnPrefOpacHiddenItems = $warnings[2];
239
240     my $invalid_yesno = Koha::Config::SysPrefs->search(
241         {
242             type  => 'YesNo',
243             value => { -or => { 'is' => undef, -not_in => [ "1", "0" ] } }
244         }
245     );
246     $template->param( invalid_yesno => $invalid_yesno );
247
248     my $errZebraConnection = C4::Context->Zconn("biblioserver",0)->errcode();
249
250     my $warnIsRootUser   = (! $loggedinuser);
251
252     my $warnNoActiveCurrency = (! defined Koha::Acquisition::Currencies->get_active);
253
254     my @xml_config_warnings;
255
256     if (    C4::Context->config('zebra_bib_index_mode')
257         and C4::Context->config('zebra_bib_index_mode') eq 'grs1' )
258     {
259         push @xml_config_warnings, { error => 'zebra_bib_index_mode_is_grs1' };
260     }
261
262     if (    C4::Context->config('zebra_auth_index_mode')
263         and C4::Context->config('zebra_auth_index_mode') eq 'grs1' )
264     {
265         push @xml_config_warnings, { error => 'zebra_auth_index_mode_is_grs1' };
266     }
267
268     my $authorityserver = C4::Context->zebraconfig('authorityserver');
269     if( (   C4::Context->config('zebra_auth_index_mode')
270         and C4::Context->config('zebra_auth_index_mode') eq 'dom' )
271         && ( $authorityserver->{config} !~ /zebra-authorities-dom.cfg/ ) )
272     {
273         push @xml_config_warnings, {
274             error => 'zebra_auth_index_mode_mismatch_warn'
275         };
276     }
277
278     if ( ! defined C4::Context->config('log4perl_conf') ) {
279         push @xml_config_warnings, {
280             error => 'log4perl_entry_missing'
281         }
282     }
283
284     if ( ! defined C4::Context->config('lockdir') ) {
285         push @xml_config_warnings, {
286             error => 'lockdir_entry_missing'
287         }
288     }
289     else {
290         unless ( -w C4::Context->config('lockdir') ) {
291             push @xml_config_warnings, {
292                 error   => 'lockdir_not_writable',
293                 lockdir => C4::Context->config('lockdir')
294             }
295         }
296     }
297
298     if ( ! defined C4::Context->config('upload_path') ) {
299         if ( Koha::Config::SysPrefs->find('OPACBaseURL')->value ) {
300             # OPACBaseURL seems to be set
301             push @xml_config_warnings, {
302                 error => 'uploadpath_entry_missing'
303             }
304         } else {
305             push @xml_config_warnings, {
306                 error => 'uploadpath_and_opacbaseurl_entry_missing'
307             }
308         }
309     }
310
311     if ( ! C4::Context->config('tmp_path') ) {
312         my $temporary_directory = C4::Context::temporary_directory;
313         push @xml_config_warnings, {
314             error             => 'tmp_path_missing',
315             effective_tmp_dir => $temporary_directory,
316         }
317     }
318
319     my $encryption_key = C4::Context->config('encryption_key');
320     if ( !$encryption_key || $encryption_key eq '__ENCRYPTION_KEY__') {
321         push @xml_config_warnings, { error => 'encryption_key_missing' };
322     }
323
324     # Test Zebra facets configuration
325     if ( !defined C4::Context->config('use_zebra_facets') ) {
326         push @xml_config_warnings, { error => 'use_zebra_facets_entry_missing' };
327     }
328
329     unless ( Koha::I18N->_base_directory ) {
330         $template->param( warnI18nMissing => 1 );
331     }
332
333     # ILL module checks
334     if ( C4::Context->preference('ILLModule') ) {
335         my $warnILLConfiguration = 0;
336         my $ill_config_from_file = C4::Context->config("interlibrary_loans");
337         my $ill_config = Koha::ILL::Request::Config->new;
338
339         my $available_ill_backends =
340         ( scalar @{ $ill_config->available_backends } > 0 );
341
342         # Check backends
343         if ( !$available_ill_backends ) {
344             $template->param( no_ill_backends => 1 );
345             $warnILLConfiguration = 1;
346         }
347
348         # Check ILLPartnerCode sys pref
349         if ( !Koha::Patron::Categories->find( C4::Context->preference('ILLPartnerCode') ) ) {
350             $template->param( ill_partner_code_doesnt_exist => C4::Context->preference('ILLPartnerCode') );
351             $warnILLConfiguration = 1;
352         } elsif ( !Koha::Patrons->search( { categorycode => C4::Context->preference('ILLPartnerCode') } )->count ) {
353             $template->param( ill_partner_code_no_patrons => C4::Context->preference('ILLPartnerCode') );
354             $warnILLConfiguration = 1;
355         }
356
357         if ( !C4::Context->preference('ILLPartnerCode') ) {
358             # partner code not defined
359             $template->param( ill_partner_code_not_defined => 1 );
360             $warnILLConfiguration = 1;
361         }
362
363
364         if ( !$ill_config_from_file->{branch} ) {
365             # branch not defined
366             $template->param( ill_branch_not_defined => 1 );
367             $warnILLConfiguration = 1;
368         }
369
370         $template->param( warnILLConfiguration => $warnILLConfiguration );
371     }
372     unless ( can_run('weasyprint') ) {
373         $template->param( weasyprint_missing => 1 );
374     }
375
376     {
377         # XSLT sysprefs
378         my @xslt_prefs = qw(
379             OPACXSLTDetailsDisplay
380             OPACXSLTListsDisplay
381             OPACXSLTResultsDisplay
382             XSLTDetailsDisplay
383             XSLTListsDisplay
384             XSLTResultsDisplay
385         );
386         my @warnXSLT;
387         for my $p ( @xslt_prefs ) {
388             my $xsl_filename = C4::XSLT::get_xsl_filename( $p );
389             next if -e $xsl_filename;
390             push @warnXSLT,
391             {
392                 syspref  => $p,
393                 value    => C4::Context->preference("$p"),
394                 filename => $xsl_filename
395             };
396         }
397
398         $template->param( warnXSLT => \@warnXSLT ) if @warnXSLT;
399     }
400
401     if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) {
402         # Check ES configuration health and runtime status
403
404         my $es_status;
405         my $es_config_error;
406         my $es_running = 1;
407         my $es_has_missing = 0;
408
409         my $es_conf;
410         try {
411             $es_conf = Koha::SearchEngine::Elasticsearch::_read_configuration();
412         }
413         catch {
414             if ( ref($_) eq 'Koha::Exceptions::Config::MissingEntry' ) {
415                 $template->param( elasticsearch_fatal_config_error => $_->message );
416                 $es_config_error = 1;
417             }
418         };
419         if ( !$es_config_error ) {
420
421             my $biblios_index_name     = $es_conf->{index_name} . "_" . $Koha::SearchEngine::BIBLIOS_INDEX;
422             my $authorities_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::AUTHORITIES_INDEX;
423
424             my @indexes = ($biblios_index_name, $authorities_index_name);
425             # TODO: When new indexes get added, we could have other ways to
426             #       fetch the list of available indexes (e.g. plugins, etc)
427             $es_status->{nodes} = $es_conf->{nodes};
428             my $es = Search::Elasticsearch->new( $es_conf );
429             my $es_status->{version} = $es->info->{version}->{number};
430
431             foreach my $index ( @indexes ) {
432                 my $index_count;
433                 try {
434                     $index_count = $es->indices->stats( index => $index )
435                         ->{_all}{primaries}{docs}{count};
436                 }
437                 catch {
438                     if ( ref($_) eq 'Search::Elasticsearch::Error::Missing' ) {
439                         push @{ $es_status->{errors} }, "Index not found ($index)";
440                         $index_count = -1;
441                     }
442                     elsif ( ref($_) eq 'Search::Elasticsearch::Error::NoNodes' ) {
443                         $es_running = 0;
444                     }
445                     else {
446                         # TODO: when time comes, we will cover more use cases
447                         die $_;
448                     }
449                 };
450
451                 my $db_count = -1;
452                 my $missing_count = 0;
453                 if ( $index eq $biblios_index_name ) {
454                     $db_count = Koha::Biblios->search->count;
455                 } elsif ( $index eq $authorities_index_name ) {
456                     $db_count = Koha::Authorities->search->count;
457                 }
458                 if ( $db_count != -1 && $index_count != -1 ) {
459                     $missing_count = $db_count - $index_count;
460                     $es_has_missing = 1 if $missing_count > 0;
461                 }
462                 push @{ $es_status->{indexes} },
463                 {
464                     index_name    => $index,
465                     index_count   => $index_count,
466                     db_count      => $db_count,
467                     missing_count => $missing_count,
468                 };
469             }
470             $es_status->{running} = $es_running;
471
472             $template->param(
473                 elasticsearch_status      => $es_status,
474                 elasticsearch_has_missing => $es_has_missing,
475             );
476         }
477     }
478
479     if ( C4::Context->preference('RESTOAuth2ClientCredentials') ) {
480         # Do we have the required deps?
481         unless ( can_load( modules => { 'Net::OAuth2::AuthorizationServer' => undef }) ) {
482             $template->param( oauth2_missing_deps => 1 );
483         }
484     }
485
486     # Sco Patron should not contain any other perms than circulate => self_checkout
487     if (  C4::Context->preference('WebBasedSelfCheck')
488         and C4::Context->preference('AutoSelfCheckAllowed')
489     ) {
490         my $userid = C4::Context->preference('AutoSelfCheckID');
491         my $all_permissions = C4::Auth::get_user_subpermissions( $userid );
492         my ( $has_self_checkout_perm, $has_other_permissions );
493         while ( my ( $module, $permissions ) = each %$all_permissions ) {
494             if ( $module eq 'self_check' ) {
495                 while ( my ( $permission, $flag ) = each %$permissions ) {
496                     if ( $permission eq 'self_checkout_module' ) {
497                         $has_self_checkout_perm = 1;
498                     } else {
499                         $has_other_permissions = 1;
500                     }
501                 }
502             } else {
503                 $has_other_permissions = 1;
504             }
505         }
506         $template->param(
507             AutoSelfCheckPatronDoesNotHaveSelfCheckPerm => not ( $has_self_checkout_perm ),
508             AutoSelfCheckPatronHasTooManyPerm => $has_other_permissions,
509         );
510     }
511
512     if ( C4::Context->preference('PatronSelfRegistration') ) {
513         $template->param( warnPrefPatronSelfRegistrationDefaultCategory => 1 )
514             unless  Koha::Patron::Categories->find(C4::Context->preference('PatronSelfRegistrationDefaultCategory'));
515     }
516
517     # Test YAML system preferences
518     # FIXME: This is list of current YAML formatted prefs, should by type of preference
519     my @yaml_prefs = (
520         "BibtexExportAdditionalFields",
521         "ItemsDeniedRenewal",
522         "MarcFieldsToOrder",
523         "MarcItemFieldsToOrder",
524         "OpacHiddenItems",
525         "RisExportAdditionalFields",
526         "UpdateitemLocationOnCheckin",
527         "UpdateItemWhenLostFromHoldList",
528         "UpdateNotForLoanStatusOnCheckin",
529         "UpdateNotForLoanStatusOnCheckout",
530     );
531     my @bad_yaml_prefs;
532     foreach my $syspref (@yaml_prefs) {
533         my $yaml = C4::Context->preference( $syspref );
534         if ( $yaml ) {
535             eval { YAML::XS::Load( Encode::encode_utf8("$yaml\n\n") ); };
536             if ($@) {
537                 push @bad_yaml_prefs, $syspref;
538             }
539         }
540     }
541     $template->param( 'bad_yaml_prefs' => \@bad_yaml_prefs ) if @bad_yaml_prefs;
542
543     {
544         my $dbh       = C4::Context->dbh;
545         my $patrons = $dbh->selectall_arrayref(
546             q|select b.borrowernumber from borrowers b join deletedborrowers db on b.borrowernumber=db.borrowernumber|,
547             { Slice => {} }
548         );
549         my $biblios = $dbh->selectall_arrayref(
550             q|select b.biblionumber from biblio b join deletedbiblio db on b.biblionumber=db.biblionumber|,
551             { Slice => {} }
552         );
553         my $biblioitems = $dbh->selectall_arrayref(
554             q|select bi.biblioitemnumber from biblioitems bi join deletedbiblioitems dbi on bi.biblionumber=dbi.biblionumber|,
555             { Slice => {} }
556         );
557         my $items = $dbh->selectall_arrayref(
558             q|select i.itemnumber from items i join deleteditems di on i.itemnumber=di.itemnumber|,
559             { Slice => {} }
560         );
561         my $checkouts = $dbh->selectall_arrayref(
562             q|select i.issue_id from issues i join old_issues oi on i.issue_id=oi.issue_id|,
563             { Slice => {} }
564         );
565         my $holds = $dbh->selectall_arrayref(
566             q|select r.reserve_id from reserves r join old_reserves o on r.reserve_id=o.reserve_id|,
567             { Slice => {} }
568         );
569         if ( @$patrons or @$biblios or @$biblioitems or @$items or @$checkouts or @$holds ) {
570             $template->param(
571                 has_ai_issues  => 1,
572                 ai_patrons     => $patrons,
573                 ai_biblios     => $biblios,
574                 ai_biblioitems => $biblioitems,
575                 ai_items       => $items,
576                 ai_checkouts   => $checkouts,
577                 ai_holds       => $holds,
578             );
579         }
580     }
581
582     # Circ rule warnings
583     {
584         my $dbh   = C4::Context->dbh;
585         my $units = Koha::CirculationRules->search({ rule_name => 'lengthunit', rule_value => { -not_in => ['days', 'hours'] } });
586
587         if ( $units->count ) {
588             $template->param(
589                 warnIssuingRules => 1,
590                 ir_units         => $units,
591             );
592         }
593     }
594
595     # Guarantor relationships warnings
596     {
597         my $dbh   = C4::Context->dbh;
598         my ($bad_relationships_count) = $dbh->selectall_arrayref(q{
599             SELECT COUNT(*)
600             FROM (
601                 SELECT relationship FROM borrower_relationships WHERE relationship='_bad_data'
602                 UNION ALL
603                 SELECT relationship FROM borrowers WHERE relationship='_bad_data') a
604         });
605
606         $bad_relationships_count = $bad_relationships_count->[0]->[0];
607
608         my $existing_relationships = $dbh->selectall_arrayref(q{
609             SELECT DISTINCT(relationship)
610             FROM (
611                 SELECT relationship FROM borrower_relationships WHERE relationship IS NOT NULL
612                 UNION ALL
613                 SELECT relationship FROM borrowers WHERE relationship IS NOT NULL) a
614         });
615
616         my %valid_relationships = map { $_ => 1 } split( /,|\|/, C4::Context->preference('borrowerRelationship') );
617         $valid_relationships{ _bad_data } = 1; # we handle this case in another way
618
619         my $wrong_relationships = [ grep { !$valid_relationships{ $_->[0] } } @{$existing_relationships} ];
620         if ( @$wrong_relationships or $bad_relationships_count ) {
621
622             $template->param(
623                 warnRelationships => 1,
624             );
625
626             if ( $wrong_relationships ) {
627                 $template->param(
628                     wrong_relationships => $wrong_relationships
629                 );
630             }
631             if ($bad_relationships_count) {
632                 $template->param(
633                     bad_relationships_count => $bad_relationships_count,
634                 );
635             }
636         }
637     }
638
639     {
640         # Test 'bcrypt_settings' config for Pseudonymization
641         $template->param( config_bcrypt_settings_no_set => 1 )
642         if C4::Context->preference('Pseudonymization')
643         and not C4::Context->config('bcrypt_settings');
644     }
645
646     {
647         my @frameworkcodes = Koha::BiblioFrameworks->search->get_column('frameworkcode');
648         my @hidden_biblionumbers;
649         push @frameworkcodes, ""; # it's not in the biblio_frameworks table!
650         my $no_FA_framework = 1;
651         for my $frameworkcode ( @frameworkcodes ) {
652             $no_FA_framework = 0 if $frameworkcode eq 'FA';
653             my $shouldhidemarc_opac = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
654                 {
655                     frameworkcode => $frameworkcode,
656                     interface     => "opac"
657                 }
658             );
659             push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'opac' }
660             if $shouldhidemarc_opac->{biblionumber};
661
662             my $shouldhidemarc_intranet = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
663                 {
664                     frameworkcode => $frameworkcode,
665                     interface     => "intranet"
666                 }
667             );
668             push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'intranet' }
669             if $shouldhidemarc_intranet->{biblionumber};
670         }
671         $template->param( warnHiddenBiblionumbers => \@hidden_biblionumbers );
672         $template->param( warnFastCataloging => $no_FA_framework );
673     }
674
675     {
676         # BackgroundJob - test connection to message broker
677         eval {
678             Koha::BackgroundJob->connect;
679         };
680         if ( $@ ) {
681             warn $@;
682             $template->param( warnConnectBroker => $@ );
683         }
684     }
685
686     #BZ 28267: Warn administrators if there are database rows with a format other than 'DYNAMIC'
687     {
688         $template->param( warnDbRowFormat => C4::Installer->has_non_dynamic_row_format );
689     }
690
691     $template->param(
692         prefRequireChoosingExistingAuthority => $prefRequireChoosingExistingAuthority,
693         prefAutoCreateAuthorities => $prefAutoCreateAuthorities,
694         warnPrefRequireChoosingExistingAuthority => $warnPrefRequireChoosingExistingAuthority,
695         warnPrefEasyAnalyticalRecords  => $warnPrefEasyAnalyticalRecords,
696         warnPrefAnonymousPatronOPACPrivacy        => $warnPrefAnonymousPatronOPACPrivacy,
697         warnPrefAnonymousPatronAnonSuggestions    => $warnPrefAnonymousPatronAnonSuggestions,
698         warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist     => $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist,
699         warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist => $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist,
700         warnPrefKohaAdminEmailAddress => $warnPrefKohaAdminEmailAddress,
701         warnPrefOpacHiddenItems => $warnPrefOpacHiddenItems,
702         errZebraConnection => $errZebraConnection,
703         warnIsRootUser => $warnIsRootUser,
704         warnNoActiveCurrency => $warnNoActiveCurrency,
705         warnNoTemplateCaching => ( C4::Context->config('template_cache_dir') ? 0 : 1 ),
706         xml_config_warnings => \@xml_config_warnings,
707         warnStatisticsFieldsError => $warnStatisticsFieldsError,
708     );
709 }
710
711 if ( $tab eq 'perl' ) {
712
713     my @components = ();
714
715     my $perl_modules = C4::Installer::PerlModules->new;
716     $perl_modules->versions_info;
717
718     my @pm_types = qw(missing_pm upgrade_pm current_pm);
719
720     foreach my $pm_type(@pm_types) {
721         my $modules = $perl_modules->get_attr($pm_type);
722         foreach (@$modules) {
723             my ($module, $stats) = each %$_;
724             push(
725                 @components,
726                 {
727                     name    => $module,
728                     version => $stats->{'cur_ver'},
729                     missing => ($pm_type eq 'missing_pm' ? 1 : 0),
730                     upgrade => ($pm_type eq 'upgrade_pm' ? 1 : 0),
731                     current => ($pm_type eq 'current_pm' ? 1 : 0),
732                     require => $stats->{'required'},
733                     reqversion => $stats->{'min_ver'},
734                     maxversion => $stats->{'max_ver'},
735                     excversion => $stats->{'exc_ver'}
736                 }
737             );
738         }
739     }
740
741     @components = sort {$a->{'name'} cmp $b->{'name'}} @components;
742
743     my $counter=0;
744     my $row = [];
745     my $table = [];
746     foreach (@components) {
747         push (@$row, $_);
748         unless (++$counter % 4) {
749             push (@$table, {row => $row});
750             $row = [];
751         }
752     }
753     # Processing the last line (if there are any modules left)
754     if (scalar(@$row) > 0) {
755         # Extending $row to the table size
756         $$row[3] = '';
757         # Pushing the last line
758         push (@$table, {row => $row});
759     }
760     ## ## $table
761
762     $template->param( table => $table );
763
764 }
765
766 if ( $tab eq 'team' ) {
767
768     ## ------------------------------------------
769     ## Koha contributions
770
771     ## Release teams
772     my $teams =
773     -e "$docdir" . "/teams.yaml"
774     ? YAML::XS::LoadFile( "$docdir" . "/teams.yaml" )
775     : {};
776     my $dev_team = (sort {$b <=> $a} (keys %{$teams->{team}}))[0];
777     my $short_version = substr($versions{'kohaVersion'},0,5);
778     my $minor = substr($versions{'kohaVersion'},3,2);
779     my $development_version = ( $minor eq '05' || $minor eq '11' ) ? 0 : 1;
780     my $codename;
781     $template->param( short_version => $short_version );
782     $template->param( development_version => $development_version );
783
784     ## Contributors
785     my $contributors =
786     -e "$docdir" . "/contributors.yaml"
787     ? YAML::XS::LoadFile( "$docdir" . "/contributors.yaml" )
788     : {};
789     delete $contributors->{_others_};
790     for my $version ( sort { $a <=> $b } keys %{$teams->{team}} ) {
791         for my $role ( keys %{ $teams->{team}->{$version} } ) {
792             my $detail = $teams->{team}->{$version}->{$role};
793             my $normalized_role = "$role";
794             $normalized_role =~ s/s$//;
795             if ( ref( $detail ) eq 'ARRAY' ) {
796                 for my $contributor ( @{ $detail } ) {
797
798                     my $localized_role = $normalized_role;
799                     if ( my $subversion = $contributor->{version} ) {
800                         $localized_role .= ':' . $subversion;
801                     }
802                     if ( my $area = $contributor->{area} ) {
803                         $localized_role .= ':' . $area;
804                     }
805
806                     my $name = $contributor->{name};
807                     # Add role to contributors
808                     push @{ $contributors->{$name}->{roles}->{$localized_role} },
809                     $version;
810                     # Add openhub to teams
811                     if ( exists( $contributors->{$name}->{openhub} ) ) {
812                         $contributor->{openhub} = $contributors->{$name}->{openhub};
813                     }
814                 }
815             }
816             elsif ( $role eq 'release_date' ) {
817                 $teams->{team}->{$version}->{$role} = DateTime->from_epoch( epoch => $teams->{team}->{$version}->{$role} );
818             }
819             elsif ( $role eq 'codename' ) {
820                 if ( $version == $short_version ) {
821                     $codename = $detail;
822                 }
823                 next;
824             }
825             else {
826                 if ( my $subversion = $detail->{version} ) {
827                     $normalized_role .= ':' . $subversion;
828                 }
829                 if ( my $area = $detail->{area} ) {
830                     $normalized_role .= ':' . $area;
831                 }
832
833                 my $name = $detail->{name};
834                 # Add role to contributors
835                 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
836                 $version;
837                 # Add openhub to teams
838                 if ( exists( $contributors->{$name}->{openhub} ) ) {
839                     $detail->{openhub} =
840                     $contributors->{$name}->{openhub};
841                 }
842             }
843         }
844     }
845
846     ## Create last name ordered array of people from contributors
847     my @people = map {
848         { name => $_, ( $contributors->{$_} ? %{ $contributors->{$_} } : () ) }
849     } sort {
850     my ($alast) = $a =~ /(\S+)$/;
851     my ($blast) = $b =~ /(\S+)$/;
852     my $cmp = lc($alast||"") cmp lc($blast||"");
853     return $cmp if $cmp;
854
855     my ($a2last) = $a =~ /(\S+)\s\S+$/;
856     my ($b2last) = $b =~ /(\S+)\s\S+$/;
857     lc($a2last||"") cmp lc($b2last||"");
858     } keys %$contributors;
859
860     $template->param( kohaCodename  => $codename);
861     $template->param( contributors => \@people );
862     $template->param( maintenance_team => $teams->{team}->{$dev_team} );
863     $template->param( release_team => $teams->{team}->{$short_version} );
864
865 }
866 if ( $tab eq 'history' ) {
867
868     ## Timeline
869     if ( open( my $file, "<:encoding(UTF-8)", "$docdir" . "/history.txt" ) ) {
870
871         my $i = 0;
872
873         my @rows2 = ();
874         my $row2  = [];
875
876         my @lines = <$file>;
877         close($file);
878
879         shift @lines; #remove header row
880
881         foreach (@lines) {
882             my ( $epoch, $date, $desc, $tag ) = split(/\t/);
883             if(!$desc && $date=~ /(?<=\d{4})\s+/) {
884                 ($date, $desc)= ($`, $');
885             }
886             push(
887                 @rows2,
888                 {
889                     date => $date,
890                     desc => $desc,
891                 }
892             );
893         }
894
895         my $table2 = [];
896         #foreach my $row2 (@rows2) {
897         foreach  (@rows2) {
898             push (@$row2, $_);
899             push( @$table2, { row2 => $row2 } );
900             $row2 = [];
901         }
902
903         $template->param( table2 => $table2 );
904     } else {
905         $template->param( timeline_read_error => 1 );
906     }
907 }
908
909 output_html_with_http_headers $query, $cookie, $template->output;