3 # Copyright 2000-2002 Katipo Communications
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
24 use Digest::MD5 qw( md5_base64 );
26 use CGI::Session::ErrorHandler;
29 use List::MoreUtils qw( uniq );
32 use C4::Templates; # to get the template
34 use C4::Search::History;
35 use C4::Output qw( output_and_exit );
39 use Koha::AuthUtils qw( get_script_name hash_password );
40 use Koha::Auth::TwoFactorAuth;
42 use Koha::DateUtils qw( dt_from_string );
43 use Koha::Library::Groups;
45 use Koha::Cash::Registers;
48 use Koha::Patron::Consents;
49 use List::MoreUtils qw( any );
51 use C4::Auth_with_shibboleth qw( shib_ok get_login_shib login_shib_url logout_shib checkpw_shib );
53 use C4::Log qw( logaction );
54 use Koha::CookieManager;
55 use Koha::Auth::Permissions;
57 use Koha::Exceptions::Token;
62 use vars qw($ldap $cas $caslogout);
63 our (@ISA, @EXPORT_OK);
65 #NOTE: The utility of keeping the safe_exit function is that it can be easily re-defined in unit tests and plugins
67 # It's fine for us to "exit" because CGI::Compile (used in Plack::App::WrapCGI) redefines "exit" for us automatically.
68 # Since we only seem to use C4::Auth::safe_exit in a CGI context, we don't actually need PSGI detection at all here.
74 C4::Context->set_remote_address;
80 checkauth check_api_auth get_session check_cookie_auth checkpw checkpw_internal checkpw_hash
81 get_all_subpermissions get_cataloguing_page_permissions get_user_subpermissions in_iprange
82 get_template_and_user haspermission create_basic_session
85 $ldap = C4::Context->config('useldapserver') || 0;
86 $cas = C4::Context->preference('casAuthentication');
87 $caslogout = C4::Context->preference('casLogout');
90 require C4::Auth_with_ldap;
91 import C4::Auth_with_ldap qw(checkpw_ldap);
94 require C4::Auth_with_cas; # no import
95 import C4::Auth_with_cas qw(check_api_auth_cas checkpw_cas login_cas logout_cas login_cas_url logout_if_required multipleAuth getMultipleAuth);
102 C4::Auth - Authenticates Koha users
106 use CGI qw ( -utf8 );
110 my $query = CGI->new;
112 my ($template, $borrowernumber, $cookie)
113 = get_template_and_user(
115 template_name => "opac-main.tt",
118 authnotrequired => 0,
119 flagsrequired => { catalogue => '*', tools => 'import_patrons' },
123 output_html_with_http_headers $query, $cookie, $template->output;
127 The main function of this module is to provide
128 authentification. However the get_template_and_user function has
129 been provided so that a users login information is passed along
130 automatically. This gets loaded into the template.
134 =head2 get_template_and_user
136 my ($template, $borrowernumber, $cookie)
137 = get_template_and_user(
139 template_name => "opac-main.tt",
142 authnotrequired => 0,
143 flagsrequired => { catalogue => '*', tools => 'import_patrons' },
147 This call passes the C<query>, C<flagsrequired> and C<authnotrequired>
148 to C<&checkauth> (in this module) to perform authentification.
149 See C<&checkauth> for an explanation of these parameters.
151 The C<template_name> is then used to find the correct template for
152 the page. The authenticated users details are loaded onto the
153 template in the logged_in_user variable (which is a Koha::Patron object). Also the
154 C<sessionID> is passed to the template. This can be used in templates
155 if cookies are disabled. It needs to be put as and input to every
158 More information on the C<gettemplate> sub can be found in the
163 sub get_template_and_user {
166 my ( $user, $cookie, $sessionID, $flags );
169 my $cookie_mgr = Koha::CookieManager->new;
171 # Get shibboleth login attribute
172 my $shib = C4::Context->config('useshibboleth') && shib_ok();
173 my $shib_login = $shib ? get_login_shib() : undef;
175 C4::Context->interface( $in->{type} );
177 $in->{'authnotrequired'} ||= 0;
179 # the following call includes a bad template check; might croak
180 my $template = C4::Templates::gettemplate(
181 $in->{'template_name'},
186 if ( $in->{'template_name'} !~ m/maintenance/ ) {
187 ( $user, $cookie, $sessionID, $flags ) = checkauth(
189 $in->{'authnotrequired'},
190 $in->{'flagsrequired'},
193 $in->{template_name},
194 { skip_csrf_check => 1 },
198 # If we enforce GDPR and the user did not consent, redirect
199 # Exceptions for consent page itself and SCI/SCO system
200 if( $in->{type} eq 'opac' && $user &&
201 $in->{'template_name'} !~ /^(opac-page|opac-patron-consent|sc[io]\/)/ &&
202 C4::Context->preference('PrivacyPolicyConsent') eq 'Enforced' )
204 my $consent = Koha::Patron::Consents->search({
205 borrowernumber => getborrowernumber($user),
206 type => 'GDPR_PROCESSING',
207 given_on => { '!=', undef },
210 print $in->{query}->redirect(-uri => '/cgi-bin/koha/opac-patron-consent.pl', -cookie => $cookie);
215 if ( $in->{type} eq 'opac' && $user ) {
218 my $session = get_session($sessionID);
220 $is_sco_user = $session->param('sco_user');
226 # If the user logged in is the SCO user and they try to go out of the SCO module,
227 # log the user out removing the CGISESSID cookie
228 $in->{template_name} !~ m|sco/| && $in->{template_name} !~ m|errors/errorpage.tt|
232 C4::Context->preference('AutoSelfCheckID')
233 && $user eq C4::Context->preference('AutoSelfCheckID')
241 # If the user logged in is the SCI user and they try to go out of the SCI module,
242 # kick them out unless it is SCO with a valid permission
243 # or they are a superlibrarian
244 $in->{template_name} !~ m|sci/|
245 && haspermission( $user, { self_check => 'self_checkin_module' } )
247 $in->{template_name} =~ m|sco/| && haspermission(
248 $user, { self_check => 'self_checkout_module' }
251 && $flags && $flags->{superlibrarian} != 1
258 $template = C4::Templates::gettemplate( 'opac-auth.tt', 'opac',
260 $cookie = $cookie_mgr->replace_in_list( $cookie, $in->{query}->cookie(
261 -name => 'CGISESSID',
264 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
268 #NOTE: This JWT should only be used by the self-check controllers
269 $cookie = $cookie_mgr->replace_in_list( $cookie, $in->{query}->cookie(
273 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
277 my $auth_error = $in->{query}->param('auth_error');
281 script_name => get_script_name(),
282 auth_error => $auth_error,
285 print $in->{query}->header(
290 'X-Frame-Options' => 'SAMEORIGIN'
302 # It's possible for $user to be the borrowernumber if they don't have a
303 # userid defined (and are logging in through some other method, such
304 # as SSL certs against an email address)
305 $borrowernumber = getborrowernumber($user) if defined($user);
306 if ( !defined($borrowernumber) && defined($user) ) {
307 $patron = Koha::Patrons->find( $user );
309 $borrowernumber = $user;
311 # A bit of a hack, but I don't know there's a nicer way
313 $user = $patron->firstname . ' ' . $patron->surname;
316 $patron = Koha::Patrons->find( $borrowernumber );
317 # FIXME What to do if $patron does not exist?
320 if ( $in->{'type'} eq 'opac' ) {
321 require Koha::Virtualshelves;
322 my $some_private_shelves = Koha::Virtualshelves->get_some_shelves(
324 borrowernumber => $borrowernumber,
328 my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
334 some_private_shelves => $some_private_shelves,
335 some_public_shelves => $some_public_shelves,
339 # We are going to use the $flags returned by checkauth
340 # to create the template's parameters that will indicate
341 # which menus the user can access.
342 my $authz = Koha::Auth::Permissions->get_authz_from_flags({ flags => $flags });
343 foreach my $permission ( keys %{ $authz } ){
344 $template->param( $permission => $authz->{$permission} );
347 # Logged-in opac search history
348 # If the requested template is an opac one and opac search history is enabled
349 if ( $in->{type} eq 'opac' && C4::Context->preference('EnableOpacSearchHistory') ) {
350 my $dbh = C4::Context->dbh;
351 my $query = "SELECT COUNT(*) FROM search_history WHERE userid=?";
352 my $sth = $dbh->prepare($query);
353 $sth->execute($borrowernumber);
355 # If at least one search has already been performed
356 if ( $sth->fetchrow_array > 0 ) {
358 # We show the link in opac
359 $template->param( EnableOpacSearchHistory => 1 );
361 if (C4::Context->preference('LoadSearchHistoryToTheFirstLoggedUser'))
363 # And if there are searches performed when the user was not logged in,
364 # we add them to the logged-in search history
365 my @recentSearches = C4::Search::History::get_from_session( { cgi => $in->{'query'} } );
366 if (@recentSearches) {
368 INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, type, total, time )
369 VALUES (?, ?, ?, ?, ?, ?, ?)
371 my $sth = $dbh->prepare($query);
372 $sth->execute( $borrowernumber,
373 $in->{query}->cookie("CGISESSID"),
376 $_->{type} || 'biblio',
379 ) foreach @recentSearches;
381 # clear out the search history from the session now that
382 # we've saved it to the database
385 C4::Search::History::set_to_session( { cgi => $in->{'query'}, search_history => [] } );
387 } elsif ( $in->{type} eq 'intranet' and C4::Context->preference('EnableSearchHistory') ) {
388 $template->param( EnableSearchHistory => 1 );
391 else { # if this is an anonymous session, setup to display public lists...
393 # If shibboleth is enabled, and we're in an anonymous session, we should allow
394 # the user to attempt login via shibboleth.
396 $template->param( shibbolethAuthentication => $shib,
397 shibbolethLoginUrl => login_shib_url( $in->{'query'} ),
400 # If shibboleth is enabled and we have a shibboleth login attribute,
401 # but we are in an anonymous session, then we clearly have an invalid
402 # shibboleth koha account.
404 $template->param( invalidShibLogin => '1' );
408 if ( $in->{'type'} eq 'opac' ){
409 require Koha::Virtualshelves;
410 my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
416 some_public_shelves => $some_public_shelves,
419 # Set default branch if one has been passed by the environment.
420 $template->param( default_branch => $ENV{OPAC_BRANCH_DEFAULT} ) if $ENV{OPAC_BRANCH_DEFAULT};
424 # Sysprefs disabled via URL param
425 # Note that value must be defined in order to override via ENV
426 foreach my $syspref (
432 OpacAdditionalStylesheet
434 intranetcolorstylesheet
439 $ENV{"OVERRIDE_SYSPREF_$syspref"} = q{}
440 if $in->{'query'}->param("DISABLE_SYSPREF_$syspref");
443 # Anonymous opac search history
444 # If opac search history is enabled and at least one search has already been performed
445 if ( C4::Context->preference('EnableOpacSearchHistory') ) {
446 my @recentSearches = C4::Search::History::get_from_session( { cgi => $in->{'query'} } );
447 if (@recentSearches) {
448 $template->param( EnableOpacSearchHistory => 1 );
452 if ( C4::Context->preference('dateformat') ) {
453 $template->param( dateformat => C4::Context->preference('dateformat') );
456 $template->param(auth_forwarded_hash => scalar $in->{'query'}->param('auth_forwarded_hash'));
458 # these template parameters are set the same regardless of $in->{'type'}
460 my $minPasswordLength = C4::Context->preference('minPasswordLength');
461 $minPasswordLength = 3 if not $minPasswordLength or $minPasswordLength < 3;
463 EnhancedMessagingPreferences => C4::Context->preference('EnhancedMessagingPreferences'),
464 GoogleJackets => C4::Context->preference("GoogleJackets"),
465 OpenLibraryCovers => C4::Context->preference("OpenLibraryCovers"),
466 KohaAdminEmailAddress => "" . C4::Context->preference("KohaAdminEmailAddress"),
467 LoginFirstname => ( C4::Context->userenv ? C4::Context->userenv->{"firstname"} : "Bel" ),
468 LoginSurname => C4::Context->userenv ? C4::Context->userenv->{"surname"} : "Inconnu",
469 emailaddress => C4::Context->userenv ? C4::Context->userenv->{"emailaddress"} : undef,
470 TagsEnabled => C4::Context->preference("TagsEnabled"),
471 hide_marc => C4::Context->preference("hide_marc"),
472 item_level_itypes => C4::Context->preference('item-level_itypes'),
473 patronimages => C4::Context->preference("patronimages"),
474 singleBranchMode => ( Koha::Libraries->search->count == 1 ),
475 noItemTypeImages => C4::Context->preference("noItemTypeImages"),
476 marcflavour => C4::Context->preference("marcflavour"),
477 OPACBaseURL => C4::Context->preference('OPACBaseURL'),
478 minPasswordLength => $minPasswordLength,
480 if ( $in->{'type'} eq "intranet" ) {
483 advancedMARCEditor => C4::Context->preference("advancedMARCEditor"),
484 AllowMultipleCovers => C4::Context->preference('AllowMultipleCovers'),
485 AmazonCoverImages => C4::Context->preference("AmazonCoverImages"),
486 AutoLocation => C4::Context->preference("AutoLocation"),
487 can_see_cataloguing_module => haspermission( $user, get_cataloguing_page_permissions() ) ? 1 : 0,
488 canreservefromotherbranches => C4::Context->preference('canreservefromotherbranches'),
489 EasyAnalyticalRecords => C4::Context->preference('EasyAnalyticalRecords'),
490 EnableBorrowerFiles => C4::Context->preference('EnableBorrowerFiles'),
491 FRBRizeEditions => C4::Context->preference("FRBRizeEditions"),
492 IndependentBranches => C4::Context->preference("IndependentBranches"),
493 intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),
494 IntranetFavicon => C4::Context->preference("IntranetFavicon"),
495 IntranetmainUserblock => C4::Context->preference("IntranetmainUserblock"),
496 IntranetNav => C4::Context->preference("IntranetNav"),
497 intranetreadinghistory => C4::Context->preference("intranetreadinghistory"),
498 IntranetReadingHistoryHolds => C4::Context->preference("IntranetReadingHistoryHolds"),
499 intranetstylesheet => C4::Context->preference("intranetstylesheet"),
500 IntranetUserCSS => C4::Context->preference("IntranetUserCSS"),
501 IntranetUserJS => C4::Context->preference("IntranetUserJS"),
502 LibraryName => C4::Context->preference("LibraryName"),
503 LocalCoverImages => C4::Context->preference('LocalCoverImages'),
504 OPACLocalCoverImages => C4::Context->preference('OPACLocalCoverImages'),
505 PatronAutoComplete => C4::Context->preference("PatronAutoComplete"),
506 pending_checkout_notes => Koha::Checkouts->search( { noteseen => 0 } ),
507 plugins_enabled => C4::Context->config("enable_plugins"),
508 StaffSerialIssueDisplayCount => C4::Context->preference("StaffSerialIssueDisplayCount"),
509 UseCourseReserves => C4::Context->preference("UseCourseReserves"),
510 useDischarge => C4::Context->preference('useDischarge'),
511 virtualshelves => C4::Context->preference("virtualshelves"),
515 warn "template type should be OPAC, here it is=[" . $in->{'type'} . "]" unless ( $in->{'type'} eq 'opac' );
517 #TODO : replace LibraryName syspref with 'system name', and remove this html processing
518 my $LibraryNameTitle = C4::Context->preference("LibraryName");
519 $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
520 $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
522 # clean up the busc param in the session
523 # if the page is not opac-detail and not the "add to list" page
524 # and not the "edit comments" page
525 if ( C4::Context->preference("OpacBrowseResults")
526 && $in->{'template_name'} =~ /opac-(.+)\.(?:tt|tmpl)$/ ) {
528 unless ( $pagename =~ /^(?:MARC|ISBD)?detail$/
529 or $pagename =~ /^showmarc$/
530 or $pagename =~ /^addbybiblionumber$/
531 or $pagename =~ /^review$/ )
533 my $sessionSearch = get_session( $sessionID );
534 $sessionSearch->clear( ["busc"] ) if $sessionSearch;
538 # variables passed from CGI: opac_css_override and opac_search_limits.
539 my $opac_search_limit = $ENV{'OPAC_SEARCH_LIMIT'};
540 my $opac_limit_override = $ENV{'OPAC_LIMIT_OVERRIDE'};
543 ( $opac_limit_override && $opac_search_limit && $opac_search_limit =~ /branch:([\w-]+)/ ) ||
544 ( $in->{'query'}->param('limit') && $in->{'query'}->param('limit') =~ /branch:([\w-]+)/ ) ||
545 ( $in->{'query'}->param('limit') && $in->{'query'}->param('limit') =~ /multibranchlimit:(\w+)/ )
547 $opac_name = $1; # opac_search_limit is a branch, so we use it.
548 } elsif ( $in->{'query'}->param('multibranchlimit') ) {
549 $opac_name = $in->{'query'}->param('multibranchlimit');
550 } elsif ( C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv && C4::Context->userenv->{'branch'} ) {
551 $opac_name = C4::Context->userenv->{'branch'};
554 # Decide if the patron can make suggestions in the OPAC
555 my $can_make_suggestions;
556 if ( C4::Context->preference('Suggestion') && C4::Context->preference('AnonSuggestions') ) {
557 $can_make_suggestions = 1;
558 } elsif ( C4::Context->userenv && C4::Context->userenv->{'number'} ) {
559 $can_make_suggestions = Koha::Patrons->find(C4::Context->userenv->{'number'})->category->can_make_suggestions;
562 my @search_groups = Koha::Library::Groups->get_search_groups({ interface => 'opac' })->as_list;
564 AnonSuggestions => "" . C4::Context->preference("AnonSuggestions"),
565 LibrarySearchGroups => \@search_groups,
566 opac_name => $opac_name,
567 LibraryName => "" . C4::Context->preference("LibraryName"),
568 LibraryNameTitle => "" . $LibraryNameTitle,
569 OPACAmazonCoverImages => C4::Context->preference("OPACAmazonCoverImages"),
570 OPACFRBRizeEditions => C4::Context->preference("OPACFRBRizeEditions"),
571 OpacHighlightedWords => C4::Context->preference("OpacHighlightedWords"),
572 OPACShelfBrowser => "" . C4::Context->preference("OPACShelfBrowser"),
573 OPACURLOpenInNewWindow => "" . C4::Context->preference("OPACURLOpenInNewWindow"),
574 OpacAuthorities => C4::Context->preference("OpacAuthorities"),
575 opac_css_override => $ENV{'OPAC_CSS_OVERRIDE'},
576 opac_search_limit => $opac_search_limit,
577 opac_limit_override => $opac_limit_override,
578 OpacBrowser => C4::Context->preference("OpacBrowser"),
579 OpacCloud => C4::Context->preference("OpacCloud"),
580 OpacKohaUrl => C4::Context->preference("OpacKohaUrl"),
581 OpacPasswordChange => C4::Context->preference("OpacPasswordChange"),
582 OPACPatronDetails => C4::Context->preference("OPACPatronDetails"),
583 OPACPrivacy => C4::Context->preference("OPACPrivacy"),
584 OPACFinesTab => C4::Context->preference("OPACFinesTab"),
585 OpacTopissue => C4::Context->preference("OpacTopissue"),
586 'Version' => C4::Context->preference('Version'),
587 hidelostitems => C4::Context->preference("hidelostitems"),
588 mylibraryfirst => ( C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv ) ? C4::Context->userenv->{'branch'} : '',
589 opacbookbag => "" . C4::Context->preference("opacbookbag"),
590 OpacFavicon => C4::Context->preference("OpacFavicon"),
591 opaclanguagesdisplay => "" . C4::Context->preference("opaclanguagesdisplay"),
592 opacreadinghistory => C4::Context->preference("opacreadinghistory"),
593 opacuserlogin => "" . C4::Context->preference("opacuserlogin"),
594 OpenLibrarySearch => C4::Context->preference("OpenLibrarySearch"),
595 ShowReviewer => C4::Context->preference("ShowReviewer"),
596 ShowReviewerPhoto => C4::Context->preference("ShowReviewerPhoto"),
597 suggestion => $can_make_suggestions,
598 virtualshelves => "" . C4::Context->preference("virtualshelves"),
599 OPACSerialIssueDisplayCount => C4::Context->preference("OPACSerialIssueDisplayCount"),
600 SyndeticsClientCode => C4::Context->preference("SyndeticsClientCode"),
601 SyndeticsEnabled => C4::Context->preference("SyndeticsEnabled"),
602 SyndeticsCoverImages => C4::Context->preference("SyndeticsCoverImages"),
603 SyndeticsTOC => C4::Context->preference("SyndeticsTOC"),
604 SyndeticsSummary => C4::Context->preference("SyndeticsSummary"),
605 SyndeticsEditions => C4::Context->preference("SyndeticsEditions"),
606 SyndeticsExcerpt => C4::Context->preference("SyndeticsExcerpt"),
607 SyndeticsReviews => C4::Context->preference("SyndeticsReviews"),
608 SyndeticsAuthorNotes => C4::Context->preference("SyndeticsAuthorNotes"),
609 SyndeticsAwards => C4::Context->preference("SyndeticsAwards"),
610 SyndeticsSeries => C4::Context->preference("SyndeticsSeries"),
611 SyndeticsCoverImageSize => C4::Context->preference("SyndeticsCoverImageSize"),
612 OPACLocalCoverImages => C4::Context->preference("OPACLocalCoverImages"),
613 PatronSelfRegistration => C4::Context->preference("PatronSelfRegistration"),
614 PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
615 useDischarge => C4::Context->preference('useDischarge'),
618 $template->param( OpacPublic => '1' ) if ( $user || C4::Context->preference("OpacPublic") );
621 # Check if we were asked using parameters to force a specific language
622 if ( defined $in->{'query'}->param('language') ) {
624 # Extract the language, let C4::Languages::getlanguage choose
626 my $language = C4::Languages::getlanguage( $in->{'query'} );
627 my $languagecookie = C4::Templates::getlanguagecookie( $in->{'query'}, $language );
628 $cookie = $cookie_mgr->replace_in_list( $cookie, $languagecookie );
632 $template->param( loggedinusername => $user ); # OBSOLETE - Do not reuse this in template, use logged_in_user.userid instead
633 $template->param( loggedinusernumber => $borrowernumber ); # FIXME Should be replaced with logged_in_user.borrowernumber
634 $template->param( logged_in_user => $patron );
635 $template->param( sessionID => $sessionID );
637 my $op = $in->{query}->param('op');
638 if ( defined $op && $op =~ m{^cud-} ) {
639 C4::Output::output_and_exit( $in->{query}, $cookie, $template, 'wrong_csrf_token' )
640 unless Koha::Token->new->check_csrf(
642 session_id => scalar $in->{query}->cookie('CGISESSID'),
643 token => scalar $in->{query}->param('csrf_token'),
648 return ( $template, $borrowernumber, $cookie, $flags );
653 ($userid, $cookie, $sessionID) = &checkauth($query, $noauth, $flagsrequired, $type);
655 Verifies that the user is authorized to run this script. If
656 the user is authorized, a (userid, cookie, session-id, flags)
657 quadruple is returned. If the user is not authorized but does
658 not have the required privilege (see $flagsrequired below), it
659 displays an error page and exits. Otherwise, it displays the
660 login page and exits.
662 Note that C<&checkauth> will return if and only if the user
663 is authorized, so it should be called early on, before any
664 unfinished operations (e.g., if you've opened a file, then
665 C<&checkauth> won't close it for you).
667 C<$query> is the CGI object for the script calling C<&checkauth>.
669 The C<$noauth> argument is optional. If it is set, then no
670 authorization is required for the script.
672 C<&checkauth> fetches user and session information from C<$query> and
673 ensures that the user is authorized to run scripts that require
676 The C<$flagsrequired> argument specifies the required privileges
677 the user must have if the username and password are correct.
678 It should be specified as a reference-to-hash; keys in the hash
679 should be the "flags" for the user, as specified in the Members
680 intranet module. Any key specified must correspond to a "flag"
681 in the userflags table. E.g., { circulate => 1 } would specify
682 that the user must have the "circulate" privilege in order to
683 proceed. To make sure that access control is correct, the
684 C<$flagsrequired> parameter must be specified correctly.
686 Koha also has a concept of sub-permissions, also known as
687 granular permissions. This makes the value of each key
688 in the C<flagsrequired> hash take on an additional
693 The user must have access to all subfunctions of the module
694 specified by the hash key.
698 The user must have access to at least one subfunction of the module
699 specified by the hash key.
701 specific permission, e.g., 'export_catalog'
703 The user must have access to the specific subfunction list, which
704 must correspond to a row in the permissions table.
706 The C<$type> argument specifies whether the template should be
707 retrieved from the opac or intranet directory tree. "opac" is
708 assumed if it is not specified; however, if C<$type> is specified,
709 "intranet" is assumed if it is not "opac".
711 If C<$query> does not have a valid session ID associated with it
712 (i.e., the user has not logged in) or if the session has expired,
713 C<&checkauth> presents the user with a login page (from the point of
714 view of the original script, C<&checkauth> does not return). Once the
715 user has authenticated, C<&checkauth> restarts the original script
716 (this time, C<&checkauth> returns).
718 The login page is provided using a HTML::Template, which is set in the
719 systempreferences table or at the top of this file. The variable C<$type>
720 selects which template to use, either the opac or the intranet
721 authentification template.
723 C<&checkauth> returns a user ID, a cookie, and a session ID. The
724 cookie should be sent back to the browser; it verifies that the user
734 # If version syspref is unavailable, it means Koha is being installed,
735 # and so we must redirect to OPAC maintenance page or to the WebInstaller
736 # also, if OpacMaintenance is ON, OPAC should redirect to maintenance
737 if ( C4::Context->preference('OpacMaintenance') && $type eq 'opac' ) {
738 warn "OPAC Install required, redirecting to maintenance";
739 print $query->redirect("/cgi-bin/koha/maintenance.pl");
742 unless ( $version = C4::Context->preference('Version') ) { # assignment, not comparison
743 if ( $type ne 'opac' ) {
744 warn "Install required, redirecting to Installer";
745 print $query->redirect("/cgi-bin/koha/installer/install.pl");
747 warn "OPAC Install required, redirecting to maintenance";
748 print $query->redirect("/cgi-bin/koha/maintenance.pl");
753 # check that database and koha version are the same
754 # there is no DB version, it's a fresh install,
755 # go to web installer
756 # there is a DB version, compare it to the code version
757 my $kohaversion = Koha::version();
759 # remove the 3 last . to have a Perl number
760 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
761 Koha::Logger->get->debug("kohaversion : $kohaversion");
762 if ( $version < $kohaversion ) {
763 my $warning = "Database update needed, redirecting to %s. Database is $version and Koha is $kohaversion";
764 if ( $type ne 'opac' ) {
765 warn sprintf( $warning, 'Installer' );
766 print $query->redirect("/cgi-bin/koha/installer/install.pl?step=1&op=updatestructure");
768 warn sprintf( "OPAC: " . $warning, 'maintenance' );
769 print $query->redirect("/cgi-bin/koha/maintenance.pl");
775 sub _timeout_syspref {
776 my $default_timeout = 600;
777 my $timeout = C4::Context->preference('timeout') || $default_timeout;
779 # value in days, convert in seconds
780 if ( $timeout =~ /^(\d+)[dD]$/ ) {
781 $timeout = $1 * 86400;
783 # value in hours, convert in seconds
784 elsif ( $timeout =~ /^(\d+)[hH]$/ ) {
785 $timeout = $1 * 3600;
787 elsif ( $timeout !~ m/^\d+$/ ) {
788 warn "The value of the system preference 'timeout' is not correct, defaulting to $default_timeout";
789 $timeout = $default_timeout;
798 # Get shibboleth login attribute
799 my $shib = C4::Context->config('useshibboleth') && shib_ok();
800 my $shib_login = $shib ? get_login_shib() : undef;
802 # $authnotrequired will be set for scripts which will run without authentication
803 my $authnotrequired = shift;
804 my $flagsrequired = shift;
806 my $emailaddress = shift;
807 my $template_name = shift;
808 my $params = shift || {}; # do_not_print, skip_csrf_check
810 my $skip_csrf_check = $params->{skip_csrf_check} || 0;
811 $type = 'opac' unless $type;
813 if ( $type eq 'opac' && !C4::Context->preference("OpacPublic") ) {
814 my @allowed_scripts_for_private_opac = qw(
816 opac-registration-email-sent.tt
817 opac-registration-confirmation.tt
818 opac-memberentry-update-submitted.tt
819 opac-password-recovery.tt
820 opac-reset-password.tt
823 $authnotrequired = 0 unless grep { $_ eq $template_name }
824 @allowed_scripts_for_private_opac;
827 my $timeout = _timeout_syspref();
829 my $cookie_mgr = Koha::CookieManager->new;
831 _version_check( $type, $query );
834 my $auth_state = 'failed';
836 my ( $userid, $cookie, $sessionID, $flags );
838 my $logout = $query->param('logout.x');
840 my $anon_search_history;
842 # This parameter is the name of the CAS server we want to authenticate against,
843 # when using authentication against multiple CAS servers, as configured in Auth_cas_servers.yaml
844 my $casparam = $query->param('cas');
845 my $q_userid = $query->param('userid') // '';
848 my $invalid_otp_token;
850 ( $type ne "opac" # Only available for the staff interface
851 && C4::Context->preference('TwoFactorAuthentication') ne "disabled" ) # If "enabled" or "enforced"
854 # Basic authentication is incompatible with the use of Shibboleth,
855 # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
856 # and it may not be the attribute we want to use to match the koha login.
858 # Also, do not consider an empty REMOTE_USER.
860 # Finally, after those tests, we can assume (although if it would be better with
861 # a syspref) that if we get a REMOTE_USER, that's from basic authentication,
862 # and we can affect it to $userid.
863 if ( !$shib and defined( $ENV{'REMOTE_USER'} ) and $ENV{'REMOTE_USER'} ne '' and $userid = $ENV{'REMOTE_USER'} ) {
865 # Using Basic Authentication, no cookies required
866 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
867 -name => 'CGISESSID',
870 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
874 elsif ( $emailaddress) {
875 # the Google OpenID Connect passes an email address
877 elsif ( $sessionID = $query->cookie("CGISESSID") ) { # assignment, not comparison
878 my ( $return, $more_info );
879 # NOTE: $flags in the following call is still undefined !
880 ( $return, $session, $more_info ) = check_cookie_auth( $sessionID, $flags,
881 { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
884 if ( $return eq 'ok' || $return eq 'additional-auth-needed' ) {
885 $userid = $session->param('id');
889 $return eq 'ok' ? 'completed'
890 : $return eq 'additional-auth-needed' ? 'additional-auth-needed'
893 # We are at the second screen if the waiting-for-2FA is set in session
894 # and otp_token param has been passed
896 && $auth_state eq 'additional-auth-needed'
897 && ( my $otp_token = $query->param('otp_token') ) )
899 my $patron = Koha::Patrons->find( { userid => $userid } );
900 my $auth = Koha::Auth::TwoFactorAuth->new( { patron => $patron } );
901 my $verified = $auth->verify($otp_token);
904 # The token is correct, the user is fully logged in!
905 $auth_state = 'completed';
906 $session->param( 'waiting-for-2FA', 0 );
907 $session->param( 'waiting-for-2FA-setup', 0 );
909 # This is an ugly trick to pass the test
910 # $query->param('koha_login_context') && ( $q_userid ne $userid )
915 $invalid_otp_token = 1;
919 if ( $auth_state eq 'completed' ) {
920 Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
922 if ( ( $query->param('koha_login_context') && ( $q_userid ne $userid ) )
923 || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
924 || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
927 #if a user enters an id ne to the id in the current session, we need to log them in...
928 #first we need to clear the anonymous session...
929 $anon_search_history = $session->param('search_history');
932 $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
933 C4::Context::_unset_userenv($sessionID);
935 undef $userid; # IMPORTANT: this assures us a new session in code below
936 $auth_state = 'failed';
939 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
940 -name => 'CGISESSID',
941 -value => $session->id,
943 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
947 $flags = haspermission( $userid, $flagsrequired );
949 $auth_state = 'failed';
950 $info{'nopermission'} = 1;
953 } elsif ( !$logout ) {
954 if ( $return eq 'expired' ) {
955 $info{timed_out} = 1;
956 } elsif ( $return eq 'restricted' ) {
957 $info{oldip} = $more_info->{old_ip};
958 $info{newip} = $more_info->{new_ip};
959 $info{different_ip} = 1;
960 } elsif ( $return eq 'password_expired' ) {
961 $info{password_has_expired} = 1;
966 my $request_method = $query->request_method // q{};
968 if ( $auth_state eq 'failed' || $logout ) {
975 # voluntary logout the user
976 # check wether the user was using their shibboleth session or a local one
977 my $shibSuccess = C4::Context->userenv ? C4::Context->userenv->{'shibboleth'} : undef;
982 C4::Context::_unset_userenv($sessionID);
983 $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
985 if ($cas and $caslogout) {
986 logout_cas($query, $type);
989 # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
990 if ( $shib and $shib_login and $shibSuccess) {
995 $auth_state = 'logout';
999 #we initiate a session prior to checking for a username to allow for anonymous sessions...
1000 if( !$session or !$sessionID ) { # if we cleared sessionID, we need a new session
1001 $session = get_session() or die "Auth ERROR: Cannot get_session()";
1004 # Save anonymous search history in new session so it can be retrieved
1005 # by get_template_and_user to store it in user's search history after
1006 # a successful login.
1007 if ($anon_search_history) {
1008 $session->param( 'search_history', $anon_search_history );
1011 $sessionID = $session->id;
1012 C4::Context->_new_userenv($sessionID);
1013 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
1014 -name => 'CGISESSID',
1015 -value => $sessionID,
1017 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1020 my $pki_field = C4::Context->preference('AllowPKIAuth');
1021 if ( !defined($pki_field) ) {
1022 print STDERR "ERROR: Missing system preference AllowPKIAuth.\n";
1023 $pki_field = 'None';
1025 if ( ( $cas && $query->param('ticket') )
1027 || ( $shib && $shib_login )
1028 || $pki_field ne 'None'
1031 my $password = $query->param('password');
1032 my $shibSuccess = 0;
1033 my ( $return, $cardnumber );
1035 # If shib is enabled and we have a shib login, does the login match a valid koha user
1036 if ( $shib && $shib_login ) {
1039 # Do not pass password here, else shib will not be checked in checkpw.
1040 ( $return, $cardnumber, $retuserid ) = checkpw( $q_userid, undef, $query );
1041 $userid = $retuserid;
1042 $shibSuccess = $return;
1043 $info{'invalidShibLogin'} = 1 unless ($return);
1046 # If shib login and match were successful, skip further login methods
1047 unless ($shibSuccess) {
1048 if ( $cas && $query->param('ticket') ) {
1051 ( $return, $cardnumber, $retuserid, $patron, $cas_ticket ) =
1052 checkpw( $userid, $password, $query, $type );
1053 $userid = $retuserid;
1054 $info{'invalidCasLogin'} = 1 unless ($return);
1057 elsif ( $emailaddress ) {
1058 my $value = $emailaddress;
1060 # If we're looking up the email, there's a chance that the person
1061 # doesn't have a userid. So if there is none, we pass along the
1062 # borrower number, and the bits of code that need to know the user
1063 # ID will have to be smart enough to handle that.
1064 my $patrons = Koha::Patrons->search({ email => $value });
1065 if ($patrons->count) {
1067 # First the userid, then the borrowernum
1068 my $patron = $patrons->next;
1069 $value = $patron->userid || $patron->borrowernumber;
1073 $return = $value ? 1 : 0;
1078 ( $pki_field eq 'Common Name' && $ENV{'SSL_CLIENT_S_DN_CN'} )
1079 || ( $pki_field eq 'emailAddress'
1080 && $ENV{'SSL_CLIENT_S_DN_Email'} )
1084 if ( $pki_field eq 'Common Name' ) {
1085 $value = $ENV{'SSL_CLIENT_S_DN_CN'};
1087 elsif ( $pki_field eq 'emailAddress' ) {
1088 $value = $ENV{'SSL_CLIENT_S_DN_Email'};
1090 # If we're looking up the email, there's a chance that the person
1091 # doesn't have a userid. So if there is none, we pass along the
1092 # borrower number, and the bits of code that need to know the user
1093 # ID will have to be smart enough to handle that.
1094 my $patrons = Koha::Patrons->search({ email => $value });
1095 if ($patrons->count) {
1097 # First the userid, then the borrowernum
1098 my $patron = $patrons->next;
1099 $value = $patron->userid || $patron->borrowernumber;
1105 $return = $value ? 1 : 0;
1113 $request_method eq 'POST'
1114 || ( C4::Context->preference('AutoSelfCheckID')
1115 && $q_userid eq C4::Context->preference('AutoSelfCheckID') )
1120 ( $return, $cardnumber, $retuserid, $patron, $cas_ticket ) =
1121 checkpw( $q_userid, $password, $query, $type );
1122 $userid = $retuserid if ($retuserid);
1123 $info{'invalid_username_or_password'} = 1 unless ($return);
1128 # If shib configured and shibOnly enabled, we should ignore anything other than a shibboleth type login.
1135 && C4::Context->preference('OPACShibOnly')
1137 || ( ( $type ne 'opac' )
1138 && C4::Context->preference('staffShibOnly') )
1145 # $return: 1 = valid user
1146 if( $return && $return > 0 ) {
1148 if ( $flags = haspermission( $userid, $flagsrequired ) ) {
1149 $auth_state = "logged_in";
1152 $auth_state = 'failed';
1153 # FIXME We could add $return = 0; or even delete the session?
1154 # Currently return == 1 and we will fill session info later on,
1155 # although we do present an authorization failure. (Yes, the
1156 # authentication was actually correct.)
1157 $info{'nopermission'} = 1;
1158 C4::Context::_unset_userenv($sessionID);
1160 my ( $borrowernumber, $firstname, $surname, $userflags,
1161 $branchcode, $branchname, $emailaddress, $desk_id,
1162 $desk_name, $register_id, $register_name );
1164 if ( $return == 1 ) {
1166 SELECT borrowernumber, firstname, surname, flags, borrowers.branchcode,
1167 branches.branchname as branchname, email
1169 LEFT JOIN branches on borrowers.branchcode=branches.branchcode
1171 my $dbh = C4::Context->dbh;
1172 my $sth = $dbh->prepare("$select where userid=?");
1173 $sth->execute($userid);
1174 unless ( $sth->rows ) {
1175 $sth = $dbh->prepare("$select where cardnumber=?");
1176 $sth->execute($cardnumber);
1178 unless ( $sth->rows ) {
1179 $sth->execute($userid);
1183 ( $borrowernumber, $firstname, $surname, $userflags,
1184 $branchcode, $branchname, $emailaddress ) = $sth->fetchrow;
1187 # launch a sequence to check if we have a ip for the branch, i
1188 # if we have one we replace the branchcode of the userenv by the branch bound in the ip.
1190 my $ip = $ENV{'REMOTE_ADDR'};
1192 # if they specify at login, use that
1193 my $patron = Koha::Patrons->find({userid => $userid});
1194 if ( $query->param('branch') && ( haspermission($userid, { 'loggedinlibrary'=> 1 }) || $patron->is_superlibrarian ) ) {
1195 $branchcode = $query->param('branch');
1196 my $library = Koha::Libraries->find($branchcode);
1197 $branchname = $library? $library->branchname: '';
1199 if ( $query->param('desk_id') ) {
1200 $desk_id = $query->param('desk_id');
1201 my $desk = Koha::Desks->find($desk_id);
1202 $desk_name = $desk ? $desk->desk_name : '';
1204 if ( C4::Context->preference('UseCashRegisters') ) {
1206 $query->param('register_id')
1207 ? Koha::Cash::Registers->find($query->param('register_id'))
1208 : Koha::Cash::Registers->search(
1209 { branch => $branchcode, branch_default => 1 },
1210 { rows => 1 } )->single;
1211 $register_id = $register->id if ($register);
1212 $register_name = $register->name if ($register);
1214 my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search->as_list };
1215 if ( $type ne 'opac' and C4::Context->preference('AutoLocation') ) {
1217 # we have to check they are coming from the right ip range
1218 my $domain = $branches->{$branchcode}->{'branchip'};
1219 $domain =~ s|\.\*||g;
1220 if ( $ip !~ /^$domain/ ) {
1221 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
1222 -name => 'CGISESSID',
1225 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1228 $info{'wrongip'} = 1;
1229 $auth_state = "failed";
1233 if ( C4::Context->preference('AutoLocation') && $auth_state ne 'failed' ) {
1234 foreach my $br ( uniq( $branchcode, keys %$branches ) ) {
1236 # now we work with the treatment of ip
1237 my $domain = $branches->{$br}->{'branchip'};
1238 if ( $domain && $ip =~ /^$domain/ ) {
1239 $branchcode = $branches->{$br}->{'branchcode'};
1241 # new op dev : add the branchname to the cookie
1242 $branchname = $branches->{$br}->{'branchname'};
1248 my $is_sco_user = 0;
1249 if ( $query->param('sco_user_login') && ( $query->param('sco_user_login') eq '1' ) ){
1253 $session->param( 'number', $borrowernumber );
1254 $session->param( 'id', $userid );
1255 $session->param( 'cardnumber', $cardnumber );
1256 $session->param( 'firstname', $firstname );
1257 $session->param( 'surname', $surname );
1258 $session->param( 'branch', $branchcode );
1259 $session->param( 'branchname', $branchname );
1260 $session->param( 'desk_id', $desk_id);
1261 $session->param( 'desk_name', $desk_name);
1262 $session->param( 'flags', $userflags );
1263 $session->param( 'emailaddress', $emailaddress );
1264 $session->param( 'ip', $session->remote_addr() );
1265 $session->param( 'lasttime', time() );
1266 $session->param( 'interface', $type);
1267 $session->param( 'shibboleth', $shibSuccess );
1268 $session->param( 'register_id', $register_id );
1269 $session->param( 'register_name', $register_name );
1270 $session->param( 'sco_user', $is_sco_user );
1272 $session->param('cas_ticket', $cas_ticket) if $cas_ticket;
1273 C4::Context->set_userenv(
1274 $session->param('number'), $session->param('id'),
1275 $session->param('cardnumber'), $session->param('firstname'),
1276 $session->param('surname'), $session->param('branch'),
1277 $session->param('branchname'), $session->param('flags'),
1278 $session->param('emailaddress'), $session->param('shibboleth'),
1279 $session->param('desk_id'), $session->param('desk_name'),
1280 $session->param('register_id'), $session->param('register_name')
1284 # $return: 0 = invalid user
1285 # reset to anonymous session
1288 $info{'invalid_username_or_password'} = 1;
1289 C4::Context::_unset_userenv($sessionID);
1291 $session->param( 'lasttime', time() );
1292 $session->param( 'ip', $session->remote_addr() );
1293 $session->param( 'sessiontype', 'anon' );
1294 $session->param( 'interface', $type);
1296 } # END if ( $q_userid
1297 elsif ( $type eq "opac" ) {
1299 # anonymous sessions are created only for the OPAC
1301 # setting a couple of other session vars...
1302 $session->param( 'ip', $session->remote_addr() );
1303 $session->param( 'lasttime', time() );
1304 $session->param( 'sessiontype', 'anon' );
1305 $session->param( 'interface', $type);
1308 } # END unless ($userid)
1311 if ( $auth_state eq 'logged_in' ) {
1312 $auth_state = 'completed';
1314 # Auth is completed unless an additional auth is needed
1315 if ( $require_2FA ) {
1316 my $patron = Koha::Patrons->find({userid => $userid});
1317 if ( C4::Context->preference('TwoFactorAuthentication') eq "enforced" && $patron->auth_method eq 'password' ) {
1318 $auth_state = 'setup-additional-auth-needed';
1319 $session->param('waiting-for-2FA-setup', 1);
1320 %info = ();# We remove the warnings/errors we may have set incorrectly before
1321 } elsif ( $patron->auth_method eq 'two-factor' ) {
1322 # Ask for the OTP token
1323 $auth_state = 'additional-auth-needed';
1324 $session->param('waiting-for-2FA', 1);
1325 %info = ();# We remove the warnings/errors we may have set incorrectly before
1330 # finished authentification, now respond
1331 if ( $auth_state eq 'completed' || $authnotrequired ) {
1334 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
1335 -name => 'CGISESSID',
1338 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1343 my $patron = $userid ? Koha::Patrons->find({ userid => $userid }) : undef;
1344 $patron->update_lastseen('login') if $patron;
1346 my $op = $query->param('op');
1347 if ( defined $op && $op =~ m{^cud-} ) {
1348 die "Cannot use GET for this request"
1349 if $request_method eq 'GET';
1353 || Koha::Token->new->check_csrf(
1355 session_id => scalar $query->cookie('CGISESSID'),
1356 token => scalar $query->param('csrf_token'),
1361 Koha::Exceptions::Token::WrongCSRFToken->throw;
1365 # In case, that this request was a login attempt, we want to prevent that users can repost the opac login
1366 # request. We therefore redirect the user to the requested page again without the login parameters.
1367 # See Post/Redirect/Get (PRG) design pattern: https://en.wikipedia.org/wiki/Post/Redirect/Get
1368 if ( $type eq "opac" && $query->param('koha_login_context') && $query->param('koha_login_context') ne 'sco' && $query->param('password') && $query->param('userid') ) {
1369 my $uri = URI->new($query->url(-relative=>1, -query_string=>1));
1370 $uri->query_param_delete('userid');
1371 $uri->query_param_delete('password');
1372 $uri->query_param_delete('koha_login_context');
1373 unless ( $params->{do_not_print} ) {
1374 print $query->redirect( -uri => $uri->as_string, -cookie => $cookie, -status => '303 See other' );
1379 return ( $userid, $cookie, $sessionID, $flags );
1384 # AUTH rejected, show the login/password template, after checking the DB.
1388 my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1390 # get the inputs from the incoming query
1392 my @inputs_to_clean = qw( userid password ticket logout.x otp_token );
1393 foreach my $name ( param $query) {
1394 next if grep { $name eq $_ } @inputs_to_clean;
1395 my @value = $query->multi_param($name);
1396 push @inputs, { name => $name, value => $_ } for @value;
1399 my $LibraryNameTitle = C4::Context->preference("LibraryName");
1400 $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1401 $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
1403 my $auth_error = $query->param('auth_error');
1404 my $auth_template_name = ( $type eq 'opac' ) ? 'opac-auth.tt' : 'auth.tt';
1405 my $template = C4::Templates::gettemplate( $auth_template_name, $type, $query );
1409 script_name => get_script_name(),
1410 casAuthentication => C4::Context->preference("casAuthentication"),
1411 shibbolethAuthentication => $shib,
1412 suggestion => C4::Context->preference("suggestion"),
1413 virtualshelves => C4::Context->preference("virtualshelves"),
1414 LibraryName => "" . C4::Context->preference("LibraryName"),
1415 LibraryNameTitle => "" . $LibraryNameTitle,
1416 opacuserlogin => C4::Context->preference("opacuserlogin"),
1417 OpacFavicon => C4::Context->preference("OpacFavicon"),
1418 opacreadinghistory => C4::Context->preference("opacreadinghistory"),
1419 opaclanguagesdisplay => C4::Context->preference("opaclanguagesdisplay"),
1420 opacbookbag => "" . C4::Context->preference("opacbookbag"),
1421 OpacCloud => C4::Context->preference("OpacCloud"),
1422 OpacTopissue => C4::Context->preference("OpacTopissue"),
1423 OpacAuthorities => C4::Context->preference("OpacAuthorities"),
1424 OpacBrowser => C4::Context->preference("OpacBrowser"),
1425 TagsEnabled => C4::Context->preference("TagsEnabled"),
1426 intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),
1427 intranetstylesheet => C4::Context->preference("intranetstylesheet"),
1428 IntranetNav => C4::Context->preference("IntranetNav"),
1429 IntranetFavicon => C4::Context->preference("IntranetFavicon"),
1430 IntranetUserCSS => C4::Context->preference("IntranetUserCSS"),
1431 IntranetUserJS => C4::Context->preference("IntranetUserJS"),
1432 IndependentBranches => C4::Context->preference("IndependentBranches"),
1433 AutoLocation => C4::Context->preference("AutoLocation"),
1434 wrongip => $info{'wrongip'},
1435 PatronSelfRegistration => C4::Context->preference("PatronSelfRegistration"),
1436 PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
1437 opac_css_override => $ENV{'OPAC_CSS_OVERRIDE'},
1438 too_many_login_attempts => ( $patron and $patron->account_locked ),
1439 password_has_expired => ( $patron and $patron->password_expired ),
1440 auth_error => $auth_error,
1443 $template->param( SCO_login => 1 ) if ( $query->param('sco_user_login') );
1444 $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1445 $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1446 $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1447 if ( $auth_state eq 'additional-auth-needed' ) {
1448 my $patron = Koha::Patrons->find( { userid => $userid } );
1451 invalid_otp_token => $invalid_otp_token,
1452 notice_email_address => $patron->notice_email_address, # We could also pass logged_in_user if necessary
1456 if ( $auth_state eq 'setup-additional-auth-needed' ) {
1462 if ( $type eq 'opac' ) {
1463 require Koha::Virtualshelves;
1464 my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
1470 some_public_shelves => $some_public_shelves,
1476 # Is authentication against multiple CAS servers enabled?
1477 require C4::Auth_with_cas;
1478 if ( multipleAuth() && !$casparam ) {
1479 my $casservers = getMultipleAuth();
1481 foreach my $key ( keys %$casservers ) {
1482 push @tmplservers, { name => $key, value => login_cas_url( $query, $key, $type ) . "?cas=$key" };
1485 casServersLoop => \@tmplservers
1489 casServerUrl => login_cas_url($query, undef, $type),
1494 invalidCasLogin => $info{'invalidCasLogin'}
1499 #If shibOnly is enabled just go ahead and redirect directly
1500 if ( (($type eq 'opac') && C4::Context->preference('OPACShibOnly')) || (($type ne 'opac') && C4::Context->preference('staffShibOnly')) ) {
1501 my $redirect_url = login_shib_url( $query );
1502 print $query->redirect( -uri => "$redirect_url", -status => 303 );
1507 shibbolethAuthentication => $shib,
1508 shibbolethLoginUrl => login_shib_url($query),
1512 if (C4::Context->preference('GoogleOpenIDConnect')) {
1513 if ($query->param("OpenIDConnectFailed")) {
1514 my $reason = $query->param('OpenIDConnectFailed');
1515 $template->param(invalidGoogleOpenIDConnectLogin => $reason);
1520 LibraryName => C4::Context->preference("LibraryName"),
1522 sessionID => $session->id,
1525 if ( $params->{do_not_print} ) {
1526 # This must be used for testing purpose only!
1527 return ( undef, undef, undef, undef, $template );
1530 print $query->header(
1531 { type => 'text/html',
1534 'X-Frame-Options' => 'SAMEORIGIN',
1542 =head2 check_api_auth
1544 ($status, $cookie, $sessionId) = check_api_auth($query, $userflags);
1546 Given a CGI query containing the parameters 'userid' and 'password' and/or a session
1547 cookie, determine if the user has the privileges specified by C<$userflags>.
1549 C<check_api_auth> is is meant for authenticating users of web services, and
1550 consequently will always return and will not attempt to redirect the user
1553 If a valid session cookie is already present, check_api_auth will return a status
1554 of "ok", the cookie, and the Koha session ID.
1556 If no session cookie is present, check_api_auth will check the 'userid' and 'password
1557 parameters and create a session cookie and Koha session if the supplied credentials
1560 Possible return values in C<$status> are:
1564 =item "ok" -- user authenticated; C<$cookie> and C<$sessionid> have valid values.
1566 =item "failed" -- credentials are not correct; C<$cookie> and C<$sessionid> are undef
1568 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1570 =item "expired -- session cookie has expired; API user should resubmit userid and password
1572 =item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1574 =item "additional-auth-needed -- User is in an authentication process that is not finished
1580 sub check_api_auth {
1583 my $flagsrequired = shift;
1584 my $timeout = _timeout_syspref();
1586 unless ( C4::Context->preference('Version') ) {
1588 # database has not been installed yet
1589 return ( "maintenance", undef, undef );
1591 my $kohaversion = Koha::version();
1592 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1593 if ( C4::Context->preference('Version') < $kohaversion ) {
1595 # database in need of version update; assume that
1596 # no API should be called while databsae is in
1598 return ( "maintenance", undef, undef );
1601 my ( $sessionID, $session );
1602 unless ( $query->param('userid') ) {
1603 $sessionID = $query->cookie("CGISESSID");
1605 if ( $sessionID && not( $cas && $query->param('PT') ) ) {
1608 ( $return, $session, undef ) = check_cookie_auth(
1609 $sessionID, $flagsrequired, { remote_addr => $ENV{REMOTE_ADDR} } );
1611 return ( $return, undef, undef ) # Cookie auth failed
1614 my $cookie = $query->cookie(
1615 -name => 'CGISESSID',
1616 -value => $session->id,
1618 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1621 return ( $return, $cookie, $session ); # return == 'ok' here
1626 my $userid = $query->param('userid');
1627 my $password = $query->param('password');
1628 my ( $return, $cardnumber, $cas_ticket );
1631 if ( $cas && $query->param('PT') ) {
1634 # In case of a CAS authentication, we use the ticket instead of the password
1635 my $PT = $query->param('PT');
1636 ( $return, $cardnumber, $userid, $cas_ticket ) = check_api_auth_cas( $PT, $query ); # EXTERNAL AUTH
1639 # User / password auth
1640 unless ( $userid and $password ) {
1642 # caller did something wrong, fail the authenticateion
1643 return ( "failed", undef, undef );
1647 ( $return, $cardnumber, $newuserid, $patron, $cas_ticket ) = checkpw( $userid, $password, $query );
1650 if ( $return and haspermission( $userid, $flagsrequired ) ) {
1651 my $session = get_session("");
1652 return ( "failed", undef, undef ) unless $session;
1654 my $sessionID = $session->id;
1655 C4::Context->_new_userenv($sessionID);
1656 my $cookie = $query->cookie(
1657 -name => 'CGISESSID',
1658 -value => $sessionID,
1660 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1663 if ( $return == 1 ) {
1665 $borrowernumber, $firstname, $surname,
1666 $userflags, $branchcode, $branchname,
1669 my $dbh = C4::Context->dbh;
1672 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where userid=?"
1674 $sth->execute($userid);
1676 $borrowernumber, $firstname, $surname,
1677 $userflags, $branchcode, $branchname,
1679 ) = $sth->fetchrow if ( $sth->rows );
1681 unless ( $sth->rows ) {
1682 my $sth = $dbh->prepare(
1683 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
1685 $sth->execute($cardnumber);
1687 $borrowernumber, $firstname, $surname,
1688 $userflags, $branchcode, $branchname,
1690 ) = $sth->fetchrow if ( $sth->rows );
1692 unless ( $sth->rows ) {
1693 $sth->execute($userid);
1695 $borrowernumber, $firstname, $surname, $userflags,
1696 $branchcode, $branchname, $emailaddress
1697 ) = $sth->fetchrow if ( $sth->rows );
1701 my $ip = $ENV{'REMOTE_ADDR'};
1703 # if they specify at login, use that
1704 if ( $query->param('branch') ) {
1705 $branchcode = $query->param('branch');
1706 my $library = Koha::Libraries->find($branchcode);
1707 $branchname = $library? $library->branchname: '';
1709 my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search->as_list };
1710 foreach my $br ( keys %$branches ) {
1712 # now we work with the treatment of ip
1713 my $domain = $branches->{$br}->{'branchip'};
1714 if ( $domain && $ip =~ /^$domain/ ) {
1715 $branchcode = $branches->{$br}->{'branchcode'};
1717 # new op dev : add the branchname to the cookie
1718 $branchname = $branches->{$br}->{'branchname'};
1721 $session->param( 'number', $borrowernumber );
1722 $session->param( 'id', $userid );
1723 $session->param( 'cardnumber', $cardnumber );
1724 $session->param( 'firstname', $firstname );
1725 $session->param( 'surname', $surname );
1726 $session->param( 'branch', $branchcode );
1727 $session->param( 'branchname', $branchname );
1728 $session->param( 'flags', $userflags );
1729 $session->param( 'emailaddress', $emailaddress );
1730 $session->param( 'ip', $session->remote_addr() );
1731 $session->param( 'lasttime', time() );
1732 $session->param( 'interface', 'api' );
1734 $session->param( 'cas_ticket', $cas_ticket);
1735 C4::Context->set_userenv(
1736 $session->param('number'), $session->param('id'),
1737 $session->param('cardnumber'), $session->param('firstname'),
1738 $session->param('surname'), $session->param('branch'),
1739 $session->param('branchname'), $session->param('flags'),
1740 $session->param('emailaddress'), $session->param('shibboleth'),
1741 $session->param('desk_id'), $session->param('desk_name'),
1742 $session->param('register_id'), $session->param('register_name')
1744 return ( "ok", $cookie, $sessionID );
1746 return ( "failed", undef, undef );
1751 =head2 check_cookie_auth
1753 ($status, $sessionId) = check_cookie_auth($cookie, $userflags);
1755 Given a CGISESSID cookie set during a previous login to Koha, determine
1756 if the user has the privileges specified by C<$userflags>. C<$userflags>
1757 is passed unaltered into C<haspermission> and as such accepts all options
1758 avaiable to that routine with the one caveat that C<check_api_auth> will
1759 also allow 'undef' to be passed and in such a case the permissions check
1760 will be skipped altogether.
1762 C<check_cookie_auth> is meant for authenticating special services
1763 such as tools/upload-file.pl that are invoked by other pages that
1764 have been authenticated in the usual way.
1766 Possible return values in C<$status> are:
1770 =item "ok" -- user authenticated; C<$sessionID> have valid values.
1772 =item "anon" -- user not authenticated but valid for anonymous session.
1774 =item "failed" -- credentials are not correct; C<$sessionid> are undef
1776 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1778 =item "expired -- session cookie has expired; API user should resubmit userid and password
1780 =item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1786 sub check_cookie_auth {
1787 my $sessionID = shift;
1788 my $flagsrequired = shift;
1791 my $remote_addr = $params->{remote_addr} || $ENV{REMOTE_ADDR};
1793 my $skip_version_check = $params->{skip_version_check}; # Only for checkauth
1795 unless ( $skip_version_check ) {
1796 unless ( C4::Context->preference('Version') ) {
1798 # database has not been installed yet
1799 return ( "maintenance", undef );
1801 my $kohaversion = Koha::version();
1802 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1803 if ( C4::Context->preference('Version') < $kohaversion ) {
1805 # database in need of version update; assume that
1806 # no API should be called while databsae is in
1808 return ( "maintenance", undef );
1812 # see if we have a valid session cookie already
1813 # however, if a userid parameter is present (i.e., from
1814 # a form submission, assume that any current cookie
1816 unless ( $sessionID ) {
1817 return ( "failed", undef );
1819 C4::Context::_unset_userenv($sessionID); # remove old userenv first
1820 my $session = get_session($sessionID);
1822 my $userid = $session->param('id');
1823 my $ip = $session->param('ip');
1824 my $lasttime = $session->param('lasttime');
1825 my $timeout = _timeout_syspref();
1827 if ( !$lasttime || ( $lasttime < time() - $timeout ) ) {
1831 return ("expired", undef);
1833 } elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $remote_addr ) {
1834 # IP address changed
1837 return ( "restricted", undef, { old_ip => $ip, new_ip => $remote_addr});
1839 } elsif ( $userid ) {
1840 $session->param( 'lasttime', time() );
1841 my $patron = Koha::Patrons->find({ userid => $userid });
1843 # If the user modify their own userid
1844 # Better than 500 but we could do better
1845 unless ( $patron ) {
1848 return ("expired", undef);
1851 $patron = Koha::Patrons->find({ cardnumber => $userid })
1853 return ("password_expired", undef ) if $patron->password_expired;
1854 my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1;
1856 C4::Context->_new_userenv($sessionID);
1857 if ( !C4::Context->interface ) {
1858 # No need to override the interface, most often set by get_template_and_user
1859 C4::Context->interface( $session->param('interface') );
1861 C4::Context->set_userenv(
1862 $session->param('number'), $session->param('id') // '',
1863 $session->param('cardnumber'), $session->param('firstname'),
1864 $session->param('surname'), $session->param('branch'),
1865 $session->param('branchname'), $session->param('flags'),
1866 $session->param('emailaddress'), $session->param('shibboleth'),
1867 $session->param('desk_id'), $session->param('desk_name'),
1868 $session->param('register_id'), $session->param('register_name')
1870 if ( C4::Context->preference('TwoFactorAuthentication') ne 'disabled' ) {
1871 return ( "additional-auth-needed", $session )
1872 if $session->param('waiting-for-2FA');
1874 return ( "setup-additional-auth-needed", $session )
1875 if $session->param('waiting-for-2FA-setup');
1878 return ( "ok", $session );
1882 return ( "failed", undef );
1886 C4::Context->_new_userenv($sessionID);
1887 C4::Context->interface($session->param('interface'));
1888 C4::Context->set_userenv( undef, q{} );
1889 return ( "anon", $session );
1892 return ( "expired", undef );
1899 my $session = get_session($sessionID);
1901 Given a session ID, retrieve the CGI::Session object used to store
1902 the session's state. The session object can be used to store
1903 data that needs to be accessed by different scripts during a
1906 If the C<$sessionID> parameter is an empty string, a new session
1911 #NOTE: We're keeping this for backwards compatibility
1912 sub _get_session_params {
1913 return Koha::Session->_get_session_params();
1916 #NOTE: We're keeping this for backwards compatibility
1918 my $sessionID = shift;
1919 my $session = Koha::Session->get_session( { sessionID => $sessionID } );
1923 =head2 create_basic_session
1925 my $session = create_basic_session({ patron => $patron, interface => $interface });
1927 Creates a session and adds all basic parameters for a session to work
1931 sub create_basic_session {
1933 my $patron = $params->{patron};
1934 my $interface = $params->{interface};
1936 $interface = 'intranet' if $interface eq 'staff';
1938 my $session = get_session("");
1940 $session->param( 'number', $patron->borrowernumber );
1941 $session->param( 'id', $patron->userid );
1942 $session->param( 'cardnumber', $patron->cardnumber );
1943 $session->param( 'firstname', $patron->firstname );
1944 $session->param( 'surname', $patron->surname );
1945 $session->param( 'branch', $patron->branchcode );
1946 $session->param( 'branchname', $patron->library->branchname );
1947 $session->param( 'flags', $patron->flags );
1948 $session->param( 'emailaddress', $patron->email );
1949 $session->param( 'ip', $session->remote_addr() );
1950 $session->param( 'lasttime', time() );
1951 $session->param( 'interface', $interface);
1957 # FIXME no_set_userenv may be replaced with force_branchcode_for_userenv
1958 # (or something similar)
1959 # Currently it's only passed from C4::SIP::ILS::Patron::check_password, but
1960 # not having a userenv defined could cause a crash.
1962 my ( $userid, $password, $query, $type, $no_set_userenv ) = @_;
1963 $type = 'opac' unless $type;
1965 # Get shibboleth login attribute
1966 my $shib = C4::Context->config('useshibboleth') && shib_ok();
1967 my $shib_login = $shib ? get_login_shib() : undef;
1971 if ( defined $userid ) {
1972 $patron = Koha::Patrons->find( { userid => $userid } );
1973 $patron = Koha::Patrons->find( { cardnumber => $userid } ) unless $patron;
1975 my $check_internal_as_fallback = 0;
1978 # Note: checkpw_* routines returns:
1981 # -1 if user bind failed (LDAP only)
1983 if ( $patron and ( $patron->account_locked ) ) {
1985 # Nothing to check, account is locked
1986 } elsif ( $ldap && defined($password) ) {
1987 my ( $retval, $retcard, $retuserid );
1988 ( $retval, $retcard, $retuserid, $patron ) = checkpw_ldap(@_); # EXTERNAL AUTH
1989 if ( $retval == 1 ) {
1990 @return = ( $retval, $retcard, $retuserid, $patron );
1993 $check_internal_as_fallback = 1 if $retval == 0;
1995 } elsif ( $cas && $query && $query->param('ticket') ) {
1997 # In case of a CAS authentication, we use the ticket instead of the password
1998 my $ticket = $query->param('ticket');
1999 $query->delete('ticket'); # remove ticket to come back to original URL
2000 my ( $retval, $retcard, $retuserid, $cas_ticket, $patron ) =
2001 checkpw_cas( $ticket, $query, $type ); # EXTERNAL AUTH
2003 @return = ( $retval, $retcard, $retuserid, $patron, $cas_ticket );
2007 $passwd_ok = $retval;
2010 # If we are in a shibboleth session (shibboleth is enabled, and a shibboleth match attribute is present)
2011 # Check for password to asertain whether we want to be testing against shibboleth or another method this
2013 elsif ( $shib && $shib_login && !$password ) {
2015 # In case of a Shibboleth authentication, we expect a shibboleth user attribute
2016 # (defined under shibboleth mapping in koha-conf.xml) to contain the login of the
2017 # shibboleth-authenticated user
2019 # Then, we check if it matches a valid koha user
2021 my ( $retval, $retcard, $retuserid, $patron ) =
2022 C4::Auth_with_shibboleth::checkpw_shib($shib_login); # EXTERNAL AUTH
2024 @return = ( $retval, $retcard, $retuserid, $patron );
2026 $passwd_ok = $retval;
2029 $check_internal_as_fallback = 1;
2033 if ($check_internal_as_fallback) {
2034 @return = checkpw_internal( $userid, $password, $no_set_userenv );
2035 push( @return, $patron );
2036 $passwd_ok = 1 if $return[0] > 0; # 1 or 2
2041 $patron->update( { login_attempts => 0 } );
2042 if ( $patron->password_expired ) {
2043 @return = ( -2, $patron );
2045 } elsif ( !$patron->account_locked ) {
2046 $patron->update( { login_attempts => $patron->login_attempts + 1 } );
2050 # Optionally log success or failure
2051 if ( $patron && $passwd_ok && C4::Context->preference('AuthSuccessLog') ) {
2052 logaction( 'AUTH', 'SUCCESS', $patron->id, "Valid password for $userid", $type );
2053 } elsif ( !$passwd_ok && C4::Context->preference('AuthFailureLog') ) {
2054 logaction( 'AUTH', 'FAILURE', $patron ? $patron->id : 0, "Wrong password for $userid", $type );
2060 sub checkpw_internal {
2061 my ( $userid, $password, $no_set_userenv ) = @_;
2063 $password = Encode::encode( 'UTF-8', $password )
2064 if Encode::is_utf8($password);
2066 my $dbh = C4::Context->dbh;
2069 "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where userid=?"
2071 $sth->execute($userid);
2073 my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
2074 $surname, $branchcode, $branchname, $flags )
2077 if ( checkpw_hash( $password, $stored_hash ) ) {
2079 C4::Context->set_userenv( "$borrowernumber", $userid, $cardnumber,
2080 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
2081 return 1, $cardnumber, $userid;
2086 "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
2088 $sth->execute($userid);
2090 my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
2091 $surname, $branchcode, $branchname, $flags )
2094 if ( checkpw_hash( $password, $stored_hash ) ) {
2096 C4::Context->set_userenv( $borrowernumber, $userid, $cardnumber,
2097 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
2098 return 1, $cardnumber, $userid;
2105 my ( $password, $stored_hash ) = @_;
2107 return if $stored_hash eq '!';
2109 # check what encryption algorithm was implemented: Bcrypt - if the hash starts with '$2' it is Bcrypt else md5
2111 if ( substr( $stored_hash, 0, 2 ) eq '$2' ) {
2112 $hash = hash_password( $password, $stored_hash );
2114 $hash = md5_base64($password);
2116 return $hash eq $stored_hash;
2121 my $authflags = getuserflags($flags, $userid, [$dbh]);
2123 Translates integer flags into permissions strings hash.
2125 C<$flags> is the integer userflags value ( borrowers.userflags )
2126 C<$userid> is the members.userid, used for building subpermissions
2127 C<$authflags> is a hashref of permissions
2134 my $dbh = @_ ? shift : C4::Context->dbh;
2137 # I don't want to do this, but if someone logs in as the database
2138 # user, it would be preferable not to spam them to death with
2139 # numeric warnings. So, we make $flags numeric.
2140 no warnings 'numeric';
2143 my $sth = $dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
2146 while ( my ( $bit, $flag, $defaulton ) = $sth->fetchrow ) {
2147 if ( ( $flags & ( 2**$bit ) ) || $defaulton ) {
2148 $userflags->{$flag} = 1;
2151 $userflags->{$flag} = 0;
2155 # get subpermissions and merge with top-level permissions
2156 my $user_subperms = get_user_subpermissions($userid);
2157 foreach my $module ( keys %$user_subperms ) {
2158 next if $userflags->{$module} == 1; # user already has permission for everything in this module
2159 $userflags->{$module} = $user_subperms->{$module};
2165 =head2 get_user_subpermissions
2167 $user_perm_hashref = get_user_subpermissions($userid);
2169 Given the userid (note, not the borrowernumber) of a staff user,
2170 return a hashref of hashrefs of the specific subpermissions
2171 accorded to the user. An example return is
2175 export_catalog => 1,
2176 import_patrons => 1,
2180 The top-level hash-key is a module or function code from
2181 userflags.flag, while the second-level key is a code
2184 The results of this function do not give a complete picture
2185 of the functions that a staff user can access; it is also
2186 necessary to check borrowers.flags.
2190 sub get_user_subpermissions {
2193 my $dbh = C4::Context->dbh;
2194 my $sth = $dbh->prepare( "SELECT flag, user_permissions.code
2195 FROM user_permissions
2196 JOIN permissions USING (module_bit, code)
2197 JOIN userflags ON (module_bit = bit)
2198 JOIN borrowers USING (borrowernumber)
2199 WHERE userid = ?" );
2200 $sth->execute($userid);
2202 my $user_perms = {};
2203 while ( my $perm = $sth->fetchrow_hashref ) {
2204 $user_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2209 =head2 get_all_subpermissions
2211 my $perm_hashref = get_all_subpermissions();
2213 Returns a hashref of hashrefs defining all specific
2214 permissions currently defined. The return value
2215 has the same structure as that of C<get_user_subpermissions>,
2216 except that the innermost hash value is the description
2217 of the subpermission.
2221 sub get_all_subpermissions {
2222 my $dbh = C4::Context->dbh;
2223 my $sth = $dbh->prepare( "SELECT flag, code
2225 JOIN userflags ON (module_bit = bit)" );
2229 while ( my $perm = $sth->fetchrow_hashref ) {
2230 $all_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2235 =head2 get_cataloguing_page_permissions
2237 my $required_permissions = get_cataloguing_page_permissions();
2239 Returns the required permissions to access the main cataloguing page. Useful for building
2240 the global I<can_see_cataloguing_module> template variable, and also for reusing in
2241 I<cataloging-home.pl>.
2245 sub get_cataloguing_page_permissions {
2247 my @cataloguing_tools_subperms = qw(
2254 marc_modification_templates
2262 { editcatalogue => '*' }, { tools => \@cataloguing_tools_subperms },
2263 C4::Context->preference('StockRotation') ? { stockrotation => 'manage_rotas' } : ()
2267 =head2 haspermission
2269 $flagsrequired = '*'; # Any permission at all
2270 $flagsrequired = 'a_flag'; # a_flag must be satisfied (all subpermissions)
2271 $flagsrequired = [ 'a_flag', 'b_flag' ]; # a_flag OR b_flag must be satisfied
2272 $flagsrequired = { 'a_flag => 1, 'b_flag' => 1 }; # a_flag AND b_flag must be satisfied
2273 $flagsrequired = { 'a_flag' => 'sub_a' }; # sub_a of a_flag must be satisfied
2274 $flagsrequired = { 'a_flag' => [ 'sub_a, 'sub_b' ] }; # sub_a OR sub_b of a_flag must be satisfied
2276 $flags = ($userid, $flagsrequired);
2278 C<$userid> the userid of the member
2279 C<$flags> is a query structure similar to that used by SQL::Abstract that
2280 denotes the combination of flags required. It is a required parameter.
2282 The main logic of this method is that things in arrays are OR'ed, and things
2283 in hashes are AND'ed. The `*` character can be used, at any depth, to denote `ANY`
2285 Returns member's flags or 0 if a permission is not met.
2290 my ($required, $flags) = @_;
2292 my $ref = ref($required);
2294 if ($required eq '*') {
2295 return 0 unless ( $flags or ref( $flags ) );
2297 return 0 unless ( $flags and (!ref( $flags ) || $flags->{$required} ));
2299 } elsif ($ref eq 'HASH') {
2300 foreach my $key (keys %{$required}) {
2301 next if $flags == 1;
2302 my $require = $required->{$key};
2303 my $rflags = $flags->{$key};
2304 return 0 unless _dispatch($require, $rflags);
2306 } elsif ($ref eq 'ARRAY') {
2308 foreach my $require ( @{$required} ) {
2310 ( ref($flags) && !ref($require) && ( $require ne '*' ) )
2311 ? $flags->{$require}
2313 $satisfied++ if _dispatch( $require, $rflags );
2315 return 0 unless $satisfied;
2317 croak "Unexpected structure found: $ref";
2324 my ( $userid, $flagsrequired ) = @_;
2326 #Koha::Exceptions::WrongParameter->throw('$flagsrequired should not be undef')
2327 # unless defined($flagsrequired);
2329 my $sth = C4::Context->dbh->prepare("SELECT flags FROM borrowers WHERE userid=?");
2330 $sth->execute($userid);
2331 my $row = $sth->fetchrow();
2332 my $flags = getuserflags( $row, $userid );
2334 return $flags unless defined($flagsrequired);
2335 return $flags if $flags->{superlibrarian};
2336 return _dispatch($flagsrequired, $flags);
2338 #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
2343 $flags = ($iprange);
2345 C<$iprange> A space separated string describing an IP range. Can include single IPs or ranges
2347 Returns 1 if the remote address is in the provided iprange, or 0 otherwise.
2354 my @allowedipranges = $iprange ? split(' ', $iprange) : ();
2355 if (scalar @allowedipranges > 0) {
2357 eval { @rangelist = Net::CIDR::range2cidr(@allowedipranges); }; return 0 if $@;
2358 eval { $result = Net::CIDR::cidrlookup($ENV{'REMOTE_ADDR'}, @rangelist) } || Koha::Logger->get->warn('cidrlookup failed for ' . join(' ',@rangelist) );
2360 return $result ? 1 : 0;
2363 sub getborrowernumber {
2365 my $userenv = C4::Context->userenv;
2366 if ( defined($userenv) && ref($userenv) eq 'HASH' && $userenv->{number} ) {
2367 return $userenv->{number};
2369 my $dbh = C4::Context->dbh;
2370 for my $field ( 'userid', 'cardnumber' ) {
2372 $dbh->prepare("select borrowernumber from borrowers where $field=?");
2373 $sth->execute($userid);
2375 my ($bnumber) = $sth->fetchrow;
2382 END { } # module clean-up code here (global destructor)
2392 Crypt::Eksblowfish::Bcrypt(3)