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;
38 use Koha::AuthUtils qw( get_script_name hash_password );
39 use Koha::Auth::TwoFactorAuth;
41 use Koha::DateUtils qw( dt_from_string );
42 use Koha::Library::Groups;
44 use Koha::Cash::Registers;
47 use Koha::Patron::Consents;
48 use List::MoreUtils qw( any );
50 use C4::Auth_with_shibboleth qw( shib_ok get_login_shib login_shib_url logout_shib checkpw_shib );
52 use C4::Log qw( logaction );
53 use Koha::CookieManager;
54 use Koha::Auth::Permissions;
60 use vars qw($ldap $cas $caslogout);
61 our (@ISA, @EXPORT_OK);
63 #NOTE: The utility of keeping the safe_exit function is that it can be easily re-defined in unit tests and plugins
65 # It's fine for us to "exit" because CGI::Compile (used in Plack::App::WrapCGI) redefines "exit" for us automatically.
66 # Since we only seem to use C4::Auth::safe_exit in a CGI context, we don't actually need PSGI detection at all here.
72 C4::Context->set_remote_address;
78 checkauth check_api_auth get_session check_cookie_auth checkpw checkpw_internal checkpw_hash
79 get_all_subpermissions get_cataloguing_page_permissions get_user_subpermissions track_login_daily in_iprange
80 get_template_and_user haspermission create_basic_session
83 $ldap = C4::Context->config('useldapserver') || 0;
84 $cas = C4::Context->preference('casAuthentication');
85 $caslogout = C4::Context->preference('casLogout');
88 require C4::Auth_with_ldap;
89 import C4::Auth_with_ldap qw(checkpw_ldap);
92 require C4::Auth_with_cas; # no import
93 import C4::Auth_with_cas qw(check_api_auth_cas checkpw_cas login_cas logout_cas login_cas_url logout_if_required multipleAuth getMultipleAuth);
100 C4::Auth - Authenticates Koha users
104 use CGI qw ( -utf8 );
108 my $query = CGI->new;
110 my ($template, $borrowernumber, $cookie)
111 = get_template_and_user(
113 template_name => "opac-main.tt",
116 authnotrequired => 0,
117 flagsrequired => { catalogue => '*', tools => 'import_patrons' },
121 output_html_with_http_headers $query, $cookie, $template->output;
125 The main function of this module is to provide
126 authentification. However the get_template_and_user function has
127 been provided so that a users login information is passed along
128 automatically. This gets loaded into the template.
132 =head2 get_template_and_user
134 my ($template, $borrowernumber, $cookie)
135 = get_template_and_user(
137 template_name => "opac-main.tt",
140 authnotrequired => 0,
141 flagsrequired => { catalogue => '*', tools => 'import_patrons' },
145 This call passes the C<query>, C<flagsrequired> and C<authnotrequired>
146 to C<&checkauth> (in this module) to perform authentification.
147 See C<&checkauth> for an explanation of these parameters.
149 The C<template_name> is then used to find the correct template for
150 the page. The authenticated users details are loaded onto the
151 template in the logged_in_user variable (which is a Koha::Patron object). Also the
152 C<sessionID> is passed to the template. This can be used in templates
153 if cookies are disabled. It needs to be put as and input to every
156 More information on the C<gettemplate> sub can be found in the
161 sub get_template_and_user {
164 my ( $user, $cookie, $sessionID, $flags );
167 my $cookie_mgr = Koha::CookieManager->new;
169 # Get shibboleth login attribute
170 my $shib = C4::Context->config('useshibboleth') && shib_ok();
171 my $shib_login = $shib ? get_login_shib() : undef;
173 C4::Context->interface( $in->{type} );
175 $in->{'authnotrequired'} ||= 0;
177 # the following call includes a bad template check; might croak
178 my $template = C4::Templates::gettemplate(
179 $in->{'template_name'},
184 if ( $in->{'template_name'} !~ m/maintenance/ ) {
185 ( $user, $cookie, $sessionID, $flags ) = checkauth(
187 $in->{'authnotrequired'},
188 $in->{'flagsrequired'},
191 $in->{template_name},
195 # If we enforce GDPR and the user did not consent, redirect
196 # Exceptions for consent page itself and SCI/SCO system
197 if( $in->{type} eq 'opac' && $user &&
198 $in->{'template_name'} !~ /^(opac-page|opac-patron-consent|sc[io]\/)/ &&
199 C4::Context->preference('PrivacyPolicyConsent') eq 'Enforced' )
201 my $consent = Koha::Patron::Consents->search({
202 borrowernumber => getborrowernumber($user),
203 type => 'GDPR_PROCESSING',
204 given_on => { '!=', undef },
207 print $in->{query}->redirect(-uri => '/cgi-bin/koha/opac-patron-consent.pl', -cookie => $cookie);
212 if ( $in->{type} eq 'opac' && $user ) {
215 my $session = get_session($sessionID);
217 $is_sco_user = $session->param('sco_user');
223 # If the user logged in is the SCO user and they try to go out of the SCO module,
224 # log the user out removing the CGISESSID cookie
225 $in->{template_name} !~ m|sco/| && $in->{template_name} !~ m|errors/errorpage.tt|
229 C4::Context->preference('AutoSelfCheckID')
230 && $user eq C4::Context->preference('AutoSelfCheckID')
238 # If the user logged in is the SCI user and they try to go out of the SCI module,
239 # kick them out unless it is SCO with a valid permission
240 # or they are a superlibrarian
241 $in->{template_name} !~ m|sci/|
242 && haspermission( $user, { self_check => 'self_checkin_module' } )
244 $in->{template_name} =~ m|sco/| && haspermission(
245 $user, { self_check => 'self_checkout_module' }
248 && $flags && $flags->{superlibrarian} != 1
255 $template = C4::Templates::gettemplate( 'opac-auth.tt', 'opac',
257 $cookie = $cookie_mgr->replace_in_list( $cookie, $in->{query}->cookie(
258 -name => 'CGISESSID',
261 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
265 #NOTE: This JWT should only be used by the self-check controllers
266 $cookie = $cookie_mgr->replace_in_list( $cookie, $in->{query}->cookie(
270 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
274 my $auth_error = $in->{query}->param('auth_error');
278 script_name => get_script_name(),
279 auth_error => $auth_error,
282 print $in->{query}->header(
287 'X-Frame-Options' => 'SAMEORIGIN'
299 # It's possible for $user to be the borrowernumber if they don't have a
300 # userid defined (and are logging in through some other method, such
301 # as SSL certs against an email address)
302 $borrowernumber = getborrowernumber($user) if defined($user);
303 if ( !defined($borrowernumber) && defined($user) ) {
304 $patron = Koha::Patrons->find( $user );
306 $borrowernumber = $user;
308 # A bit of a hack, but I don't know there's a nicer way
310 $user = $patron->firstname . ' ' . $patron->surname;
313 $patron = Koha::Patrons->find( $borrowernumber );
314 # FIXME What to do if $patron does not exist?
317 if ( $in->{'type'} eq 'opac' ) {
318 require Koha::Virtualshelves;
319 my $some_private_shelves = Koha::Virtualshelves->get_some_shelves(
321 borrowernumber => $borrowernumber,
325 my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
331 some_private_shelves => $some_private_shelves,
332 some_public_shelves => $some_public_shelves,
336 # We are going to use the $flags returned by checkauth
337 # to create the template's parameters that will indicate
338 # which menus the user can access.
339 my $authz = Koha::Auth::Permissions->get_authz_from_flags({ flags => $flags });
340 foreach my $permission ( keys %{ $authz } ){
341 $template->param( $permission => $authz->{$permission} );
344 # Logged-in opac search history
345 # If the requested template is an opac one and opac search history is enabled
346 if ( $in->{type} eq 'opac' && C4::Context->preference('EnableOpacSearchHistory') ) {
347 my $dbh = C4::Context->dbh;
348 my $query = "SELECT COUNT(*) FROM search_history WHERE userid=?";
349 my $sth = $dbh->prepare($query);
350 $sth->execute($borrowernumber);
352 # If at least one search has already been performed
353 if ( $sth->fetchrow_array > 0 ) {
355 # We show the link in opac
356 $template->param( EnableOpacSearchHistory => 1 );
358 if (C4::Context->preference('LoadSearchHistoryToTheFirstLoggedUser'))
360 # And if there are searches performed when the user was not logged in,
361 # we add them to the logged-in search history
362 my @recentSearches = C4::Search::History::get_from_session( { cgi => $in->{'query'} } );
363 if (@recentSearches) {
365 INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, type, total, time )
366 VALUES (?, ?, ?, ?, ?, ?, ?)
368 my $sth = $dbh->prepare($query);
369 $sth->execute( $borrowernumber,
370 $in->{query}->cookie("CGISESSID"),
373 $_->{type} || 'biblio',
376 ) foreach @recentSearches;
378 # clear out the search history from the session now that
379 # we've saved it to the database
382 C4::Search::History::set_to_session( { cgi => $in->{'query'}, search_history => [] } );
384 } elsif ( $in->{type} eq 'intranet' and C4::Context->preference('EnableSearchHistory') ) {
385 $template->param( EnableSearchHistory => 1 );
388 else { # if this is an anonymous session, setup to display public lists...
390 # If shibboleth is enabled, and we're in an anonymous session, we should allow
391 # the user to attempt login via shibboleth.
393 $template->param( shibbolethAuthentication => $shib,
394 shibbolethLoginUrl => login_shib_url( $in->{'query'} ),
397 # If shibboleth is enabled and we have a shibboleth login attribute,
398 # but we are in an anonymous session, then we clearly have an invalid
399 # shibboleth koha account.
401 $template->param( invalidShibLogin => '1' );
405 if ( $in->{'type'} eq 'opac' ){
406 require Koha::Virtualshelves;
407 my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
413 some_public_shelves => $some_public_shelves,
416 # Set default branch if one has been passed by the environment.
417 $template->param( default_branch => $ENV{OPAC_BRANCH_DEFAULT} ) if $ENV{OPAC_BRANCH_DEFAULT};
421 # Sysprefs disabled via URL param
422 # Note that value must be defined in order to override via ENV
423 foreach my $syspref (
429 OpacAdditionalStylesheet
431 intranetcolorstylesheet
436 $ENV{"OVERRIDE_SYSPREF_$syspref"} = q{}
437 if $in->{'query'}->param("DISABLE_SYSPREF_$syspref");
440 # Anonymous opac search history
441 # If opac search history is enabled and at least one search has already been performed
442 if ( C4::Context->preference('EnableOpacSearchHistory') ) {
443 my @recentSearches = C4::Search::History::get_from_session( { cgi => $in->{'query'} } );
444 if (@recentSearches) {
445 $template->param( EnableOpacSearchHistory => 1 );
449 if ( C4::Context->preference('dateformat') ) {
450 $template->param( dateformat => C4::Context->preference('dateformat') );
453 $template->param(auth_forwarded_hash => scalar $in->{'query'}->param('auth_forwarded_hash'));
455 # these template parameters are set the same regardless of $in->{'type'}
457 my $minPasswordLength = C4::Context->preference('minPasswordLength');
458 $minPasswordLength = 3 if not $minPasswordLength or $minPasswordLength < 3;
460 EnhancedMessagingPreferences => C4::Context->preference('EnhancedMessagingPreferences'),
461 GoogleJackets => C4::Context->preference("GoogleJackets"),
462 OpenLibraryCovers => C4::Context->preference("OpenLibraryCovers"),
463 KohaAdminEmailAddress => "" . C4::Context->preference("KohaAdminEmailAddress"),
464 LoginFirstname => ( C4::Context->userenv ? C4::Context->userenv->{"firstname"} : "Bel" ),
465 LoginSurname => C4::Context->userenv ? C4::Context->userenv->{"surname"} : "Inconnu",
466 emailaddress => C4::Context->userenv ? C4::Context->userenv->{"emailaddress"} : undef,
467 TagsEnabled => C4::Context->preference("TagsEnabled"),
468 hide_marc => C4::Context->preference("hide_marc"),
469 item_level_itypes => C4::Context->preference('item-level_itypes'),
470 patronimages => C4::Context->preference("patronimages"),
471 singleBranchMode => ( Koha::Libraries->search->count == 1 ),
472 noItemTypeImages => C4::Context->preference("noItemTypeImages"),
473 marcflavour => C4::Context->preference("marcflavour"),
474 OPACBaseURL => C4::Context->preference('OPACBaseURL'),
475 minPasswordLength => $minPasswordLength,
477 if ( $in->{'type'} eq "intranet" ) {
480 advancedMARCEditor => C4::Context->preference("advancedMARCEditor"),
481 AllowMultipleCovers => C4::Context->preference('AllowMultipleCovers'),
482 AmazonCoverImages => C4::Context->preference("AmazonCoverImages"),
483 AutoLocation => C4::Context->preference("AutoLocation"),
484 can_see_cataloguing_module => haspermission( $user, get_cataloguing_page_permissions() ) ? 1 : 0,
485 canreservefromotherbranches => C4::Context->preference('canreservefromotherbranches'),
486 EasyAnalyticalRecords => C4::Context->preference('EasyAnalyticalRecords'),
487 EnableBorrowerFiles => C4::Context->preference('EnableBorrowerFiles'),
488 FRBRizeEditions => C4::Context->preference("FRBRizeEditions"),
489 IndependentBranches => C4::Context->preference("IndependentBranches"),
490 intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),
491 IntranetFavicon => C4::Context->preference("IntranetFavicon"),
492 IntranetmainUserblock => C4::Context->preference("IntranetmainUserblock"),
493 IntranetNav => C4::Context->preference("IntranetNav"),
494 intranetreadinghistory => C4::Context->preference("intranetreadinghistory"),
495 intranetstylesheet => C4::Context->preference("intranetstylesheet"),
496 IntranetUserCSS => C4::Context->preference("IntranetUserCSS"),
497 IntranetUserJS => C4::Context->preference("IntranetUserJS"),
498 LibraryName => C4::Context->preference("LibraryName"),
499 LocalCoverImages => C4::Context->preference('LocalCoverImages'),
500 OPACLocalCoverImages => C4::Context->preference('OPACLocalCoverImages'),
501 PatronAutoComplete => C4::Context->preference("PatronAutoComplete"),
502 pending_checkout_notes => Koha::Checkouts->search( { noteseen => 0 } ),
503 plugins_enabled => C4::Context->config("enable_plugins"),
504 StaffSerialIssueDisplayCount => C4::Context->preference("StaffSerialIssueDisplayCount"),
505 UseCourseReserves => C4::Context->preference("UseCourseReserves"),
506 useDischarge => C4::Context->preference('useDischarge'),
507 virtualshelves => C4::Context->preference("virtualshelves"),
511 warn "template type should be OPAC, here it is=[" . $in->{'type'} . "]" unless ( $in->{'type'} eq 'opac' );
513 #TODO : replace LibraryName syspref with 'system name', and remove this html processing
514 my $LibraryNameTitle = C4::Context->preference("LibraryName");
515 $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
516 $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
518 # clean up the busc param in the session
519 # if the page is not opac-detail and not the "add to list" page
520 # and not the "edit comments" page
521 if ( C4::Context->preference("OpacBrowseResults")
522 && $in->{'template_name'} =~ /opac-(.+)\.(?:tt|tmpl)$/ ) {
524 unless ( $pagename =~ /^(?:MARC|ISBD)?detail$/
525 or $pagename =~ /^showmarc$/
526 or $pagename =~ /^addbybiblionumber$/
527 or $pagename =~ /^review$/ )
529 my $sessionSearch = get_session( $sessionID );
530 $sessionSearch->clear( ["busc"] ) if $sessionSearch;
534 # variables passed from CGI: opac_css_override and opac_search_limits.
535 my $opac_search_limit = $ENV{'OPAC_SEARCH_LIMIT'};
536 my $opac_limit_override = $ENV{'OPAC_LIMIT_OVERRIDE'};
539 ( $opac_limit_override && $opac_search_limit && $opac_search_limit =~ /branch:([\w-]+)/ ) ||
540 ( $in->{'query'}->param('limit') && $in->{'query'}->param('limit') =~ /branch:([\w-]+)/ ) ||
541 ( $in->{'query'}->param('limit') && $in->{'query'}->param('limit') =~ /multibranchlimit:(\w+)/ )
543 $opac_name = $1; # opac_search_limit is a branch, so we use it.
544 } elsif ( $in->{'query'}->param('multibranchlimit') ) {
545 $opac_name = $in->{'query'}->param('multibranchlimit');
546 } elsif ( C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv && C4::Context->userenv->{'branch'} ) {
547 $opac_name = C4::Context->userenv->{'branch'};
550 # Decide if the patron can make suggestions in the OPAC
551 my $can_make_suggestions;
552 if ( C4::Context->preference('Suggestion') && C4::Context->preference('AnonSuggestions') ) {
553 $can_make_suggestions = 1;
554 } elsif ( C4::Context->userenv && C4::Context->userenv->{'number'} ) {
555 $can_make_suggestions = Koha::Patrons->find(C4::Context->userenv->{'number'})->category->can_make_suggestions;
558 my @search_groups = Koha::Library::Groups->get_search_groups({ interface => 'opac' })->as_list;
560 AnonSuggestions => "" . C4::Context->preference("AnonSuggestions"),
561 LibrarySearchGroups => \@search_groups,
562 opac_name => $opac_name,
563 LibraryName => "" . C4::Context->preference("LibraryName"),
564 LibraryNameTitle => "" . $LibraryNameTitle,
565 OPACAmazonCoverImages => C4::Context->preference("OPACAmazonCoverImages"),
566 OPACFRBRizeEditions => C4::Context->preference("OPACFRBRizeEditions"),
567 OpacHighlightedWords => C4::Context->preference("OpacHighlightedWords"),
568 OPACShelfBrowser => "" . C4::Context->preference("OPACShelfBrowser"),
569 OPACURLOpenInNewWindow => "" . C4::Context->preference("OPACURLOpenInNewWindow"),
570 OPACUserCSS => "" . C4::Context->preference("OPACUserCSS"),
571 OpacAuthorities => C4::Context->preference("OpacAuthorities"),
572 opac_css_override => $ENV{'OPAC_CSS_OVERRIDE'},
573 opac_search_limit => $opac_search_limit,
574 opac_limit_override => $opac_limit_override,
575 OpacBrowser => C4::Context->preference("OpacBrowser"),
576 OpacCloud => C4::Context->preference("OpacCloud"),
577 OpacKohaUrl => C4::Context->preference("OpacKohaUrl"),
578 OpacPasswordChange => C4::Context->preference("OpacPasswordChange"),
579 OPACPatronDetails => C4::Context->preference("OPACPatronDetails"),
580 OPACPrivacy => C4::Context->preference("OPACPrivacy"),
581 OPACFinesTab => C4::Context->preference("OPACFinesTab"),
582 OpacTopissue => C4::Context->preference("OpacTopissue"),
583 'Version' => C4::Context->preference('Version'),
584 hidelostitems => C4::Context->preference("hidelostitems"),
585 mylibraryfirst => ( C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv ) ? C4::Context->userenv->{'branch'} : '',
586 opacbookbag => "" . C4::Context->preference("opacbookbag"),
587 OpacFavicon => C4::Context->preference("OpacFavicon"),
588 opaclanguagesdisplay => "" . C4::Context->preference("opaclanguagesdisplay"),
589 opacreadinghistory => C4::Context->preference("opacreadinghistory"),
590 OPACUserJS => C4::Context->preference("OPACUserJS"),
591 opacuserlogin => "" . C4::Context->preference("opacuserlogin"),
592 OpenLibrarySearch => C4::Context->preference("OpenLibrarySearch"),
593 ShowReviewer => C4::Context->preference("ShowReviewer"),
594 ShowReviewerPhoto => C4::Context->preference("ShowReviewerPhoto"),
595 suggestion => $can_make_suggestions,
596 virtualshelves => "" . C4::Context->preference("virtualshelves"),
597 OPACSerialIssueDisplayCount => C4::Context->preference("OPACSerialIssueDisplayCount"),
598 SyndeticsClientCode => C4::Context->preference("SyndeticsClientCode"),
599 SyndeticsEnabled => C4::Context->preference("SyndeticsEnabled"),
600 SyndeticsCoverImages => C4::Context->preference("SyndeticsCoverImages"),
601 SyndeticsTOC => C4::Context->preference("SyndeticsTOC"),
602 SyndeticsSummary => C4::Context->preference("SyndeticsSummary"),
603 SyndeticsEditions => C4::Context->preference("SyndeticsEditions"),
604 SyndeticsExcerpt => C4::Context->preference("SyndeticsExcerpt"),
605 SyndeticsReviews => C4::Context->preference("SyndeticsReviews"),
606 SyndeticsAuthorNotes => C4::Context->preference("SyndeticsAuthorNotes"),
607 SyndeticsAwards => C4::Context->preference("SyndeticsAwards"),
608 SyndeticsSeries => C4::Context->preference("SyndeticsSeries"),
609 SyndeticsCoverImageSize => C4::Context->preference("SyndeticsCoverImageSize"),
610 OPACLocalCoverImages => C4::Context->preference("OPACLocalCoverImages"),
611 PatronSelfRegistration => C4::Context->preference("PatronSelfRegistration"),
612 PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
613 useDischarge => C4::Context->preference('useDischarge'),
616 $template->param( OpacPublic => '1' ) if ( $user || C4::Context->preference("OpacPublic") );
619 # Check if we were asked using parameters to force a specific language
620 if ( defined $in->{'query'}->param('language') ) {
622 # Extract the language, let C4::Languages::getlanguage choose
624 my $language = C4::Languages::getlanguage( $in->{'query'} );
625 my $languagecookie = C4::Templates::getlanguagecookie( $in->{'query'}, $language );
626 $cookie = $cookie_mgr->replace_in_list( $cookie, $languagecookie );
630 $template->param( loggedinusername => $user ); # OBSOLETE - Do not reuse this in template, use logged_in_user.userid instead
631 $template->param( loggedinusernumber => $borrowernumber ); # FIXME Should be replaced with logged_in_user.borrowernumber
632 $template->param( logged_in_user => $patron );
633 $template->param( sessionID => $sessionID );
635 return ( $template, $borrowernumber, $cookie, $flags );
640 ($userid, $cookie, $sessionID) = &checkauth($query, $noauth, $flagsrequired, $type);
642 Verifies that the user is authorized to run this script. If
643 the user is authorized, a (userid, cookie, session-id, flags)
644 quadruple is returned. If the user is not authorized but does
645 not have the required privilege (see $flagsrequired below), it
646 displays an error page and exits. Otherwise, it displays the
647 login page and exits.
649 Note that C<&checkauth> will return if and only if the user
650 is authorized, so it should be called early on, before any
651 unfinished operations (e.g., if you've opened a file, then
652 C<&checkauth> won't close it for you).
654 C<$query> is the CGI object for the script calling C<&checkauth>.
656 The C<$noauth> argument is optional. If it is set, then no
657 authorization is required for the script.
659 C<&checkauth> fetches user and session information from C<$query> and
660 ensures that the user is authorized to run scripts that require
663 The C<$flagsrequired> argument specifies the required privileges
664 the user must have if the username and password are correct.
665 It should be specified as a reference-to-hash; keys in the hash
666 should be the "flags" for the user, as specified in the Members
667 intranet module. Any key specified must correspond to a "flag"
668 in the userflags table. E.g., { circulate => 1 } would specify
669 that the user must have the "circulate" privilege in order to
670 proceed. To make sure that access control is correct, the
671 C<$flagsrequired> parameter must be specified correctly.
673 Koha also has a concept of sub-permissions, also known as
674 granular permissions. This makes the value of each key
675 in the C<flagsrequired> hash take on an additional
680 The user must have access to all subfunctions of the module
681 specified by the hash key.
685 The user must have access to at least one subfunction of the module
686 specified by the hash key.
688 specific permission, e.g., 'export_catalog'
690 The user must have access to the specific subfunction list, which
691 must correspond to a row in the permissions table.
693 The C<$type> argument specifies whether the template should be
694 retrieved from the opac or intranet directory tree. "opac" is
695 assumed if it is not specified; however, if C<$type> is specified,
696 "intranet" is assumed if it is not "opac".
698 If C<$query> does not have a valid session ID associated with it
699 (i.e., the user has not logged in) or if the session has expired,
700 C<&checkauth> presents the user with a login page (from the point of
701 view of the original script, C<&checkauth> does not return). Once the
702 user has authenticated, C<&checkauth> restarts the original script
703 (this time, C<&checkauth> returns).
705 The login page is provided using a HTML::Template, which is set in the
706 systempreferences table or at the top of this file. The variable C<$type>
707 selects which template to use, either the opac or the intranet
708 authentification template.
710 C<&checkauth> returns a user ID, a cookie, and a session ID. The
711 cookie should be sent back to the browser; it verifies that the user
721 # If version syspref is unavailable, it means Koha is being installed,
722 # and so we must redirect to OPAC maintenance page or to the WebInstaller
723 # also, if OpacMaintenance is ON, OPAC should redirect to maintenance
724 if ( C4::Context->preference('OpacMaintenance') && $type eq 'opac' ) {
725 warn "OPAC Install required, redirecting to maintenance";
726 print $query->redirect("/cgi-bin/koha/maintenance.pl");
729 unless ( $version = C4::Context->preference('Version') ) { # assignment, not comparison
730 if ( $type ne 'opac' ) {
731 warn "Install required, redirecting to Installer";
732 print $query->redirect("/cgi-bin/koha/installer/install.pl");
734 warn "OPAC Install required, redirecting to maintenance";
735 print $query->redirect("/cgi-bin/koha/maintenance.pl");
740 # check that database and koha version are the same
741 # there is no DB version, it's a fresh install,
742 # go to web installer
743 # there is a DB version, compare it to the code version
744 my $kohaversion = Koha::version();
746 # remove the 3 last . to have a Perl number
747 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
748 Koha::Logger->get->debug("kohaversion : $kohaversion");
749 if ( $version < $kohaversion ) {
750 my $warning = "Database update needed, redirecting to %s. Database is $version and Koha is $kohaversion";
751 if ( $type ne 'opac' ) {
752 warn sprintf( $warning, 'Installer' );
753 print $query->redirect("/cgi-bin/koha/installer/install.pl?step=1&op=updatestructure");
755 warn sprintf( "OPAC: " . $warning, 'maintenance' );
756 print $query->redirect("/cgi-bin/koha/maintenance.pl");
762 sub _timeout_syspref {
763 my $default_timeout = 600;
764 my $timeout = C4::Context->preference('timeout') || $default_timeout;
766 # value in days, convert in seconds
767 if ( $timeout =~ /^(\d+)[dD]$/ ) {
768 $timeout = $1 * 86400;
770 # value in hours, convert in seconds
771 elsif ( $timeout =~ /^(\d+)[hH]$/ ) {
772 $timeout = $1 * 3600;
774 elsif ( $timeout !~ m/^\d+$/ ) {
775 warn "The value of the system preference 'timeout' is not correct, defaulting to $default_timeout";
776 $timeout = $default_timeout;
785 # Get shibboleth login attribute
786 my $shib = C4::Context->config('useshibboleth') && shib_ok();
787 my $shib_login = $shib ? get_login_shib() : undef;
789 # $authnotrequired will be set for scripts which will run without authentication
790 my $authnotrequired = shift;
791 my $flagsrequired = shift;
793 my $emailaddress = shift;
794 my $template_name = shift;
795 my $params = shift || {}; # do_not_print
796 $type = 'opac' unless $type;
798 if ( $type eq 'opac' && !C4::Context->preference("OpacPublic") ) {
799 my @allowed_scripts_for_private_opac = qw(
801 opac-registration-email-sent.tt
802 opac-registration-confirmation.tt
803 opac-memberentry-update-submitted.tt
804 opac-password-recovery.tt
805 opac-reset-password.tt
808 $authnotrequired = 0 unless grep { $_ eq $template_name }
809 @allowed_scripts_for_private_opac;
812 my $timeout = _timeout_syspref();
814 my $cookie_mgr = Koha::CookieManager->new;
816 _version_check( $type, $query );
819 my $auth_state = 'failed';
821 my ( $userid, $cookie, $sessionID, $flags );
823 my $logout = $query->param('logout.x');
825 my $anon_search_history;
827 # This parameter is the name of the CAS server we want to authenticate against,
828 # when using authentication against multiple CAS servers, as configured in Auth_cas_servers.yaml
829 my $casparam = $query->param('cas');
830 my $q_userid = $query->param('userid') // '';
833 my $invalid_otp_token;
835 ( $type ne "opac" # Only available for the staff interface
836 && C4::Context->preference('TwoFactorAuthentication') ne "disabled" ) # If "enabled" or "enforced"
839 # Basic authentication is incompatible with the use of Shibboleth,
840 # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
841 # and it may not be the attribute we want to use to match the koha login.
843 # Also, do not consider an empty REMOTE_USER.
845 # Finally, after those tests, we can assume (although if it would be better with
846 # a syspref) that if we get a REMOTE_USER, that's from basic authentication,
847 # and we can affect it to $userid.
848 if ( !$shib and defined( $ENV{'REMOTE_USER'} ) and $ENV{'REMOTE_USER'} ne '' and $userid = $ENV{'REMOTE_USER'} ) {
850 # Using Basic Authentication, no cookies required
851 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
852 -name => 'CGISESSID',
855 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
859 elsif ( $emailaddress) {
860 # the Google OpenID Connect passes an email address
862 elsif ( $sessionID = $query->cookie("CGISESSID") ) { # assignment, not comparison
863 my ( $return, $more_info );
864 # NOTE: $flags in the following call is still undefined !
865 ( $return, $session, $more_info ) = check_cookie_auth( $sessionID, $flags,
866 { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
869 if ( $return eq 'ok' || $return eq 'additional-auth-needed' ) {
870 $userid = $session->param('id');
874 $return eq 'ok' ? 'completed'
875 : $return eq 'additional-auth-needed' ? 'additional-auth-needed'
878 # We are at the second screen if the waiting-for-2FA is set in session
879 # and otp_token param has been passed
881 && $auth_state eq 'additional-auth-needed'
882 && ( my $otp_token = $query->param('otp_token') ) )
884 my $patron = Koha::Patrons->find( { userid => $userid } );
885 my $auth = Koha::Auth::TwoFactorAuth->new( { patron => $patron } );
886 my $verified = $auth->verify($otp_token);
889 # The token is correct, the user is fully logged in!
890 $auth_state = 'completed';
891 $session->param( 'waiting-for-2FA', 0 );
892 $session->param( 'waiting-for-2FA-setup', 0 );
894 # This is an ugly trick to pass the test
895 # $query->param('koha_login_context') && ( $q_userid ne $userid )
900 $invalid_otp_token = 1;
904 if ( $auth_state eq 'completed' ) {
905 Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
907 if ( ( $query->param('koha_login_context') && ( $q_userid ne $userid ) )
908 || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
909 || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
912 #if a user enters an id ne to the id in the current session, we need to log them in...
913 #first we need to clear the anonymous session...
914 $anon_search_history = $session->param('search_history');
917 $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
918 C4::Context::_unset_userenv($sessionID);
920 undef $userid; # IMPORTANT: this assures us a new session in code below
921 $auth_state = 'failed';
924 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
925 -name => 'CGISESSID',
926 -value => $session->id,
928 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
932 $flags = haspermission( $userid, $flagsrequired );
934 $auth_state = 'failed';
935 $info{'nopermission'} = 1;
938 } elsif ( !$logout ) {
939 if ( $return eq 'expired' ) {
940 $info{timed_out} = 1;
941 } elsif ( $return eq 'restricted' ) {
942 $info{oldip} = $more_info->{old_ip};
943 $info{newip} = $more_info->{new_ip};
944 $info{different_ip} = 1;
945 } elsif ( $return eq 'password_expired' ) {
946 $info{password_has_expired} = 1;
951 if ( $auth_state eq 'failed' || $logout ) {
958 # voluntary logout the user
959 # check wether the user was using their shibboleth session or a local one
960 my $shibSuccess = C4::Context->userenv ? C4::Context->userenv->{'shibboleth'} : undef;
965 C4::Context::_unset_userenv($sessionID);
966 $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
968 if ($cas and $caslogout) {
969 logout_cas($query, $type);
972 # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
973 if ( $shib and $shib_login and $shibSuccess) {
978 $auth_state = 'logout';
982 #we initiate a session prior to checking for a username to allow for anonymous sessions...
983 if( !$session or !$sessionID ) { # if we cleared sessionID, we need a new session
984 $session = get_session() or die "Auth ERROR: Cannot get_session()";
987 # Save anonymous search history in new session so it can be retrieved
988 # by get_template_and_user to store it in user's search history after
989 # a successful login.
990 if ($anon_search_history) {
991 $session->param( 'search_history', $anon_search_history );
994 $sessionID = $session->id;
995 C4::Context->_new_userenv($sessionID);
996 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
997 -name => 'CGISESSID',
998 -value => $sessionID,
1000 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1003 my $pki_field = C4::Context->preference('AllowPKIAuth');
1004 if ( !defined($pki_field) ) {
1005 print STDERR "ERROR: Missing system preference AllowPKIAuth.\n";
1006 $pki_field = 'None';
1008 if ( ( $cas && $query->param('ticket') )
1010 || ( $shib && $shib_login )
1011 || $pki_field ne 'None'
1014 my $password = $query->param('password');
1015 my $shibSuccess = 0;
1016 my ( $return, $cardnumber );
1018 # If shib is enabled and we have a shib login, does the login match a valid koha user
1019 if ( $shib && $shib_login ) {
1022 # Do not pass password here, else shib will not be checked in checkpw.
1023 ( $return, $cardnumber, $retuserid ) = checkpw( $q_userid, undef, $query );
1024 $userid = $retuserid;
1025 $shibSuccess = $return;
1026 $info{'invalidShibLogin'} = 1 unless ($return);
1029 # If shib login and match were successful, skip further login methods
1030 unless ($shibSuccess) {
1031 if ( $cas && $query->param('ticket') ) {
1034 ( $return, $cardnumber, $retuserid, $patron, $cas_ticket ) =
1035 checkpw( $userid, $password, $query, $type );
1036 $userid = $retuserid;
1037 $info{'invalidCasLogin'} = 1 unless ($return);
1040 elsif ( $emailaddress ) {
1041 my $value = $emailaddress;
1043 # If we're looking up the email, there's a chance that the person
1044 # doesn't have a userid. So if there is none, we pass along the
1045 # borrower number, and the bits of code that need to know the user
1046 # ID will have to be smart enough to handle that.
1047 my $patrons = Koha::Patrons->search({ email => $value });
1048 if ($patrons->count) {
1050 # First the userid, then the borrowernum
1051 my $patron = $patrons->next;
1052 $value = $patron->userid || $patron->borrowernumber;
1056 $return = $value ? 1 : 0;
1061 ( $pki_field eq 'Common Name' && $ENV{'SSL_CLIENT_S_DN_CN'} )
1062 || ( $pki_field eq 'emailAddress'
1063 && $ENV{'SSL_CLIENT_S_DN_Email'} )
1067 if ( $pki_field eq 'Common Name' ) {
1068 $value = $ENV{'SSL_CLIENT_S_DN_CN'};
1070 elsif ( $pki_field eq 'emailAddress' ) {
1071 $value = $ENV{'SSL_CLIENT_S_DN_Email'};
1073 # If we're looking up the email, there's a chance that the person
1074 # doesn't have a userid. So if there is none, we pass along the
1075 # borrower number, and the bits of code that need to know the user
1076 # ID will have to be smart enough to handle that.
1077 my $patrons = Koha::Patrons->search({ email => $value });
1078 if ($patrons->count) {
1080 # First the userid, then the borrowernum
1081 my $patron = $patrons->next;
1082 $value = $patron->userid || $patron->borrowernumber;
1088 $return = $value ? 1 : 0;
1094 my $request_method = $query->request_method // q{};
1097 $request_method eq 'POST'
1098 || ( C4::Context->preference('AutoSelfCheckID')
1099 && $q_userid eq C4::Context->preference('AutoSelfCheckID') )
1104 ( $return, $cardnumber, $retuserid, $patron, $cas_ticket ) =
1105 checkpw( $q_userid, $password, $query, $type );
1106 $userid = $retuserid if ($retuserid);
1107 $info{'invalid_username_or_password'} = 1 unless ($return);
1112 # If shib configured and shibOnly enabled, we should ignore anything other than a shibboleth type login.
1119 && C4::Context->preference('OPACShibOnly')
1121 || ( ( $type ne 'opac' )
1122 && C4::Context->preference('staffShibOnly') )
1129 # $return: 1 = valid user
1130 if( $return && $return > 0 ) {
1132 if ( $flags = haspermission( $userid, $flagsrequired ) ) {
1133 $auth_state = "logged_in";
1136 $auth_state = 'failed';
1137 # FIXME We could add $return = 0; or even delete the session?
1138 # Currently return == 1 and we will fill session info later on,
1139 # although we do present an authorization failure. (Yes, the
1140 # authentication was actually correct.)
1141 $info{'nopermission'} = 1;
1142 C4::Context::_unset_userenv($sessionID);
1144 my ( $borrowernumber, $firstname, $surname, $userflags,
1145 $branchcode, $branchname, $emailaddress, $desk_id,
1146 $desk_name, $register_id, $register_name );
1148 if ( $return == 1 ) {
1150 SELECT borrowernumber, firstname, surname, flags, borrowers.branchcode,
1151 branches.branchname as branchname, email
1153 LEFT JOIN branches on borrowers.branchcode=branches.branchcode
1155 my $dbh = C4::Context->dbh;
1156 my $sth = $dbh->prepare("$select where userid=?");
1157 $sth->execute($userid);
1158 unless ( $sth->rows ) {
1159 $sth = $dbh->prepare("$select where cardnumber=?");
1160 $sth->execute($cardnumber);
1162 unless ( $sth->rows ) {
1163 $sth->execute($userid);
1167 ( $borrowernumber, $firstname, $surname, $userflags,
1168 $branchcode, $branchname, $emailaddress ) = $sth->fetchrow;
1171 # launch a sequence to check if we have a ip for the branch, i
1172 # if we have one we replace the branchcode of the userenv by the branch bound in the ip.
1174 my $ip = $ENV{'REMOTE_ADDR'};
1176 # if they specify at login, use that
1177 my $patron = Koha::Patrons->find({userid => $userid});
1178 if ( $query->param('branch') && ( haspermission($userid, { 'loggedinlibrary'=> 1 }) || $patron->is_superlibrarian ) ) {
1179 $branchcode = $query->param('branch');
1180 my $library = Koha::Libraries->find($branchcode);
1181 $branchname = $library? $library->branchname: '';
1183 if ( $query->param('desk_id') ) {
1184 $desk_id = $query->param('desk_id');
1185 my $desk = Koha::Desks->find($desk_id);
1186 $desk_name = $desk ? $desk->desk_name : '';
1188 if ( C4::Context->preference('UseCashRegisters') ) {
1190 $query->param('register_id')
1191 ? Koha::Cash::Registers->find($query->param('register_id'))
1192 : Koha::Cash::Registers->search(
1193 { branch => $branchcode, branch_default => 1 },
1194 { rows => 1 } )->single;
1195 $register_id = $register->id if ($register);
1196 $register_name = $register->name if ($register);
1198 my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search->as_list };
1199 if ( $type ne 'opac' and C4::Context->preference('AutoLocation') ) {
1201 # we have to check they are coming from the right ip range
1202 my $domain = $branches->{$branchcode}->{'branchip'};
1203 $domain =~ s|\.\*||g;
1204 if ( $ip !~ /^$domain/ ) {
1205 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
1206 -name => 'CGISESSID',
1209 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1212 $info{'wrongip'} = 1;
1213 $auth_state = "failed";
1217 if ( C4::Context->preference('AutoLocation') && $auth_state ne 'failed' ) {
1218 foreach my $br ( uniq( $branchcode, keys %$branches ) ) {
1220 # now we work with the treatment of ip
1221 my $domain = $branches->{$br}->{'branchip'};
1222 if ( $domain && $ip =~ /^$domain/ ) {
1223 $branchcode = $branches->{$br}->{'branchcode'};
1225 # new op dev : add the branchname to the cookie
1226 $branchname = $branches->{$br}->{'branchname'};
1232 my $is_sco_user = 0;
1233 if ( $query->param('sco_user_login') && ( $query->param('sco_user_login') eq '1' ) ){
1237 $session->param( 'number', $borrowernumber );
1238 $session->param( 'id', $userid );
1239 $session->param( 'cardnumber', $cardnumber );
1240 $session->param( 'firstname', $firstname );
1241 $session->param( 'surname', $surname );
1242 $session->param( 'branch', $branchcode );
1243 $session->param( 'branchname', $branchname );
1244 $session->param( 'desk_id', $desk_id);
1245 $session->param( 'desk_name', $desk_name);
1246 $session->param( 'flags', $userflags );
1247 $session->param( 'emailaddress', $emailaddress );
1248 $session->param( 'ip', $session->remote_addr() );
1249 $session->param( 'lasttime', time() );
1250 $session->param( 'interface', $type);
1251 $session->param( 'shibboleth', $shibSuccess );
1252 $session->param( 'register_id', $register_id );
1253 $session->param( 'register_name', $register_name );
1254 $session->param( 'sco_user', $is_sco_user );
1256 $session->param('cas_ticket', $cas_ticket) if $cas_ticket;
1257 C4::Context->set_userenv(
1258 $session->param('number'), $session->param('id'),
1259 $session->param('cardnumber'), $session->param('firstname'),
1260 $session->param('surname'), $session->param('branch'),
1261 $session->param('branchname'), $session->param('flags'),
1262 $session->param('emailaddress'), $session->param('shibboleth'),
1263 $session->param('desk_id'), $session->param('desk_name'),
1264 $session->param('register_id'), $session->param('register_name')
1268 # $return: 0 = invalid user
1269 # reset to anonymous session
1272 $info{'invalid_username_or_password'} = 1;
1273 C4::Context::_unset_userenv($sessionID);
1275 $session->param( 'lasttime', time() );
1276 $session->param( 'ip', $session->remote_addr() );
1277 $session->param( 'sessiontype', 'anon' );
1278 $session->param( 'interface', $type);
1280 } # END if ( $q_userid
1281 elsif ( $type eq "opac" ) {
1283 # anonymous sessions are created only for the OPAC
1285 # setting a couple of other session vars...
1286 $session->param( 'ip', $session->remote_addr() );
1287 $session->param( 'lasttime', time() );
1288 $session->param( 'sessiontype', 'anon' );
1289 $session->param( 'interface', $type);
1292 } # END unless ($userid)
1295 if ( $auth_state eq 'logged_in' ) {
1296 $auth_state = 'completed';
1298 # Auth is completed unless an additional auth is needed
1299 if ( $require_2FA ) {
1300 my $patron = Koha::Patrons->find({userid => $userid});
1301 if ( C4::Context->preference('TwoFactorAuthentication') eq "enforced" && $patron->auth_method eq 'password' ) {
1302 $auth_state = 'setup-additional-auth-needed';
1303 $session->param('waiting-for-2FA-setup', 1);
1304 %info = ();# We remove the warnings/errors we may have set incorrectly before
1305 } elsif ( $patron->auth_method eq 'two-factor' ) {
1306 # Ask for the OTP token
1307 $auth_state = 'additional-auth-needed';
1308 $session->param('waiting-for-2FA', 1);
1309 %info = ();# We remove the warnings/errors we may have set incorrectly before
1314 # finished authentification, now respond
1315 if ( $auth_state eq 'completed' || $authnotrequired ) {
1318 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
1319 -name => 'CGISESSID',
1322 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1327 track_login_daily( $userid );
1329 # In case, that this request was a login attempt, we want to prevent that users can repost the opac login
1330 # request. We therefore redirect the user to the requested page again without the login parameters.
1331 # See Post/Redirect/Get (PRG) design pattern: https://en.wikipedia.org/wiki/Post/Redirect/Get
1332 if ( $type eq "opac" && $query->param('koha_login_context') && $query->param('koha_login_context') ne 'sco' && $query->param('password') && $query->param('userid') ) {
1333 my $uri = URI->new($query->url(-relative=>1, -query_string=>1));
1334 $uri->query_param_delete('userid');
1335 $uri->query_param_delete('password');
1336 $uri->query_param_delete('koha_login_context');
1337 unless ( $params->{do_not_print} ) {
1338 print $query->redirect( -uri => $uri->as_string, -cookie => $cookie, -status => '303 See other' );
1343 return ( $userid, $cookie, $sessionID, $flags );
1348 # AUTH rejected, show the login/password template, after checking the DB.
1352 my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1354 # get the inputs from the incoming query
1356 my @inputs_to_clean = qw( userid password ticket logout.x otp_token );
1357 foreach my $name ( param $query) {
1358 next if grep { $name eq $_ } @inputs_to_clean;
1359 my @value = $query->multi_param($name);
1360 push @inputs, { name => $name, value => $_ } for @value;
1363 my $LibraryNameTitle = C4::Context->preference("LibraryName");
1364 $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1365 $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
1367 my $auth_template_name = ( $type eq 'opac' ) ? 'opac-auth.tt' : 'auth.tt';
1368 my $auth_error = $query->param('auth_error');
1369 my $template = C4::Templates::gettemplate( $auth_template_name, $type, $query );
1373 script_name => get_script_name(),
1374 casAuthentication => C4::Context->preference("casAuthentication"),
1375 shibbolethAuthentication => $shib,
1376 suggestion => C4::Context->preference("suggestion"),
1377 virtualshelves => C4::Context->preference("virtualshelves"),
1378 LibraryName => "" . C4::Context->preference("LibraryName"),
1379 LibraryNameTitle => "" . $LibraryNameTitle,
1380 opacuserlogin => C4::Context->preference("opacuserlogin"),
1381 OpacFavicon => C4::Context->preference("OpacFavicon"),
1382 opacreadinghistory => C4::Context->preference("opacreadinghistory"),
1383 opaclanguagesdisplay => C4::Context->preference("opaclanguagesdisplay"),
1384 OPACUserJS => C4::Context->preference("OPACUserJS"),
1385 OPACUserCSS => C4::Context->preference("OPACUserCSS"),
1386 intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),
1387 intranetstylesheet => C4::Context->preference("intranetstylesheet"),
1388 IntranetNav => C4::Context->preference("IntranetNav"),
1389 IntranetFavicon => C4::Context->preference("IntranetFavicon"),
1390 IntranetUserCSS => C4::Context->preference("IntranetUserCSS"),
1391 IntranetUserJS => C4::Context->preference("IntranetUserJS"),
1392 IndependentBranches => C4::Context->preference("IndependentBranches"),
1393 AutoLocation => C4::Context->preference("AutoLocation"),
1394 wrongip => $info{'wrongip'},
1395 PatronSelfRegistration => C4::Context->preference("PatronSelfRegistration"),
1396 PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
1397 opac_css_override => $ENV{'OPAC_CSS_OVERRIDE'},
1398 too_many_login_attempts => ( $patron and $patron->account_locked ),
1399 password_has_expired => ( $patron and $patron->password_expired ),
1400 auth_error => $auth_error,
1403 $template->param( SCO_login => 1 ) if ( $query->param('sco_user_login') );
1404 $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1405 $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1406 $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1407 if ( $auth_state eq 'additional-auth-needed' ) {
1408 my $patron = Koha::Patrons->find( { userid => $userid } );
1411 invalid_otp_token => $invalid_otp_token,
1412 notice_email_address => $patron->notice_email_address, # We could also pass logged_in_user if necessary
1416 if ( $auth_state eq 'setup-additional-auth-needed' ) {
1422 if ( $type eq 'opac' ) {
1423 require Koha::Virtualshelves;
1424 my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
1430 some_public_shelves => $some_public_shelves,
1436 # Is authentication against multiple CAS servers enabled?
1437 require C4::Auth_with_cas;
1438 if ( multipleAuth() && !$casparam ) {
1439 my $casservers = getMultipleAuth();
1441 foreach my $key ( keys %$casservers ) {
1442 push @tmplservers, { name => $key, value => login_cas_url( $query, $key, $type ) . "?cas=$key" };
1445 casServersLoop => \@tmplservers
1449 casServerUrl => login_cas_url($query, undef, $type),
1454 invalidCasLogin => $info{'invalidCasLogin'}
1459 #If shibOnly is enabled just go ahead and redirect directly
1460 if ( (($type eq 'opac') && C4::Context->preference('OPACShibOnly')) || (($type ne 'opac') && C4::Context->preference('staffShibOnly')) ) {
1461 my $redirect_url = login_shib_url( $query );
1462 print $query->redirect( -uri => "$redirect_url", -status => 303 );
1467 shibbolethAuthentication => $shib,
1468 shibbolethLoginUrl => login_shib_url($query),
1472 if (C4::Context->preference('GoogleOpenIDConnect')) {
1473 if ($query->param("OpenIDConnectFailed")) {
1474 my $reason = $query->param('OpenIDConnectFailed');
1475 $template->param(invalidGoogleOpenIDConnectLogin => $reason);
1480 LibraryName => C4::Context->preference("LibraryName"),
1482 sessionID => $session->id,
1485 if ( $params->{do_not_print} ) {
1486 # This must be used for testing purpose only!
1487 return ( undef, undef, undef, undef, $template );
1490 print $query->header(
1491 { type => 'text/html',
1494 'X-Frame-Options' => 'SAMEORIGIN',
1502 =head2 check_api_auth
1504 ($status, $cookie, $sessionId) = check_api_auth($query, $userflags);
1506 Given a CGI query containing the parameters 'userid' and 'password' and/or a session
1507 cookie, determine if the user has the privileges specified by C<$userflags>.
1509 C<check_api_auth> is is meant for authenticating users of web services, and
1510 consequently will always return and will not attempt to redirect the user
1513 If a valid session cookie is already present, check_api_auth will return a status
1514 of "ok", the cookie, and the Koha session ID.
1516 If no session cookie is present, check_api_auth will check the 'userid' and 'password
1517 parameters and create a session cookie and Koha session if the supplied credentials
1520 Possible return values in C<$status> are:
1524 =item "ok" -- user authenticated; C<$cookie> and C<$sessionid> have valid values.
1526 =item "failed" -- credentials are not correct; C<$cookie> and C<$sessionid> are undef
1528 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1530 =item "expired -- session cookie has expired; API user should resubmit userid and password
1532 =item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1534 =item "additional-auth-needed -- User is in an authentication process that is not finished
1540 sub check_api_auth {
1543 my $flagsrequired = shift;
1544 my $timeout = _timeout_syspref();
1546 unless ( C4::Context->preference('Version') ) {
1548 # database has not been installed yet
1549 return ( "maintenance", undef, undef );
1551 my $kohaversion = Koha::version();
1552 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1553 if ( C4::Context->preference('Version') < $kohaversion ) {
1555 # database in need of version update; assume that
1556 # no API should be called while databsae is in
1558 return ( "maintenance", undef, undef );
1561 my ( $sessionID, $session );
1562 unless ( $query->param('userid') ) {
1563 $sessionID = $query->cookie("CGISESSID");
1565 if ( $sessionID && not( $cas && $query->param('PT') ) ) {
1568 ( $return, $session, undef ) = check_cookie_auth(
1569 $sessionID, $flagsrequired, { remote_addr => $ENV{REMOTE_ADDR} } );
1571 return ( $return, undef, undef ) # Cookie auth failed
1574 my $cookie = $query->cookie(
1575 -name => 'CGISESSID',
1576 -value => $session->id,
1578 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1581 return ( $return, $cookie, $session ); # return == 'ok' here
1586 my $userid = $query->param('userid');
1587 my $password = $query->param('password');
1588 my ( $return, $cardnumber, $cas_ticket );
1591 if ( $cas && $query->param('PT') ) {
1594 # In case of a CAS authentication, we use the ticket instead of the password
1595 my $PT = $query->param('PT');
1596 ( $return, $cardnumber, $userid, $cas_ticket ) = check_api_auth_cas( $PT, $query ); # EXTERNAL AUTH
1599 # User / password auth
1600 unless ( $userid and $password ) {
1602 # caller did something wrong, fail the authenticateion
1603 return ( "failed", undef, undef );
1607 ( $return, $cardnumber, $newuserid, $patron, $cas_ticket ) = checkpw( $userid, $password, $query );
1610 if ( $return and haspermission( $userid, $flagsrequired ) ) {
1611 my $session = get_session("");
1612 return ( "failed", undef, undef ) unless $session;
1614 my $sessionID = $session->id;
1615 C4::Context->_new_userenv($sessionID);
1616 my $cookie = $query->cookie(
1617 -name => 'CGISESSID',
1618 -value => $sessionID,
1620 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1623 if ( $return == 1 ) {
1625 $borrowernumber, $firstname, $surname,
1626 $userflags, $branchcode, $branchname,
1629 my $dbh = C4::Context->dbh;
1632 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where userid=?"
1634 $sth->execute($userid);
1636 $borrowernumber, $firstname, $surname,
1637 $userflags, $branchcode, $branchname,
1639 ) = $sth->fetchrow if ( $sth->rows );
1641 unless ( $sth->rows ) {
1642 my $sth = $dbh->prepare(
1643 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
1645 $sth->execute($cardnumber);
1647 $borrowernumber, $firstname, $surname,
1648 $userflags, $branchcode, $branchname,
1650 ) = $sth->fetchrow if ( $sth->rows );
1652 unless ( $sth->rows ) {
1653 $sth->execute($userid);
1655 $borrowernumber, $firstname, $surname, $userflags,
1656 $branchcode, $branchname, $emailaddress
1657 ) = $sth->fetchrow if ( $sth->rows );
1661 my $ip = $ENV{'REMOTE_ADDR'};
1663 # if they specify at login, use that
1664 if ( $query->param('branch') ) {
1665 $branchcode = $query->param('branch');
1666 my $library = Koha::Libraries->find($branchcode);
1667 $branchname = $library? $library->branchname: '';
1669 my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search->as_list };
1670 foreach my $br ( keys %$branches ) {
1672 # now we work with the treatment of ip
1673 my $domain = $branches->{$br}->{'branchip'};
1674 if ( $domain && $ip =~ /^$domain/ ) {
1675 $branchcode = $branches->{$br}->{'branchcode'};
1677 # new op dev : add the branchname to the cookie
1678 $branchname = $branches->{$br}->{'branchname'};
1681 $session->param( 'number', $borrowernumber );
1682 $session->param( 'id', $userid );
1683 $session->param( 'cardnumber', $cardnumber );
1684 $session->param( 'firstname', $firstname );
1685 $session->param( 'surname', $surname );
1686 $session->param( 'branch', $branchcode );
1687 $session->param( 'branchname', $branchname );
1688 $session->param( 'flags', $userflags );
1689 $session->param( 'emailaddress', $emailaddress );
1690 $session->param( 'ip', $session->remote_addr() );
1691 $session->param( 'lasttime', time() );
1692 $session->param( 'interface', 'api' );
1694 $session->param( 'cas_ticket', $cas_ticket);
1695 C4::Context->set_userenv(
1696 $session->param('number'), $session->param('id'),
1697 $session->param('cardnumber'), $session->param('firstname'),
1698 $session->param('surname'), $session->param('branch'),
1699 $session->param('branchname'), $session->param('flags'),
1700 $session->param('emailaddress'), $session->param('shibboleth'),
1701 $session->param('desk_id'), $session->param('desk_name'),
1702 $session->param('register_id'), $session->param('register_name')
1704 return ( "ok", $cookie, $sessionID );
1706 return ( "failed", undef, undef );
1711 =head2 check_cookie_auth
1713 ($status, $sessionId) = check_cookie_auth($cookie, $userflags);
1715 Given a CGISESSID cookie set during a previous login to Koha, determine
1716 if the user has the privileges specified by C<$userflags>. C<$userflags>
1717 is passed unaltered into C<haspermission> and as such accepts all options
1718 avaiable to that routine with the one caveat that C<check_api_auth> will
1719 also allow 'undef' to be passed and in such a case the permissions check
1720 will be skipped altogether.
1722 C<check_cookie_auth> is meant for authenticating special services
1723 such as tools/upload-file.pl that are invoked by other pages that
1724 have been authenticated in the usual way.
1726 Possible return values in C<$status> are:
1730 =item "ok" -- user authenticated; C<$sessionID> have valid values.
1732 =item "anon" -- user not authenticated but valid for anonymous session.
1734 =item "failed" -- credentials are not correct; C<$sessionid> are undef
1736 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1738 =item "expired -- session cookie has expired; API user should resubmit userid and password
1740 =item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1746 sub check_cookie_auth {
1747 my $sessionID = shift;
1748 my $flagsrequired = shift;
1751 my $remote_addr = $params->{remote_addr} || $ENV{REMOTE_ADDR};
1753 my $skip_version_check = $params->{skip_version_check}; # Only for checkauth
1755 unless ( $skip_version_check ) {
1756 unless ( C4::Context->preference('Version') ) {
1758 # database has not been installed yet
1759 return ( "maintenance", undef );
1761 my $kohaversion = Koha::version();
1762 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1763 if ( C4::Context->preference('Version') < $kohaversion ) {
1765 # database in need of version update; assume that
1766 # no API should be called while databsae is in
1768 return ( "maintenance", undef );
1772 # see if we have a valid session cookie already
1773 # however, if a userid parameter is present (i.e., from
1774 # a form submission, assume that any current cookie
1776 unless ( $sessionID ) {
1777 return ( "failed", undef );
1779 C4::Context::_unset_userenv($sessionID); # remove old userenv first
1780 my $session = get_session($sessionID);
1782 my $userid = $session->param('id');
1783 my $ip = $session->param('ip');
1784 my $lasttime = $session->param('lasttime');
1785 my $timeout = _timeout_syspref();
1787 if ( !$lasttime || ( $lasttime < time() - $timeout ) ) {
1791 return ("expired", undef);
1793 } elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $remote_addr ) {
1794 # IP address changed
1797 return ( "restricted", undef, { old_ip => $ip, new_ip => $remote_addr});
1799 } elsif ( $userid ) {
1800 $session->param( 'lasttime', time() );
1801 my $patron = Koha::Patrons->find({ userid => $userid });
1803 # If the user modify their own userid
1804 # Better than 500 but we could do better
1805 unless ( $patron ) {
1808 return ("expired", undef);
1811 $patron = Koha::Patrons->find({ cardnumber => $userid })
1813 return ("password_expired", undef ) if $patron->password_expired;
1814 my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1;
1816 C4::Context->_new_userenv($sessionID);
1817 if ( !C4::Context->interface ) {
1818 # No need to override the interface, most often set by get_template_and_user
1819 C4::Context->interface( $session->param('interface') );
1821 C4::Context->set_userenv(
1822 $session->param('number'), $session->param('id') // '',
1823 $session->param('cardnumber'), $session->param('firstname'),
1824 $session->param('surname'), $session->param('branch'),
1825 $session->param('branchname'), $session->param('flags'),
1826 $session->param('emailaddress'), $session->param('shibboleth'),
1827 $session->param('desk_id'), $session->param('desk_name'),
1828 $session->param('register_id'), $session->param('register_name')
1830 if ( C4::Context->preference('TwoFactorAuthentication') ne 'disabled' ) {
1831 return ( "additional-auth-needed", $session )
1832 if $session->param('waiting-for-2FA');
1834 return ( "setup-additional-auth-needed", $session )
1835 if $session->param('waiting-for-2FA-setup');
1838 return ( "ok", $session );
1842 return ( "failed", undef );
1846 C4::Context->_new_userenv($sessionID);
1847 C4::Context->interface($session->param('interface'));
1848 C4::Context->set_userenv( undef, q{} );
1849 return ( "anon", $session );
1852 return ( "expired", undef );
1859 my $session = get_session($sessionID);
1861 Given a session ID, retrieve the CGI::Session object used to store
1862 the session's state. The session object can be used to store
1863 data that needs to be accessed by different scripts during a
1866 If the C<$sessionID> parameter is an empty string, a new session
1871 #NOTE: We're keeping this for backwards compatibility
1872 sub _get_session_params {
1873 return Koha::Session->_get_session_params();
1876 #NOTE: We're keeping this for backwards compatibility
1878 my $sessionID = shift;
1879 my $session = Koha::Session->get_session( { sessionID => $sessionID } );
1883 =head2 create_basic_session
1885 my $session = create_basic_session({ patron => $patron, interface => $interface });
1887 Creates a session and adds all basic parameters for a session to work
1891 sub create_basic_session {
1893 my $patron = $params->{patron};
1894 my $interface = $params->{interface};
1896 $interface = 'intranet' if $interface eq 'staff';
1898 my $session = get_session("");
1900 $session->param( 'number', $patron->borrowernumber );
1901 $session->param( 'id', $patron->userid );
1902 $session->param( 'cardnumber', $patron->cardnumber );
1903 $session->param( 'firstname', $patron->firstname );
1904 $session->param( 'surname', $patron->surname );
1905 $session->param( 'branch', $patron->branchcode );
1906 $session->param( 'branchname', $patron->library->branchname );
1907 $session->param( 'flags', $patron->flags );
1908 $session->param( 'emailaddress', $patron->email );
1909 $session->param( 'ip', $session->remote_addr() );
1910 $session->param( 'lasttime', time() );
1911 $session->param( 'interface', $interface);
1917 # FIXME no_set_userenv may be replaced with force_branchcode_for_userenv
1918 # (or something similar)
1919 # Currently it's only passed from C4::SIP::ILS::Patron::check_password, but
1920 # not having a userenv defined could cause a crash.
1922 my ( $userid, $password, $query, $type, $no_set_userenv ) = @_;
1923 $type = 'opac' unless $type;
1925 # Get shibboleth login attribute
1926 my $shib = C4::Context->config('useshibboleth') && shib_ok();
1927 my $shib_login = $shib ? get_login_shib() : undef;
1931 if ( defined $userid ) {
1932 $patron = Koha::Patrons->find( { userid => $userid } );
1933 $patron = Koha::Patrons->find( { cardnumber => $userid } ) unless $patron;
1935 my $check_internal_as_fallback = 0;
1938 # Note: checkpw_* routines returns:
1941 # -1 if user bind failed (LDAP only)
1943 if ( $patron and ( $patron->account_locked ) ) {
1945 # Nothing to check, account is locked
1946 } elsif ( $ldap && defined($password) ) {
1947 my ( $retval, $retcard, $retuserid );
1948 ( $retval, $retcard, $retuserid, $patron ) = checkpw_ldap(@_); # EXTERNAL AUTH
1949 if ( $retval == 1 ) {
1950 @return = ( $retval, $retcard, $retuserid, $patron );
1953 $check_internal_as_fallback = 1 if $retval == 0;
1955 } elsif ( $cas && $query && $query->param('ticket') ) {
1957 # In case of a CAS authentication, we use the ticket instead of the password
1958 my $ticket = $query->param('ticket');
1959 $query->delete('ticket'); # remove ticket to come back to original URL
1960 my ( $retval, $retcard, $retuserid, $cas_ticket, $patron ) =
1961 checkpw_cas( $ticket, $query, $type ); # EXTERNAL AUTH
1963 @return = ( $retval, $retcard, $retuserid, $patron, $cas_ticket );
1967 $passwd_ok = $retval;
1970 # If we are in a shibboleth session (shibboleth is enabled, and a shibboleth match attribute is present)
1971 # Check for password to asertain whether we want to be testing against shibboleth or another method this
1973 elsif ( $shib && $shib_login && !$password ) {
1975 # In case of a Shibboleth authentication, we expect a shibboleth user attribute
1976 # (defined under shibboleth mapping in koha-conf.xml) to contain the login of the
1977 # shibboleth-authenticated user
1979 # Then, we check if it matches a valid koha user
1981 my ( $retval, $retcard, $retuserid, $patron ) =
1982 C4::Auth_with_shibboleth::checkpw_shib($shib_login); # EXTERNAL AUTH
1984 @return = ( $retval, $retcard, $retuserid, $patron );
1986 $passwd_ok = $retval;
1989 $check_internal_as_fallback = 1;
1993 if ($check_internal_as_fallback) {
1994 @return = checkpw_internal( $userid, $password, $no_set_userenv );
1995 push( @return, $patron );
1996 $passwd_ok = 1 if $return[0] > 0; # 1 or 2
2001 $patron->update( { login_attempts => 0 } );
2002 if ( $patron->password_expired ) {
2003 @return = ( -2, $patron );
2005 } elsif ( !$patron->account_locked ) {
2006 $patron->update( { login_attempts => $patron->login_attempts + 1 } );
2010 # Optionally log success or failure
2011 if ( $patron && $passwd_ok && C4::Context->preference('AuthSuccessLog') ) {
2012 logaction( 'AUTH', 'SUCCESS', $patron->id, "Valid password for $userid", $type );
2013 } elsif ( !$passwd_ok && C4::Context->preference('AuthFailureLog') ) {
2014 logaction( 'AUTH', 'FAILURE', $patron ? $patron->id : 0, "Wrong password for $userid", $type );
2020 sub checkpw_internal {
2021 my ( $userid, $password, $no_set_userenv ) = @_;
2023 $password = Encode::encode( 'UTF-8', $password )
2024 if Encode::is_utf8($password);
2026 my $dbh = C4::Context->dbh;
2029 "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where userid=?"
2031 $sth->execute($userid);
2033 my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
2034 $surname, $branchcode, $branchname, $flags )
2037 if ( checkpw_hash( $password, $stored_hash ) ) {
2039 C4::Context->set_userenv( "$borrowernumber", $userid, $cardnumber,
2040 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
2041 return 1, $cardnumber, $userid;
2046 "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
2048 $sth->execute($userid);
2050 my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
2051 $surname, $branchcode, $branchname, $flags )
2054 if ( checkpw_hash( $password, $stored_hash ) ) {
2056 C4::Context->set_userenv( $borrowernumber, $userid, $cardnumber,
2057 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
2058 return 1, $cardnumber, $userid;
2065 my ( $password, $stored_hash ) = @_;
2067 return if $stored_hash eq '!';
2069 # check what encryption algorithm was implemented: Bcrypt - if the hash starts with '$2' it is Bcrypt else md5
2071 if ( substr( $stored_hash, 0, 2 ) eq '$2' ) {
2072 $hash = hash_password( $password, $stored_hash );
2074 $hash = md5_base64($password);
2076 return $hash eq $stored_hash;
2081 my $authflags = getuserflags($flags, $userid, [$dbh]);
2083 Translates integer flags into permissions strings hash.
2085 C<$flags> is the integer userflags value ( borrowers.userflags )
2086 C<$userid> is the members.userid, used for building subpermissions
2087 C<$authflags> is a hashref of permissions
2094 my $dbh = @_ ? shift : C4::Context->dbh;
2097 # I don't want to do this, but if someone logs in as the database
2098 # user, it would be preferable not to spam them to death with
2099 # numeric warnings. So, we make $flags numeric.
2100 no warnings 'numeric';
2103 my $sth = $dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
2106 while ( my ( $bit, $flag, $defaulton ) = $sth->fetchrow ) {
2107 if ( ( $flags & ( 2**$bit ) ) || $defaulton ) {
2108 $userflags->{$flag} = 1;
2111 $userflags->{$flag} = 0;
2115 # get subpermissions and merge with top-level permissions
2116 my $user_subperms = get_user_subpermissions($userid);
2117 foreach my $module ( keys %$user_subperms ) {
2118 next if $userflags->{$module} == 1; # user already has permission for everything in this module
2119 $userflags->{$module} = $user_subperms->{$module};
2125 =head2 get_user_subpermissions
2127 $user_perm_hashref = get_user_subpermissions($userid);
2129 Given the userid (note, not the borrowernumber) of a staff user,
2130 return a hashref of hashrefs of the specific subpermissions
2131 accorded to the user. An example return is
2135 export_catalog => 1,
2136 import_patrons => 1,
2140 The top-level hash-key is a module or function code from
2141 userflags.flag, while the second-level key is a code
2144 The results of this function do not give a complete picture
2145 of the functions that a staff user can access; it is also
2146 necessary to check borrowers.flags.
2150 sub get_user_subpermissions {
2153 my $dbh = C4::Context->dbh;
2154 my $sth = $dbh->prepare( "SELECT flag, user_permissions.code
2155 FROM user_permissions
2156 JOIN permissions USING (module_bit, code)
2157 JOIN userflags ON (module_bit = bit)
2158 JOIN borrowers USING (borrowernumber)
2159 WHERE userid = ?" );
2160 $sth->execute($userid);
2162 my $user_perms = {};
2163 while ( my $perm = $sth->fetchrow_hashref ) {
2164 $user_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2169 =head2 get_all_subpermissions
2171 my $perm_hashref = get_all_subpermissions();
2173 Returns a hashref of hashrefs defining all specific
2174 permissions currently defined. The return value
2175 has the same structure as that of C<get_user_subpermissions>,
2176 except that the innermost hash value is the description
2177 of the subpermission.
2181 sub get_all_subpermissions {
2182 my $dbh = C4::Context->dbh;
2183 my $sth = $dbh->prepare( "SELECT flag, code
2185 JOIN userflags ON (module_bit = bit)" );
2189 while ( my $perm = $sth->fetchrow_hashref ) {
2190 $all_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2195 =head2 get_cataloguing_page_permissions
2197 my $required_permissions = get_cataloguing_page_permissions();
2199 Returns the required permissions to access the main cataloguing page. Useful for building
2200 the global I<can_see_cataloguing_module> template variable, and also for reusing in
2201 I<cataloging-home.pl>.
2205 sub get_cataloguing_page_permissions {
2207 my @cataloguing_tools_subperms = qw(
2214 marc_modification_templates
2222 { editcatalogue => '*' }, { tools => \@cataloguing_tools_subperms },
2223 C4::Context->preference('StockRotation') ? { stockrotation => 'manage_rotas' } : ()
2227 =head2 haspermission
2229 $flagsrequired = '*'; # Any permission at all
2230 $flagsrequired = 'a_flag'; # a_flag must be satisfied (all subpermissions)
2231 $flagsrequired = [ 'a_flag', 'b_flag' ]; # a_flag OR b_flag must be satisfied
2232 $flagsrequired = { 'a_flag => 1, 'b_flag' => 1 }; # a_flag AND b_flag must be satisfied
2233 $flagsrequired = { 'a_flag' => 'sub_a' }; # sub_a of a_flag must be satisfied
2234 $flagsrequired = { 'a_flag' => [ 'sub_a, 'sub_b' ] }; # sub_a OR sub_b of a_flag must be satisfied
2235 $flagsrequired = { 'a_flag' => { 'sub_a' => 1, 'sub_b' => 1 } }; # sub_a AND sub_b of a_flag must be satisfied
2237 $flags = ($userid, $flagsrequired);
2239 C<$userid> the userid of the member
2240 C<$flags> is a query structure similar to that used by SQL::Abstract that
2241 denotes the combination of flags required. It is a required parameter.
2243 The main logic of this method is that things in arrays are OR'ed, and things
2244 in hashes are AND'ed. The `*` character can be used, at any depth, to denote `ANY`
2246 Returns member's flags or 0 if a permission is not met.
2251 my ($required, $flags) = @_;
2253 my $ref = ref($required);
2255 if ($required eq '*') {
2256 return 0 unless ( $flags or ref( $flags ) );
2258 return 0 unless ( $flags and (!ref( $flags ) || $flags->{$required} ));
2260 } elsif ($ref eq 'HASH') {
2261 foreach my $key (keys %{$required}) {
2262 next if $flags == 1;
2263 my $require = $required->{$key};
2264 my $rflags = $flags->{$key};
2265 return 0 unless _dispatch($require, $rflags);
2267 } elsif ($ref eq 'ARRAY') {
2269 foreach my $require ( @{$required} ) {
2271 ( ref($flags) && !ref($require) && ( $require ne '*' ) )
2272 ? $flags->{$require}
2274 $satisfied++ if _dispatch( $require, $rflags );
2276 return 0 unless $satisfied;
2278 croak "Unexpected structure found: $ref";
2285 my ( $userid, $flagsrequired ) = @_;
2287 #Koha::Exceptions::WrongParameter->throw('$flagsrequired should not be undef')
2288 # unless defined($flagsrequired);
2290 my $sth = C4::Context->dbh->prepare("SELECT flags FROM borrowers WHERE userid=?");
2291 $sth->execute($userid);
2292 my $row = $sth->fetchrow();
2293 my $flags = getuserflags( $row, $userid );
2295 return $flags unless defined($flagsrequired);
2296 return $flags if $flags->{superlibrarian};
2297 return _dispatch($flagsrequired, $flags);
2299 #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
2304 $flags = ($iprange);
2306 C<$iprange> A space separated string describing an IP range. Can include single IPs or ranges
2308 Returns 1 if the remote address is in the provided iprange, or 0 otherwise.
2315 my @allowedipranges = $iprange ? split(' ', $iprange) : ();
2316 if (scalar @allowedipranges > 0) {
2318 eval { @rangelist = Net::CIDR::range2cidr(@allowedipranges); }; return 0 if $@;
2319 eval { $result = Net::CIDR::cidrlookup($ENV{'REMOTE_ADDR'}, @rangelist) } || Koha::Logger->get->warn('cidrlookup failed for ' . join(' ',@rangelist) );
2321 return $result ? 1 : 0;
2324 sub getborrowernumber {
2326 my $userenv = C4::Context->userenv;
2327 if ( defined($userenv) && ref($userenv) eq 'HASH' && $userenv->{number} ) {
2328 return $userenv->{number};
2330 my $dbh = C4::Context->dbh;
2331 for my $field ( 'userid', 'cardnumber' ) {
2333 $dbh->prepare("select borrowernumber from borrowers where $field=?");
2334 $sth->execute($userid);
2336 my ($bnumber) = $sth->fetchrow;
2343 =head2 track_login_daily
2345 track_login_daily( $userid );
2347 Wraps the call to $patron->track_login, the method used to update borrowers.lastseen. We only call track_login once a day.
2351 sub track_login_daily {
2353 return if !$userid || !C4::Context->preference('TrackLastPatronActivity');
2355 my $cache = Koha::Caches->get_instance();
2356 my $cache_key = "track_login_" . $userid;
2357 my $cached = $cache->get_from_cache($cache_key);
2358 my $today = dt_from_string()->ymd;
2359 return if $cached && $cached eq $today;
2361 my $patron = Koha::Patrons->find({ userid => $userid });
2362 return unless $patron;
2363 $patron->track_login;
2364 $cache->set_in_cache( $cache_key, $today );
2367 END { } # module clean-up code here (global destructor)
2377 Crypt::Eksblowfish::Bcrypt(3)