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 under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 use Digest::MD5 qw(md5_base64);
23 use JSON qw/encode_json decode_json/;
26 use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);
27 use Fcntl qw/O_RDONLY/; # O_RDONLY is used in generate_salt
31 use C4::Templates; # to get the template
32 use C4::Branch; # GetBranches
33 use C4::VirtualShelves;
34 use POSIX qw/strftime/;
35 use List::MoreUtils qw/ any /;
38 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug $ldap $cas $caslogout);
41 sub psgi_env { any { /^psgi\./ } keys %ENV }
43 if ( psgi_env ) { die 'psgi:exit' }
46 $VERSION = 3.07.00.049; # set version for version checking
50 @EXPORT = qw(&checkauth &get_template_and_user &haspermission &get_user_subpermissions);
51 @EXPORT_OK = qw(&check_api_auth &get_session &check_cookie_auth &checkpw &checkpw_internal &checkpw_hash
52 &get_all_subpermissions &get_user_subpermissions
53 ParseSearchHistoryCookie hash_password
55 %EXPORT_TAGS = ( EditPermissions => [qw(get_all_subpermissions get_user_subpermissions)] );
56 $ldap = C4::Context->config('useldapserver') || 0;
57 $cas = C4::Context->preference('casAuthentication');
58 $caslogout = C4::Context->preference('casLogout');
59 require C4::Auth_with_cas; # no import
61 require C4::Auth_with_ldap;
62 import C4::Auth_with_ldap qw(checkpw_ldap);
65 import C4::Auth_with_cas qw(check_api_auth_cas checkpw_cas login_cas logout_cas login_cas_url);
72 C4::Auth - Authenticates Koha users
82 my ($template, $borrowernumber, $cookie)
83 = get_template_and_user(
85 template_name => "opac-main.tmpl",
89 flagsrequired => {borrow => 1, catalogue => '*', tools => 'import_patrons' },
93 output_html_with_http_headers $query, $cookie, $template->output;
97 The main function of this module is to provide
98 authentification. However the get_template_and_user function has
99 been provided so that a users login information is passed along
100 automatically. This gets loaded into the template.
104 =head2 get_template_and_user
106 my ($template, $borrowernumber, $cookie)
107 = get_template_and_user(
109 template_name => "opac-main.tmpl",
112 authnotrequired => 1,
113 flagsrequired => {borrow => 1, catalogue => '*', tools => 'import_patrons' },
117 This call passes the C<query>, C<flagsrequired> and C<authnotrequired>
118 to C<&checkauth> (in this module) to perform authentification.
119 See C<&checkauth> for an explanation of these parameters.
121 The C<template_name> is then used to find the correct template for
122 the page. The authenticated users details are loaded onto the
123 template in the HTML::Template LOOP variable C<USER_INFO>. Also the
124 C<sessionID> is passed to the template. This can be used in templates
125 if cookies are disabled. It needs to be put as and input to every
128 More information on the C<gettemplate> sub can be found in the
133 my $SEARCH_HISTORY_INSERT_SQL =<<EOQ;
134 INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, total, time )
135 VALUES ( ?, ?, ?, ?, ?, FROM_UNIXTIME(?))
138 sub get_template_and_user {
141 C4::Templates::gettemplate( $in->{'template_name'}, $in->{'type'}, $in->{'query'}, $in->{'is_plugin'} );
142 my ( $user, $cookie, $sessionID, $flags );
143 if ( $in->{'template_name'} !~m/maintenance/ ) {
144 ( $user, $cookie, $sessionID, $flags ) = checkauth(
146 $in->{'authnotrequired'},
147 $in->{'flagsrequired'},
155 # It's possible for $user to be the borrowernumber if they don't have a
156 # userid defined (and are logging in through some other method, such
157 # as SSL certs against an email address)
158 $borrowernumber = getborrowernumber($user) if defined($user);
159 if (!defined($borrowernumber) && defined($user)) {
160 my $borrower = C4::Members::GetMember(borrowernumber => $user);
162 $borrowernumber = $user;
163 # A bit of a hack, but I don't know there's a nicer way
165 $user = $borrower->{firstname} . ' ' . $borrower->{surname};
170 $template->param( loggedinusername => $user );
171 $template->param( sessionID => $sessionID );
173 my ($total, $pubshelves, $barshelves) = C4::VirtualShelves::GetSomeShelfNames($borrowernumber, 'MASTHEAD');
175 pubshelves => $total->{pubtotal},
176 pubshelvesloop => $pubshelves,
177 barshelves => $total->{bartotal},
178 barshelvesloop => $barshelves,
181 my ( $borr ) = C4::Members::GetMemberDetails( $borrowernumber );
184 $template->param( "USER_INFO" => \@bordat );
186 my $all_perms = get_all_subpermissions();
188 my @flagroots = qw(circulate catalogue parameters borrowers permissions reserveforothers borrow
189 editcatalogue updatecharges management tools editauthorities serials reports acquisition);
190 # We are going to use the $flags returned by checkauth
191 # to create the template's parameters that will indicate
192 # which menus the user can access.
193 if ( $flags && $flags->{superlibrarian}==1 ) {
194 $template->param( CAN_user_circulate => 1 );
195 $template->param( CAN_user_catalogue => 1 );
196 $template->param( CAN_user_parameters => 1 );
197 $template->param( CAN_user_borrowers => 1 );
198 $template->param( CAN_user_permissions => 1 );
199 $template->param( CAN_user_reserveforothers => 1 );
200 $template->param( CAN_user_borrow => 1 );
201 $template->param( CAN_user_editcatalogue => 1 );
202 $template->param( CAN_user_updatecharges => 1 );
203 $template->param( CAN_user_acquisition => 1 );
204 $template->param( CAN_user_management => 1 );
205 $template->param( CAN_user_tools => 1 );
206 $template->param( CAN_user_editauthorities => 1 );
207 $template->param( CAN_user_serials => 1 );
208 $template->param( CAN_user_reports => 1 );
209 $template->param( CAN_user_staffaccess => 1 );
210 $template->param( CAN_user_plugins => 1 );
211 $template->param( CAN_user_coursereserves => 1 );
212 foreach my $module (keys %$all_perms) {
213 foreach my $subperm (keys %{ $all_perms->{$module} }) {
214 $template->param( "CAN_user_${module}_${subperm}" => 1 );
220 foreach my $module (keys %$all_perms) {
221 if ( $flags->{$module} == 1) {
222 foreach my $subperm (keys %{ $all_perms->{$module} }) {
223 $template->param( "CAN_user_${module}_${subperm}" => 1 );
225 } elsif ( ref($flags->{$module}) ) {
226 foreach my $subperm (keys %{ $flags->{$module} } ) {
227 $template->param( "CAN_user_${module}_${subperm}" => 1 );
234 foreach my $module (keys %$flags) {
235 if ( $flags->{$module} == 1 or ref($flags->{$module}) ) {
236 $template->param( "CAN_user_$module" => 1 );
237 if ($module eq "parameters") {
238 $template->param( CAN_user_management => 1 );
243 # Logged-in opac search history
244 # If the requested template is an opac one and opac search history is enabled
245 if ($in->{type} eq 'opac' && C4::Context->preference('EnableOpacSearchHistory')) {
246 my $dbh = C4::Context->dbh;
247 my $query = "SELECT COUNT(*) FROM search_history WHERE userid=?";
248 my $sth = $dbh->prepare($query);
249 $sth->execute($borrowernumber);
251 # If at least one search has already been performed
252 if ($sth->fetchrow_array > 0) {
253 # We show the link in opac
254 $template->param(ShowOpacRecentSearchLink => 1);
257 # And if there's a cookie with searches performed when the user was not logged in,
258 # we add them to the logged-in search history
259 my @recentSearches = ParseSearchHistoryCookie($in->{'query'});
260 if (@recentSearches) {
261 my $sth = $dbh->prepare($SEARCH_HISTORY_INSERT_SQL);
262 $sth->execute( $borrowernumber,
263 $in->{'query'}->cookie("CGISESSID"),
268 ) foreach @recentSearches;
270 # And then, delete the cookie's content
271 my $newsearchcookie = $in->{'query'}->cookie(
272 -name => 'KohaOpacRecentSearches',
273 -value => encode_json([]),
277 $cookie = [$cookie, $newsearchcookie];
281 else { # if this is an anonymous session, setup to display public lists...
283 $template->param( sessionID => $sessionID );
285 my ($total, $pubshelves) = C4::VirtualShelves::GetSomeShelfNames(undef, 'MASTHEAD');
287 pubshelves => $total->{pubtotal},
288 pubshelvesloop => $pubshelves,
291 # Anonymous opac search history
292 # If opac search history is enabled and at least one search has already been performed
293 if (C4::Context->preference('EnableOpacSearchHistory')) {
294 my @recentSearches = ParseSearchHistoryCookie($in->{'query'});
295 if (@recentSearches) {
296 $template->param(ShowOpacRecentSearchLink => 1);
300 if(C4::Context->preference('dateformat')){
301 $template->param(dateformat => C4::Context->preference('dateformat'))
304 # these template parameters are set the same regardless of $in->{'type'}
306 "BiblioDefaultView".C4::Context->preference("BiblioDefaultView") => 1,
307 EnhancedMessagingPreferences => C4::Context->preference('EnhancedMessagingPreferences'),
308 GoogleJackets => C4::Context->preference("GoogleJackets"),
309 OpenLibraryCovers => C4::Context->preference("OpenLibraryCovers"),
310 KohaAdminEmailAddress => "" . C4::Context->preference("KohaAdminEmailAddress"),
311 LoginBranchcode => (C4::Context->userenv?C4::Context->userenv->{"branch"}:undef),
312 LoginFirstname => (C4::Context->userenv?C4::Context->userenv->{"firstname"}:"Bel"),
313 LoginSurname => C4::Context->userenv?C4::Context->userenv->{"surname"}:"Inconnu",
314 emailaddress => C4::Context->userenv?C4::Context->userenv->{"emailaddress"}:undef,
315 loggedinpersona => C4::Context->userenv?C4::Context->userenv->{"persona"}:undef,
316 TagsEnabled => C4::Context->preference("TagsEnabled"),
317 hide_marc => C4::Context->preference("hide_marc"),
318 item_level_itypes => C4::Context->preference('item-level_itypes'),
319 patronimages => C4::Context->preference("patronimages"),
320 singleBranchMode => C4::Context->preference("singleBranchMode"),
321 XSLTDetailsDisplay => C4::Context->preference("XSLTDetailsDisplay"),
322 XSLTResultsDisplay => C4::Context->preference("XSLTResultsDisplay"),
323 using_https => $in->{'query'}->https() ? 1 : 0,
324 noItemTypeImages => C4::Context->preference("noItemTypeImages"),
325 marcflavour => C4::Context->preference("marcflavour"),
326 persona => C4::Context->preference("persona"),
328 if ( $in->{'type'} eq "intranet" ) {
330 AmazonCoverImages => C4::Context->preference("AmazonCoverImages"),
331 AutoLocation => C4::Context->preference("AutoLocation"),
332 "BiblioDefaultView".C4::Context->preference("IntranetBiblioDefaultView") => 1,
333 CalendarFirstDayOfWeek => (C4::Context->preference("CalendarFirstDayOfWeek") eq "Sunday")?0:1,
334 CircAutocompl => C4::Context->preference("CircAutocompl"),
335 FRBRizeEditions => C4::Context->preference("FRBRizeEditions"),
336 IndependentBranches => C4::Context->preference("IndependentBranches"),
337 IntranetNav => C4::Context->preference("IntranetNav"),
338 IntranetmainUserblock => C4::Context->preference("IntranetmainUserblock"),
339 LibraryName => C4::Context->preference("LibraryName"),
340 LoginBranchname => (C4::Context->userenv?C4::Context->userenv->{"branchname"}:undef),
341 advancedMARCEditor => C4::Context->preference("advancedMARCEditor"),
342 canreservefromotherbranches => C4::Context->preference('canreservefromotherbranches'),
343 intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),
344 IntranetFavicon => C4::Context->preference("IntranetFavicon"),
345 intranetreadinghistory => C4::Context->preference("intranetreadinghistory"),
346 intranetstylesheet => C4::Context->preference("intranetstylesheet"),
347 IntranetUserCSS => C4::Context->preference("IntranetUserCSS"),
348 intranetuserjs => C4::Context->preference("intranetuserjs"),
349 intranetbookbag => C4::Context->preference("intranetbookbag"),
350 suggestion => C4::Context->preference("suggestion"),
351 virtualshelves => C4::Context->preference("virtualshelves"),
352 StaffSerialIssueDisplayCount => C4::Context->preference("StaffSerialIssueDisplayCount"),
353 EasyAnalyticalRecords => C4::Context->preference('EasyAnalyticalRecords'),
354 LocalCoverImages => C4::Context->preference('LocalCoverImages'),
355 OPACLocalCoverImages => C4::Context->preference('OPACLocalCoverImages'),
356 AllowMultipleCovers => C4::Context->preference('AllowMultipleCovers'),
357 EnableBorrowerFiles => C4::Context->preference('EnableBorrowerFiles'),
358 UseKohaPlugins => C4::Context->preference('UseKohaPlugins'),
359 UseCourseReserves => C4::Context->preference("UseCourseReserves"),
363 warn "template type should be OPAC, here it is=[" . $in->{'type'} . "]" unless ( $in->{'type'} eq 'opac' );
364 #TODO : replace LibraryName syspref with 'system name', and remove this html processing
365 my $LibraryNameTitle = C4::Context->preference("LibraryName");
366 $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
367 $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
368 # clean up the busc param in the session if the page is not opac-detail
369 if (C4::Context->preference("OpacBrowseResults") && $in->{'template_name'} =~ /opac-(.+)\.(?:tt|tmpl)$/ && $1 !~ /^(?:MARC|ISBD)?detail$/) {
370 my $sessionSearch = get_session($sessionID || $in->{'query'}->cookie("CGISESSID"));
371 $sessionSearch->clear(["busc"]) if ($sessionSearch->param("busc"));
373 # variables passed from CGI: opac_css_override and opac_search_limits.
374 my $opac_search_limit = $ENV{'OPAC_SEARCH_LIMIT'};
375 my $opac_limit_override = $ENV{'OPAC_LIMIT_OVERRIDE'};
377 if (($opac_search_limit && $opac_search_limit =~ /branch:(\w+)/ && $opac_limit_override) || ($in->{'query'}->param('limit') && $in->{'query'}->param('limit') =~ /branch:(\w+)/)){
378 $opac_name = $1; # opac_search_limit is a branch, so we use it.
379 } elsif ( $in->{'query'}->param('multibranchlimit') ) {
380 $opac_name = $in->{'query'}->param('multibranchlimit');
381 } elsif (C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv && C4::Context->userenv->{'branch'}) {
382 $opac_name = C4::Context->userenv->{'branch'};
385 opaccolorstylesheet => C4::Context->preference("opaccolorstylesheet"),
386 AnonSuggestions => "" . C4::Context->preference("AnonSuggestions"),
387 AuthorisedValueImages => C4::Context->preference("AuthorisedValueImages"),
388 BranchesLoop => GetBranchesLoop($opac_name),
389 BranchCategoriesLoop => GetBranchCategories( 'searchdomain', 1, $opac_name ),
390 CalendarFirstDayOfWeek => (C4::Context->preference("CalendarFirstDayOfWeek") eq "Sunday")?0:1,
391 LibraryName => "" . C4::Context->preference("LibraryName"),
392 LibraryNameTitle => "" . $LibraryNameTitle,
393 LoginBranchname => C4::Context->userenv?C4::Context->userenv->{"branchname"}:"",
394 OPACAmazonCoverImages => C4::Context->preference("OPACAmazonCoverImages"),
395 OPACFRBRizeEditions => C4::Context->preference("OPACFRBRizeEditions"),
396 OpacHighlightedWords => C4::Context->preference("OpacHighlightedWords"),
397 OPACItemHolds => C4::Context->preference("OPACItemHolds"),
398 OPACShelfBrowser => "". C4::Context->preference("OPACShelfBrowser"),
399 OPACURLOpenInNewWindow => "" . C4::Context->preference("OPACURLOpenInNewWindow"),
400 OPACUserCSS => "". C4::Context->preference("OPACUserCSS"),
401 OPACMobileUserCSS => "". C4::Context->preference("OPACMobileUserCSS"),
402 OPACViewOthersSuggestions => "" . C4::Context->preference("OPACViewOthersSuggestions"),
403 OpacAuthorities => C4::Context->preference("OpacAuthorities"),
404 OPACBaseURL => ($in->{'query'}->https() ? "https://" : "http://") . $ENV{'SERVER_NAME'} .
405 ($ENV{'SERVER_PORT'} eq ($in->{'query'}->https() ? "443" : "80") ? '' : ":$ENV{'SERVER_PORT'}"),
406 opac_css_override => $ENV{'OPAC_CSS_OVERRIDE'},
407 opac_search_limit => $opac_search_limit,
408 opac_limit_override => $opac_limit_override,
409 OpacBrowser => C4::Context->preference("OpacBrowser"),
410 OpacCloud => C4::Context->preference("OpacCloud"),
411 OpacKohaUrl => C4::Context->preference("OpacKohaUrl"),
412 OpacMainUserBlock => "" . C4::Context->preference("OpacMainUserBlock"),
413 OpacMainUserBlockMobile => "" . C4::Context->preference("OpacMainUserBlockMobile"),
414 OpacShowFiltersPulldownMobile => C4::Context->preference("OpacShowFiltersPulldownMobile"),
415 OpacShowLibrariesPulldownMobile => C4::Context->preference("OpacShowLibrariesPulldownMobile"),
416 OpacNav => "" . C4::Context->preference("OpacNav"),
417 OpacNavRight => "" . C4::Context->preference("OpacNavRight"),
418 OpacNavBottom => "" . C4::Context->preference("OpacNavBottom"),
419 OpacPasswordChange => C4::Context->preference("OpacPasswordChange"),
420 OPACPatronDetails => C4::Context->preference("OPACPatronDetails"),
421 OPACPrivacy => C4::Context->preference("OPACPrivacy"),
422 OPACFinesTab => C4::Context->preference("OPACFinesTab"),
423 OpacTopissue => C4::Context->preference("OpacTopissue"),
424 RequestOnOpac => C4::Context->preference("RequestOnOpac"),
425 'Version' => C4::Context->preference('Version'),
426 hidelostitems => C4::Context->preference("hidelostitems"),
427 mylibraryfirst => (C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv) ? C4::Context->userenv->{'branch'} : '',
428 opaclayoutstylesheet => "" . C4::Context->preference("opaclayoutstylesheet"),
429 opacbookbag => "" . C4::Context->preference("opacbookbag"),
430 opaccredits => "" . C4::Context->preference("opaccredits"),
431 OpacFavicon => C4::Context->preference("OpacFavicon"),
432 opacheader => "" . C4::Context->preference("opacheader"),
433 opaclanguagesdisplay => "" . C4::Context->preference("opaclanguagesdisplay"),
434 opacreadinghistory => C4::Context->preference("opacreadinghistory"),
435 opacsmallimage => "" . C4::Context->preference("opacsmallimage"),
436 opacuserjs => C4::Context->preference("opacuserjs"),
437 opacuserlogin => "" . C4::Context->preference("opacuserlogin"),
438 ShowReviewer => C4::Context->preference("ShowReviewer"),
439 ShowReviewerPhoto => C4::Context->preference("ShowReviewerPhoto"),
440 suggestion => "" . C4::Context->preference("suggestion"),
441 virtualshelves => "" . C4::Context->preference("virtualshelves"),
442 OPACSerialIssueDisplayCount => C4::Context->preference("OPACSerialIssueDisplayCount"),
443 OPACXSLTDetailsDisplay => C4::Context->preference("OPACXSLTDetailsDisplay"),
444 OPACXSLTResultsDisplay => C4::Context->preference("OPACXSLTResultsDisplay"),
445 SyndeticsClientCode => C4::Context->preference("SyndeticsClientCode"),
446 SyndeticsEnabled => C4::Context->preference("SyndeticsEnabled"),
447 SyndeticsCoverImages => C4::Context->preference("SyndeticsCoverImages"),
448 SyndeticsTOC => C4::Context->preference("SyndeticsTOC"),
449 SyndeticsSummary => C4::Context->preference("SyndeticsSummary"),
450 SyndeticsEditions => C4::Context->preference("SyndeticsEditions"),
451 SyndeticsExcerpt => C4::Context->preference("SyndeticsExcerpt"),
452 SyndeticsReviews => C4::Context->preference("SyndeticsReviews"),
453 SyndeticsAuthorNotes => C4::Context->preference("SyndeticsAuthorNotes"),
454 SyndeticsAwards => C4::Context->preference("SyndeticsAwards"),
455 SyndeticsSeries => C4::Context->preference("SyndeticsSeries"),
456 SyndeticsCoverImageSize => C4::Context->preference("SyndeticsCoverImageSize"),
457 OPACLocalCoverImages => C4::Context->preference("OPACLocalCoverImages"),
458 PatronSelfRegistration => C4::Context->preference("PatronSelfRegistration"),
459 PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
462 $template->param(OpacPublic => '1') if ($user || C4::Context->preference("OpacPublic"));
465 # Check if we were asked using parameters to force a specific language
466 if ( defined $in->{'query'}->param('language') ) {
467 # Extract the language, let C4::Templates::getlanguage choose
469 my $language = C4::Templates::getlanguage($in->{'query'},$in->{'type'});
470 my $languagecookie = C4::Templates::getlanguagecookie($in->{'query'},$language);
471 $cookie = [$cookie, $languagecookie];
474 return ( $template, $borrowernumber, $cookie, $flags);
479 ($userid, $cookie, $sessionID) = &checkauth($query, $noauth, $flagsrequired, $type);
481 Verifies that the user is authorized to run this script. If
482 the user is authorized, a (userid, cookie, session-id, flags)
483 quadruple is returned. If the user is not authorized but does
484 not have the required privilege (see $flagsrequired below), it
485 displays an error page and exits. Otherwise, it displays the
486 login page and exits.
488 Note that C<&checkauth> will return if and only if the user
489 is authorized, so it should be called early on, before any
490 unfinished operations (e.g., if you've opened a file, then
491 C<&checkauth> won't close it for you).
493 C<$query> is the CGI object for the script calling C<&checkauth>.
495 The C<$noauth> argument is optional. If it is set, then no
496 authorization is required for the script.
498 C<&checkauth> fetches user and session information from C<$query> and
499 ensures that the user is authorized to run scripts that require
502 The C<$flagsrequired> argument specifies the required privileges
503 the user must have if the username and password are correct.
504 It should be specified as a reference-to-hash; keys in the hash
505 should be the "flags" for the user, as specified in the Members
506 intranet module. Any key specified must correspond to a "flag"
507 in the userflags table. E.g., { circulate => 1 } would specify
508 that the user must have the "circulate" privilege in order to
509 proceed. To make sure that access control is correct, the
510 C<$flagsrequired> parameter must be specified correctly.
512 Koha also has a concept of sub-permissions, also known as
513 granular permissions. This makes the value of each key
514 in the C<flagsrequired> hash take on an additional
519 The user must have access to all subfunctions of the module
520 specified by the hash key.
524 The user must have access to at least one subfunction of the module
525 specified by the hash key.
527 specific permission, e.g., 'export_catalog'
529 The user must have access to the specific subfunction list, which
530 must correspond to a row in the permissions table.
532 The C<$type> argument specifies whether the template should be
533 retrieved from the opac or intranet directory tree. "opac" is
534 assumed if it is not specified; however, if C<$type> is specified,
535 "intranet" is assumed if it is not "opac".
537 If C<$query> does not have a valid session ID associated with it
538 (i.e., the user has not logged in) or if the session has expired,
539 C<&checkauth> presents the user with a login page (from the point of
540 view of the original script, C<&checkauth> does not return). Once the
541 user has authenticated, C<&checkauth> restarts the original script
542 (this time, C<&checkauth> returns).
544 The login page is provided using a HTML::Template, which is set in the
545 systempreferences table or at the top of this file. The variable C<$type>
546 selects which template to use, either the opac or the intranet
547 authentification template.
549 C<&checkauth> returns a user ID, a cookie, and a session ID. The
550 cookie should be sent back to the browser; it verifies that the user
559 # If Version syspref is unavailable, it means Koha is beeing installed,
560 # and so we must redirect to OPAC maintenance page or to the WebInstaller
561 # also, if OpacMaintenance is ON, OPAC should redirect to maintenance
562 if (C4::Context->preference('OpacMaintenance') && $type eq 'opac') {
563 warn "OPAC Install required, redirecting to maintenance";
564 print $query->redirect("/cgi-bin/koha/maintenance.pl");
567 unless ( $version = C4::Context->preference('Version') ) { # assignment, not comparison
568 if ( $type ne 'opac' ) {
569 warn "Install required, redirecting to Installer";
570 print $query->redirect("/cgi-bin/koha/installer/install.pl");
572 warn "OPAC Install required, redirecting to maintenance";
573 print $query->redirect("/cgi-bin/koha/maintenance.pl");
578 # check that database and koha version are the same
579 # there is no DB version, it's a fresh install,
580 # go to web installer
581 # there is a DB version, compare it to the code version
582 my $kohaversion=C4::Context::KOHAVERSION;
583 # remove the 3 last . to have a Perl number
584 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
585 $debug and print STDERR "kohaversion : $kohaversion\n";
586 if ($version < $kohaversion){
587 my $warning = "Database update needed, redirecting to %s. Database is $version and Koha is $kohaversion";
588 if ($type ne 'opac'){
589 warn sprintf($warning, 'Installer');
590 print $query->redirect("/cgi-bin/koha/installer/install.pl?step=3");
592 warn sprintf("OPAC: " . $warning, 'maintenance');
593 print $query->redirect("/cgi-bin/koha/maintenance.pl");
601 open my $fh, '>>', "/tmp/sessionlog" or warn "ERROR: Cannot append to /tmp/sessionlog";
602 printf $fh join("\n",@_);
606 sub _timeout_syspref {
607 my $timeout = C4::Context->preference('timeout') || 600;
608 # value in days, convert in seconds
609 if ($timeout =~ /(\d+)[dD]/) {
610 $timeout = $1 * 86400;
617 $debug and warn "Checking Auth";
618 # $authnotrequired will be set for scripts which will run without authentication
619 my $authnotrequired = shift;
620 my $flagsrequired = shift;
623 $type = 'opac' unless $type;
625 my $dbh = C4::Context->dbh;
626 my $timeout = _timeout_syspref();
628 _version_check($type,$query);
632 my ( $userid, $cookie, $sessionID, $flags, $barshelves, $pubshelves );
633 my $logout = $query->param('logout.x');
635 # This parameter is the name of the CAS server we want to authenticate against,
636 # when using authentication against multiple CAS servers, as configured in Auth_cas_servers.yaml
637 my $casparam = $query->param('cas');
639 if ( $userid = $ENV{'REMOTE_USER'} ) {
640 # Using Basic Authentication, no cookies required
641 $cookie = $query->cookie(
642 -name => 'CGISESSID',
650 # we dont want to set a session because we are being called by a persona callback
652 elsif ( $sessionID = $query->cookie("CGISESSID") )
653 { # assignment, not comparison
654 my $session = get_session($sessionID);
655 C4::Context->_new_userenv($sessionID);
656 my ($ip, $lasttime, $sessiontype);
658 C4::Context::set_userenv(
659 $session->param('number'), $session->param('id'),
660 $session->param('cardnumber'), $session->param('firstname'),
661 $session->param('surname'), $session->param('branch'),
662 $session->param('branchname'), $session->param('flags'),
663 $session->param('emailaddress'), $session->param('branchprinter'),
664 $session->param('persona')
666 C4::Context::set_shelves_userenv('bar',$session->param('barshelves'));
667 C4::Context::set_shelves_userenv('pub',$session->param('pubshelves'));
668 C4::Context::set_shelves_userenv('tot',$session->param('totshelves'));
669 $debug and printf STDERR "AUTH_SESSION: (%s)\t%s %s - %s\n", map {$session->param($_)} qw(cardnumber firstname surname branch) ;
670 $ip = $session->param('ip');
671 $lasttime = $session->param('lasttime');
672 $userid = $session->param('id');
673 $sessiontype = $session->param('sessiontype') || '';
675 if ( ( ($query->param('koha_login_context')) && ($query->param('userid') ne $session->param('id')) )
676 || ( $cas && $query->param('ticket') ) ) {
677 #if a user enters an id ne to the id in the current session, we need to log them in...
678 #first we need to clear the anonymous session...
679 $debug and warn "query id = " . $query->param('userid') . " but session id = " . $session->param('id');
682 C4::Context->_unset_userenv($sessionID);
687 # voluntary logout the user
690 C4::Context->_unset_userenv($sessionID);
691 #_session_log(sprintf "%20s from %16s logged out at %30s (manually).\n", $userid,$ip,(strftime "%c",localtime));
695 if ($cas and $caslogout) {
699 elsif ( $lasttime < time() - $timeout ) {
701 $info{'timed_out'} = 1;
702 $session->delete() if $session;
703 C4::Context->_unset_userenv($sessionID);
704 #_session_log(sprintf "%20s from %16s logged out at %30s (inactivity).\n", $userid,$ip,(strftime "%c",localtime));
708 elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) {
709 # Different ip than originally logged in from
710 $info{'oldip'} = $ip;
711 $info{'newip'} = $ENV{'REMOTE_ADDR'};
712 $info{'different_ip'} = 1;
714 C4::Context->_unset_userenv($sessionID);
715 #_session_log(sprintf "%20s from %16s logged out at %30s (ip changed to %16s).\n", $userid,$ip,(strftime "%c",localtime), $info{'newip'});
720 $cookie = $query->cookie(
721 -name => 'CGISESSID',
722 -value => $session->id,
725 $session->param( 'lasttime', time() );
726 unless ( $sessiontype && $sessiontype eq 'anon' ) { #if this is an anonymous session, we want to update the session, but not behave as if they are logged in...
727 $flags = haspermission($userid, $flagsrequired);
731 $info{'nopermission'} = 1;
736 unless ($userid || $sessionID) {
738 #we initiate a session prior to checking for a username to allow for anonymous sessions...
739 my $session = get_session("") or die "Auth ERROR: Cannot get_session()";
740 my $sessionID = $session->id;
741 C4::Context->_new_userenv($sessionID);
742 $cookie = $query->cookie(
743 -name => 'CGISESSID',
744 -value => $session->id,
747 $userid = $query->param('userid');
748 if ( ( $cas && $query->param('ticket') )
750 || ( my $pki_field = C4::Context->preference('AllowPKIAuth') ) ne
753 my $password = $query->param('password');
755 my ( $return, $cardnumber );
756 if ( $cas && $query->param('ticket') ) {
758 ( $return, $cardnumber, $retuserid ) =
759 checkpw( $dbh, $userid, $password, $query );
760 $userid = $retuserid;
761 $info{'invalidCasLogin'} = 1 unless ($return);
765 my $value = $persona;
767 # If we're looking up the email, there's a chance that the person
768 # doesn't have a userid. So if there is none, we pass along the
769 # borrower number, and the bits of code that need to know the user
770 # ID will have to be smart enough to handle that.
772 my @users_info = C4::Members::GetBorrowersWithEmail($value);
775 # First the userid, then the borrowernum
776 $value = $users_info[0][1] || $users_info[0][0];
781 $return = $value ? 1 : 0;
786 ( $pki_field eq 'Common Name' && $ENV{'SSL_CLIENT_S_DN_CN'} )
787 || ( $pki_field eq 'emailAddress'
788 && $ENV{'SSL_CLIENT_S_DN_Email'} )
792 if ( $pki_field eq 'Common Name' ) {
793 $value = $ENV{'SSL_CLIENT_S_DN_CN'};
795 elsif ( $pki_field eq 'emailAddress' ) {
796 $value = $ENV{'SSL_CLIENT_S_DN_Email'};
798 # If we're looking up the email, there's a chance that the person
799 # doesn't have a userid. So if there is none, we pass along the
800 # borrower number, and the bits of code that need to know the user
801 # ID will have to be smart enough to handle that.
803 my @users_info = C4::Members::GetBorrowersWithEmail($value);
806 # First the userid, then the borrowernum
807 $value = $users_info[0][1] || $users_info[0][0];
814 $return = $value ? 1 : 0;
820 ( $return, $cardnumber, $retuserid ) =
821 checkpw( $dbh, $userid, $password, $query );
822 $userid = $retuserid if ( $retuserid ne '' );
825 #_session_log(sprintf "%20s from %16s logged in at %30s.\n", $userid,$ENV{'REMOTE_ADDR'},(strftime '%c', localtime));
826 if ( $flags = haspermission( $userid, $flagsrequired ) ) {
830 $info{'nopermission'} = 1;
831 C4::Context->_unset_userenv($sessionID);
833 my ($borrowernumber, $firstname, $surname, $userflags,
834 $branchcode, $branchname, $branchprinter, $emailaddress);
836 if ( $return == 1 ) {
838 SELECT borrowernumber, firstname, surname, flags, borrowers.branchcode,
839 branches.branchname as branchname,
840 branches.branchprinter as branchprinter,
843 LEFT JOIN branches on borrowers.branchcode=branches.branchcode
845 my $sth = $dbh->prepare("$select where userid=?");
846 $sth->execute($userid);
847 unless ($sth->rows) {
848 $debug and print STDERR "AUTH_1: no rows for userid='$userid'\n";
849 $sth = $dbh->prepare("$select where cardnumber=?");
850 $sth->execute($cardnumber);
852 unless ($sth->rows) {
853 $debug and print STDERR "AUTH_2a: no rows for cardnumber='$cardnumber'\n";
854 $sth->execute($userid);
855 unless ($sth->rows) {
856 $debug and print STDERR "AUTH_2b: no rows for userid='$userid' AS cardnumber\n";
861 ($borrowernumber, $firstname, $surname, $userflags,
862 $branchcode, $branchname, $branchprinter, $emailaddress) = $sth->fetchrow;
863 $debug and print STDERR "AUTH_3 results: " .
864 "$cardnumber,$borrowernumber,$userid,$firstname,$surname,$userflags,$branchcode,$emailaddress\n";
866 print STDERR "AUTH_3: no results for userid='$userid', cardnumber='$cardnumber'.\n";
869 # launch a sequence to check if we have a ip for the branch, i
870 # if we have one we replace the branchcode of the userenv by the branch bound in the ip.
872 my $ip = $ENV{'REMOTE_ADDR'};
873 # if they specify at login, use that
874 if ($query->param('branch')) {
875 $branchcode = $query->param('branch');
876 $branchname = GetBranchName($branchcode);
878 my $branches = GetBranches();
879 if (C4::Context->boolean_preference('IndependentBranches') && C4::Context->boolean_preference('Autolocation')){
880 # we have to check they are coming from the right ip range
881 my $domain = $branches->{$branchcode}->{'branchip'};
882 if ($ip !~ /^$domain/){
884 $info{'wrongip'} = 1;
889 foreach my $br ( keys %$branches ) {
890 # now we work with the treatment of ip
891 my $domain = $branches->{$br}->{'branchip'};
892 if ( $domain && $ip =~ /^$domain/ ) {
893 $branchcode = $branches->{$br}->{'branchcode'};
895 # new op dev : add the branchprinter and branchname in the cookie
896 $branchprinter = $branches->{$br}->{'branchprinter'};
897 $branchname = $branches->{$br}->{'branchname'};
900 $session->param('number',$borrowernumber);
901 $session->param('id',$userid);
902 $session->param('cardnumber',$cardnumber);
903 $session->param('firstname',$firstname);
904 $session->param('surname',$surname);
905 $session->param('branch',$branchcode);
906 $session->param('branchname',$branchname);
907 $session->param('flags',$userflags);
908 $session->param('emailaddress',$emailaddress);
909 $session->param('ip',$session->remote_addr());
910 $session->param('lasttime',time());
911 $debug and printf STDERR "AUTH_4: (%s)\t%s %s - %s\n", map {$session->param($_)} qw(cardnumber firstname surname branch) ;
913 elsif ( $return == 2 ) {
914 #We suppose the user is the superlibrarian
916 $session->param('number',0);
917 $session->param('id',C4::Context->config('user'));
918 $session->param('cardnumber',C4::Context->config('user'));
919 $session->param('firstname',C4::Context->config('user'));
920 $session->param('surname',C4::Context->config('user'));
921 $session->param('branch','NO_LIBRARY_SET');
922 $session->param('branchname','NO_LIBRARY_SET');
923 $session->param('flags',1);
924 $session->param('emailaddress', C4::Context->preference('KohaAdminEmailAddress'));
925 $session->param('ip',$session->remote_addr());
926 $session->param('lasttime',time());
929 $session->param('persona',1);
931 C4::Context::set_userenv(
932 $session->param('number'), $session->param('id'),
933 $session->param('cardnumber'), $session->param('firstname'),
934 $session->param('surname'), $session->param('branch'),
935 $session->param('branchname'), $session->param('flags'),
936 $session->param('emailaddress'), $session->param('branchprinter'),
937 $session->param('persona')
943 $info{'invalid_username_or_password'} = 1;
944 C4::Context->_unset_userenv($sessionID);
947 } # END if ( $userid = $query->param('userid') )
948 elsif ($type eq "opac") {
949 # if we are here this is an anonymous session; add public lists to it and a few other items...
950 # anonymous sessions are created only for the OPAC
951 $debug and warn "Initiating an anonymous session...";
953 # setting a couple of other session vars...
954 $session->param('ip',$session->remote_addr());
955 $session->param('lasttime',time());
956 $session->param('sessiontype','anon');
958 } # END unless ($userid)
960 # finished authentification, now respond
961 if ( $loggedin || $authnotrequired )
965 $cookie = $query->cookie(
966 -name => 'CGISESSID',
971 return ( $userid, $cookie, $sessionID, $flags );
976 # AUTH rejected, show the login/password template, after checking the DB.
980 # get the inputs from the incoming query
982 foreach my $name ( param $query) {
983 (next) if ( $name eq 'userid' || $name eq 'password' || $name eq 'ticket' );
984 my $value = $query->param($name);
985 push @inputs, { name => $name, value => $value };
988 my $LibraryNameTitle = C4::Context->preference("LibraryName");
989 $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
990 $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
992 my $template_name = ( $type eq 'opac' ) ? 'opac-auth.tmpl' : 'auth.tmpl';
993 my $template = C4::Templates::gettemplate($template_name, $type, $query );
995 branchloop => GetBranchesLoop(),
996 opaccolorstylesheet => C4::Context->preference("opaccolorstylesheet"),
997 opaclayoutstylesheet => C4::Context->preference("opaclayoutstylesheet"),
1000 casAuthentication => C4::Context->preference("casAuthentication"),
1001 suggestion => C4::Context->preference("suggestion"),
1002 virtualshelves => C4::Context->preference("virtualshelves"),
1003 LibraryName => "" . C4::Context->preference("LibraryName"),
1004 LibraryNameTitle => "" . $LibraryNameTitle,
1005 opacuserlogin => C4::Context->preference("opacuserlogin"),
1006 OpacNav => C4::Context->preference("OpacNav"),
1007 OpacNavRight => C4::Context->preference("OpacNavRight"),
1008 OpacNavBottom => C4::Context->preference("OpacNavBottom"),
1009 opaccredits => C4::Context->preference("opaccredits"),
1010 OpacFavicon => C4::Context->preference("OpacFavicon"),
1011 opacreadinghistory => C4::Context->preference("opacreadinghistory"),
1012 opacsmallimage => C4::Context->preference("opacsmallimage"),
1013 opaclanguagesdisplay => C4::Context->preference("opaclanguagesdisplay"),
1014 opacuserjs => C4::Context->preference("opacuserjs"),
1015 opacbookbag => "" . C4::Context->preference("opacbookbag"),
1016 OpacCloud => C4::Context->preference("OpacCloud"),
1017 OpacTopissue => C4::Context->preference("OpacTopissue"),
1018 OpacAuthorities => C4::Context->preference("OpacAuthorities"),
1019 OpacBrowser => C4::Context->preference("OpacBrowser"),
1020 opacheader => C4::Context->preference("opacheader"),
1021 TagsEnabled => C4::Context->preference("TagsEnabled"),
1022 OPACUserCSS => C4::Context->preference("OPACUserCSS"),
1023 intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),
1024 intranetstylesheet => C4::Context->preference("intranetstylesheet"),
1025 intranetbookbag => C4::Context->preference("intranetbookbag"),
1026 IntranetNav => C4::Context->preference("IntranetNav"),
1027 IntranetFavicon => C4::Context->preference("IntranetFavicon"),
1028 intranetuserjs => C4::Context->preference("intranetuserjs"),
1029 IndependentBranches=> C4::Context->preference("IndependentBranches"),
1030 AutoLocation => C4::Context->preference("AutoLocation"),
1031 wrongip => $info{'wrongip'},
1032 PatronSelfRegistration => C4::Context->preference("PatronSelfRegistration"),
1033 PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
1034 persona => C4::Context->preference("Persona"),
1035 opac_css_override => $ENV{'OPAC_CSS_OVERRIDE'},
1038 $template->param( OpacPublic => C4::Context->preference("OpacPublic"));
1039 $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1041 if($type eq 'opac'){
1042 my ($total, $pubshelves) = C4::VirtualShelves::GetSomeShelfNames(undef, 'MASTHEAD');
1044 pubshelves => $total->{pubtotal},
1045 pubshelvesloop => $pubshelves,
1051 # Is authentication against multiple CAS servers enabled?
1052 if (C4::Auth_with_cas::multipleAuth && !$casparam) {
1053 my $casservers = C4::Auth_with_cas::getMultipleAuth();
1055 foreach my $key (keys %$casservers) {
1056 push @tmplservers, {name => $key, value => login_cas_url($query, $key) . "?cas=$key" };
1059 casServersLoop => \@tmplservers
1063 casServerUrl => login_cas_url($query),
1068 invalidCasLogin => $info{'invalidCasLogin'}
1072 my $self_url = $query->url( -absolute => 1 );
1075 LibraryName => C4::Context->preference("LibraryName"),
1077 $template->param( %info );
1078 # $cookie = $query->cookie(CGISESSID => $session->id
1080 print $query->header(
1081 -type => 'text/html',
1082 -charset => 'utf-8',
1089 =head2 check_api_auth
1091 ($status, $cookie, $sessionId) = check_api_auth($query, $userflags);
1093 Given a CGI query containing the parameters 'userid' and 'password' and/or a session
1094 cookie, determine if the user has the privileges specified by C<$userflags>.
1096 C<check_api_auth> is is meant for authenticating users of web services, and
1097 consequently will always return and will not attempt to redirect the user
1100 If a valid session cookie is already present, check_api_auth will return a status
1101 of "ok", the cookie, and the Koha session ID.
1103 If no session cookie is present, check_api_auth will check the 'userid' and 'password
1104 parameters and create a session cookie and Koha session if the supplied credentials
1107 Possible return values in C<$status> are:
1111 =item "ok" -- user authenticated; C<$cookie> and C<$sessionid> have valid values.
1113 =item "failed" -- credentials are not correct; C<$cookie> and C<$sessionid> are undef
1115 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1117 =item "expired -- session cookie has expired; API user should resubmit userid and password
1123 sub check_api_auth {
1125 my $flagsrequired = shift;
1127 my $dbh = C4::Context->dbh;
1128 my $timeout = _timeout_syspref();
1130 unless (C4::Context->preference('Version')) {
1131 # database has not been installed yet
1132 return ("maintenance", undef, undef);
1134 my $kohaversion=C4::Context::KOHAVERSION;
1135 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1136 if (C4::Context->preference('Version') < $kohaversion) {
1137 # database in need of version update; assume that
1138 # no API should be called while databsae is in
1140 return ("maintenance", undef, undef);
1143 # FIXME -- most of what follows is a copy-and-paste
1144 # of code from checkauth. There is an obvious need
1145 # for refactoring to separate the various parts of
1146 # the authentication code, but as of 2007-11-19 this
1147 # is deferred so as to not introduce bugs into the
1148 # regular authentication code for Koha 3.0.
1150 # see if we have a valid session cookie already
1151 # however, if a userid parameter is present (i.e., from
1152 # a form submission, assume that any current cookie
1154 my $sessionID = undef;
1155 unless ($query->param('userid')) {
1156 $sessionID = $query->cookie("CGISESSID");
1158 if ($sessionID && not ($cas && $query->param('PT')) ) {
1159 my $session = get_session($sessionID);
1160 C4::Context->_new_userenv($sessionID);
1162 C4::Context::set_userenv(
1163 $session->param('number'), $session->param('id'),
1164 $session->param('cardnumber'), $session->param('firstname'),
1165 $session->param('surname'), $session->param('branch'),
1166 $session->param('branchname'), $session->param('flags'),
1167 $session->param('emailaddress'), $session->param('branchprinter')
1170 my $ip = $session->param('ip');
1171 my $lasttime = $session->param('lasttime');
1172 my $userid = $session->param('id');
1173 if ( $lasttime < time() - $timeout ) {
1176 C4::Context->_unset_userenv($sessionID);
1179 return ("expired", undef, undef);
1180 } elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) {
1181 # IP address changed
1183 C4::Context->_unset_userenv($sessionID);
1186 return ("expired", undef, undef);
1188 my $cookie = $query->cookie(
1189 -name => 'CGISESSID',
1190 -value => $session->id,
1193 $session->param('lasttime',time());
1194 my $flags = haspermission($userid, $flagsrequired);
1196 return ("ok", $cookie, $sessionID);
1199 C4::Context->_unset_userenv($sessionID);
1202 return ("failed", undef, undef);
1206 return ("expired", undef, undef);
1210 my $userid = $query->param('userid');
1211 my $password = $query->param('password');
1212 my ($return, $cardnumber);
1215 if ($cas && $query->param('PT')) {
1217 $debug and print STDERR "## check_api_auth - checking CAS\n";
1218 # In case of a CAS authentication, we use the ticket instead of the password
1219 my $PT = $query->param('PT');
1220 ($return,$cardnumber,$userid) = check_api_auth_cas($dbh, $PT, $query); # EXTERNAL AUTH
1222 # User / password auth
1223 unless ($userid and $password) {
1224 # caller did something wrong, fail the authenticateion
1225 return ("failed", undef, undef);
1227 ( $return, $cardnumber ) = checkpw( $dbh, $userid, $password, $query );
1230 if ($return and haspermission( $userid, $flagsrequired)) {
1231 my $session = get_session("");
1232 return ("failed", undef, undef) unless $session;
1234 my $sessionID = $session->id;
1235 C4::Context->_new_userenv($sessionID);
1236 my $cookie = $query->cookie(
1237 -name => 'CGISESSID',
1238 -value => $sessionID,
1241 if ( $return == 1 ) {
1243 $borrowernumber, $firstname, $surname,
1244 $userflags, $branchcode, $branchname,
1245 $branchprinter, $emailaddress
1249 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname,branches.branchprinter as branchprinter, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where userid=?"
1251 $sth->execute($userid);
1253 $borrowernumber, $firstname, $surname,
1254 $userflags, $branchcode, $branchname,
1255 $branchprinter, $emailaddress
1256 ) = $sth->fetchrow if ( $sth->rows );
1258 unless ($sth->rows ) {
1259 my $sth = $dbh->prepare(
1260 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, branches.branchprinter as branchprinter, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
1262 $sth->execute($cardnumber);
1264 $borrowernumber, $firstname, $surname,
1265 $userflags, $branchcode, $branchname,
1266 $branchprinter, $emailaddress
1267 ) = $sth->fetchrow if ( $sth->rows );
1269 unless ( $sth->rows ) {
1270 $sth->execute($userid);
1272 $borrowernumber, $firstname, $surname, $userflags,
1273 $branchcode, $branchname, $branchprinter, $emailaddress
1274 ) = $sth->fetchrow if ( $sth->rows );
1278 my $ip = $ENV{'REMOTE_ADDR'};
1279 # if they specify at login, use that
1280 if ($query->param('branch')) {
1281 $branchcode = $query->param('branch');
1282 $branchname = GetBranchName($branchcode);
1284 my $branches = GetBranches();
1286 foreach my $br ( keys %$branches ) {
1287 # now we work with the treatment of ip
1288 my $domain = $branches->{$br}->{'branchip'};
1289 if ( $domain && $ip =~ /^$domain/ ) {
1290 $branchcode = $branches->{$br}->{'branchcode'};
1292 # new op dev : add the branchprinter and branchname in the cookie
1293 $branchprinter = $branches->{$br}->{'branchprinter'};
1294 $branchname = $branches->{$br}->{'branchname'};
1297 $session->param('number',$borrowernumber);
1298 $session->param('id',$userid);
1299 $session->param('cardnumber',$cardnumber);
1300 $session->param('firstname',$firstname);
1301 $session->param('surname',$surname);
1302 $session->param('branch',$branchcode);
1303 $session->param('branchname',$branchname);
1304 $session->param('flags',$userflags);
1305 $session->param('emailaddress',$emailaddress);
1306 $session->param('ip',$session->remote_addr());
1307 $session->param('lasttime',time());
1308 } elsif ( $return == 2 ) {
1309 #We suppose the user is the superlibrarian
1310 $session->param('number',0);
1311 $session->param('id',C4::Context->config('user'));
1312 $session->param('cardnumber',C4::Context->config('user'));
1313 $session->param('firstname',C4::Context->config('user'));
1314 $session->param('surname',C4::Context->config('user'));
1315 $session->param('branch','NO_LIBRARY_SET');
1316 $session->param('branchname','NO_LIBRARY_SET');
1317 $session->param('flags',1);
1318 $session->param('emailaddress', C4::Context->preference('KohaAdminEmailAddress'));
1319 $session->param('ip',$session->remote_addr());
1320 $session->param('lasttime',time());
1322 C4::Context::set_userenv(
1323 $session->param('number'), $session->param('id'),
1324 $session->param('cardnumber'), $session->param('firstname'),
1325 $session->param('surname'), $session->param('branch'),
1326 $session->param('branchname'), $session->param('flags'),
1327 $session->param('emailaddress'), $session->param('branchprinter')
1329 return ("ok", $cookie, $sessionID);
1331 return ("failed", undef, undef);
1336 =head2 check_cookie_auth
1338 ($status, $sessionId) = check_api_auth($cookie, $userflags);
1340 Given a CGISESSID cookie set during a previous login to Koha, determine
1341 if the user has the privileges specified by C<$userflags>.
1343 C<check_cookie_auth> is meant for authenticating special services
1344 such as tools/upload-file.pl that are invoked by other pages that
1345 have been authenticated in the usual way.
1347 Possible return values in C<$status> are:
1351 =item "ok" -- user authenticated; C<$sessionID> have valid values.
1353 =item "failed" -- credentials are not correct; C<$sessionid> are undef
1355 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1357 =item "expired -- session cookie has expired; API user should resubmit userid and password
1363 sub check_cookie_auth {
1365 my $flagsrequired = shift;
1367 my $dbh = C4::Context->dbh;
1368 my $timeout = _timeout_syspref();
1370 unless (C4::Context->preference('Version')) {
1371 # database has not been installed yet
1372 return ("maintenance", undef);
1374 my $kohaversion=C4::Context::KOHAVERSION;
1375 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1376 if (C4::Context->preference('Version') < $kohaversion) {
1377 # database in need of version update; assume that
1378 # no API should be called while databsae is in
1380 return ("maintenance", undef);
1383 # FIXME -- most of what follows is a copy-and-paste
1384 # of code from checkauth. There is an obvious need
1385 # for refactoring to separate the various parts of
1386 # the authentication code, but as of 2007-11-23 this
1387 # is deferred so as to not introduce bugs into the
1388 # regular authentication code for Koha 3.0.
1390 # see if we have a valid session cookie already
1391 # however, if a userid parameter is present (i.e., from
1392 # a form submission, assume that any current cookie
1394 unless (defined $cookie and $cookie) {
1395 return ("failed", undef);
1397 my $sessionID = $cookie;
1398 my $session = get_session($sessionID);
1399 C4::Context->_new_userenv($sessionID);
1401 C4::Context::set_userenv(
1402 $session->param('number'), $session->param('id'),
1403 $session->param('cardnumber'), $session->param('firstname'),
1404 $session->param('surname'), $session->param('branch'),
1405 $session->param('branchname'), $session->param('flags'),
1406 $session->param('emailaddress'), $session->param('branchprinter')
1409 my $ip = $session->param('ip');
1410 my $lasttime = $session->param('lasttime');
1411 my $userid = $session->param('id');
1412 if ( $lasttime < time() - $timeout ) {
1415 C4::Context->_unset_userenv($sessionID);
1418 return ("expired", undef);
1419 } elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) {
1420 # IP address changed
1422 C4::Context->_unset_userenv($sessionID);
1425 return ("expired", undef);
1427 $session->param('lasttime',time());
1428 my $flags = haspermission($userid, $flagsrequired);
1430 return ("ok", $sessionID);
1433 C4::Context->_unset_userenv($sessionID);
1436 return ("failed", undef);
1440 return ("expired", undef);
1447 my $session = get_session($sessionID);
1449 Given a session ID, retrieve the CGI::Session object used to store
1450 the session's state. The session object can be used to store
1451 data that needs to be accessed by different scripts during a
1454 If the C<$sessionID> parameter is an empty string, a new session
1460 my $sessionID = shift;
1461 my $storage_method = C4::Context->preference('SessionStorage');
1462 my $dbh = C4::Context->dbh;
1464 if ($storage_method eq 'mysql'){
1465 $session = new CGI::Session("driver:MySQL;serializer:yaml;id:md5", $sessionID, {Handle=>$dbh});
1467 elsif ($storage_method eq 'Pg') {
1468 $session = new CGI::Session("driver:PostgreSQL;serializer:yaml;id:md5", $sessionID, {Handle=>$dbh});
1470 elsif ($storage_method eq 'memcached' && C4::Context->ismemcached){
1471 $session = new CGI::Session("driver:memcached;serializer:yaml;id:md5", $sessionID, { Memcached => C4::Context->memcached } );
1474 # catch all defaults to tmp should work on all systems
1475 $session = new CGI::Session("driver:File;serializer:yaml;id:md5", $sessionID, {Directory=>'/tmp'});
1480 # Using Bcrypt method for hashing. This can be changed to something else in future, if needed.
1482 my $password = shift;
1484 # Generate a salt if one is not passed
1485 my $settings = shift;
1486 unless( defined $settings ){ # if there are no settings, we need to create a salt and append settings
1487 # Set the cost to 8 and append a NULL
1488 $settings = '$2a$08$'.en_base64(generate_salt('weak', 16));
1491 return bcrypt($password, $settings);
1494 =head2 generate_salt
1497 my $salt = C4::Auth::generate_salt($strength, $length);
1503 For general password salting a C<$strength> of C<weak> is recommend,
1504 For generating a server-salt a C<$strength> of C<strong> is recommended
1506 'strong' uses /dev/random which may block until sufficient entropy is acheived.
1507 'weak' uses /dev/urandom and is non-blocking.
1511 C<$length> is a positive integer which specifies the desired length of the returned string
1518 # the implementation of generate_salt is loosely based on Crypt::Random::Provider::File
1520 # strength is 'strong' or 'weak'
1521 # length is number of bytes to read, positive integer
1522 my ($strength, $length) = @_;
1527 die "non-positive strength of '$strength' passed to C4::Auth::generate_salt\n";
1530 if( $strength eq "strong" ){
1531 $source = '/dev/random'; # blocking
1533 unless( $strength eq 'weak' ){
1534 warn "unsuppored strength of '$strength' passed to C4::Auth::generate_salt, defaulting to 'weak'\n";
1536 $source = '/dev/urandom'; # non-blocking
1539 sysopen SOURCE, $source, O_RDONLY
1540 or die "failed to open source '$source' in C4::Auth::generate_salt\n";
1542 # $bytes is the bytes just read
1543 # $string is the concatenation of all the bytes read so far
1544 my( $bytes, $string ) = ("", "");
1546 # keep reading until we have $length bytes in $strength
1547 while( length($string) < $length ){
1548 # return the number of bytes read, 0 (EOF), or -1 (ERROR)
1549 my $return = sysread SOURCE, $bytes, $length - length($string);
1551 # if no bytes were read, keep reading (if using /dev/random it is possible there was insufficient entropy so this may block)
1552 next unless $return;
1553 if( $return == -1 ){
1554 die "error while reading from $source in C4::Auth::generate_salt\n";
1566 my ( $dbh, $userid, $password, $query ) = @_;
1569 $debug and print STDERR "## checkpw - checking LDAP\n";
1570 my ($retval,$retcard,$retuserid) = checkpw_ldap(@_); # EXTERNAL AUTH
1571 ($retval) and return ($retval,$retcard,$retuserid);
1574 if ($cas && $query && $query->param('ticket')) {
1575 $debug and print STDERR "## checkpw - checking CAS\n";
1576 # In case of a CAS authentication, we use the ticket instead of the password
1577 my $ticket = $query->param('ticket');
1578 my ($retval,$retcard,$retuserid) = checkpw_cas($dbh, $ticket, $query); # EXTERNAL AUTH
1579 ($retval) and return ($retval,$retcard,$retuserid);
1583 return checkpw_internal(@_)
1586 sub checkpw_internal {
1587 my ( $dbh, $userid, $password ) = @_;
1591 "select password,cardnumber,borrowernumber,userid,firstname,surname,branchcode,flags from borrowers where userid=?"
1593 $sth->execute($userid);
1595 my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
1596 $surname, $branchcode, $flags )
1599 if ( checkpw_hash($password, $stored_hash) ) {
1601 C4::Context->set_userenv( "$borrowernumber", $userid, $cardnumber,
1602 $firstname, $surname, $branchcode, $flags );
1603 return 1, $cardnumber, $userid;
1608 "select password,cardnumber,borrowernumber,userid, firstname,surname,branchcode,flags from borrowers where cardnumber=?"
1610 $sth->execute($userid);
1612 my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
1613 $surname, $branchcode, $flags )
1616 if ( checkpw_hash($password, $stored_hash) ) {
1618 C4::Context->set_userenv( $borrowernumber, $userid, $cardnumber,
1619 $firstname, $surname, $branchcode, $flags );
1620 return 1, $cardnumber, $userid;
1623 if ( $userid && $userid eq C4::Context->config('user')
1624 && "$password" eq C4::Context->config('pass') )
1627 # Koha superuser account
1628 # C4::Context->set_userenv(0,0,C4::Context->config('user'),C4::Context->config('user'),C4::Context->config('user'),"",1);
1631 if ( $userid && $userid eq 'demo'
1632 && "$password" eq 'demo'
1633 && C4::Context->config('demo') )
1636 # DEMO => the demo user is allowed to do everything (if demo set to 1 in koha.conf
1637 # some features won't be effective : modify systempref, modify MARC structure,
1644 my ( $password, $stored_hash ) = @_;
1646 return if $stored_hash eq '!';
1648 # check what encryption algorithm was implemented: Bcrypt - if the hash starts with '$2' it is Bcrypt else md5
1650 if ( substr($stored_hash,0,2) eq '$2') {
1651 $hash = hash_password($password, $stored_hash);
1653 $hash = md5_base64($password);
1655 return $hash eq $stored_hash;
1660 my $authflags = getuserflags($flags, $userid, [$dbh]);
1662 Translates integer flags into permissions strings hash.
1664 C<$flags> is the integer userflags value ( borrowers.userflags )
1665 C<$userid> is the members.userid, used for building subpermissions
1666 C<$authflags> is a hashref of permissions
1673 my $dbh = @_ ? shift : C4::Context->dbh;
1676 # I don't want to do this, but if someone logs in as the database
1677 # user, it would be preferable not to spam them to death with
1678 # numeric warnings. So, we make $flags numeric.
1679 no warnings 'numeric';
1682 my $sth = $dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
1685 while ( my ( $bit, $flag, $defaulton ) = $sth->fetchrow ) {
1686 if ( ( $flags & ( 2**$bit ) ) || $defaulton ) {
1687 $userflags->{$flag} = 1;
1690 $userflags->{$flag} = 0;
1694 # get subpermissions and merge with top-level permissions
1695 my $user_subperms = get_user_subpermissions($userid);
1696 foreach my $module (keys %$user_subperms) {
1697 next if $userflags->{$module} == 1; # user already has permission for everything in this module
1698 $userflags->{$module} = $user_subperms->{$module};
1704 =head2 get_user_subpermissions
1706 $user_perm_hashref = get_user_subpermissions($userid);
1708 Given the userid (note, not the borrowernumber) of a staff user,
1709 return a hashref of hashrefs of the specific subpermissions
1710 accorded to the user. An example return is
1714 export_catalog => 1,
1715 import_patrons => 1,
1719 The top-level hash-key is a module or function code from
1720 userflags.flag, while the second-level key is a code
1723 The results of this function do not give a complete picture
1724 of the functions that a staff user can access; it is also
1725 necessary to check borrowers.flags.
1729 sub get_user_subpermissions {
1732 my $dbh = C4::Context->dbh;
1733 my $sth = $dbh->prepare("SELECT flag, user_permissions.code
1734 FROM user_permissions
1735 JOIN permissions USING (module_bit, code)
1736 JOIN userflags ON (module_bit = bit)
1737 JOIN borrowers USING (borrowernumber)
1739 $sth->execute($userid);
1741 my $user_perms = {};
1742 while (my $perm = $sth->fetchrow_hashref) {
1743 $user_perms->{$perm->{'flag'}}->{$perm->{'code'}} = 1;
1748 =head2 get_all_subpermissions
1750 my $perm_hashref = get_all_subpermissions();
1752 Returns a hashref of hashrefs defining all specific
1753 permissions currently defined. The return value
1754 has the same structure as that of C<get_user_subpermissions>,
1755 except that the innermost hash value is the description
1756 of the subpermission.
1760 sub get_all_subpermissions {
1761 my $dbh = C4::Context->dbh;
1762 my $sth = $dbh->prepare("SELECT flag, code, description
1764 JOIN userflags ON (module_bit = bit)");
1768 while (my $perm = $sth->fetchrow_hashref) {
1769 $all_perms->{$perm->{'flag'}}->{$perm->{'code'}} = $perm->{'description'};
1774 =head2 haspermission
1776 $flags = ($userid, $flagsrequired);
1778 C<$userid> the userid of the member
1779 C<$flags> is a hashref of required flags like C<$borrower-<{authflags}>
1781 Returns member's flags or 0 if a permission is not met.
1786 my ($userid, $flagsrequired) = @_;
1787 my $sth = C4::Context->dbh->prepare("SELECT flags FROM borrowers WHERE userid=?");
1788 $sth->execute($userid);
1789 my $flags = getuserflags($sth->fetchrow(), $userid);
1790 if ( $userid eq C4::Context->config('user') ) {
1791 # Super User Account from /etc/koha.conf
1792 $flags->{'superlibrarian'} = 1;
1794 elsif ( $userid eq 'demo' && C4::Context->config('demo') ) {
1795 # Demo user that can do "anything" (demo=1 in /etc/koha.conf)
1796 $flags->{'superlibrarian'} = 1;
1799 return $flags if $flags->{superlibrarian};
1801 foreach my $module ( keys %$flagsrequired ) {
1802 my $subperm = $flagsrequired->{$module};
1803 if ($subperm eq '*') {
1804 return 0 unless ( $flags->{$module} == 1 or ref($flags->{$module}) );
1806 return 0 unless ( $flags->{$module} == 1 or
1807 ( ref($flags->{$module}) and
1808 exists $flags->{$module}->{$subperm} and
1809 $flags->{$module}->{$subperm} == 1
1815 #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
1819 sub getborrowernumber {
1821 my $userenv = C4::Context->userenv;
1822 if ( defined( $userenv ) && ref( $userenv ) eq 'HASH' && $userenv->{number} ) {
1823 return $userenv->{number};
1825 my $dbh = C4::Context->dbh;
1826 for my $field ( 'userid', 'cardnumber' ) {
1828 $dbh->prepare("select borrowernumber from borrowers where $field=?");
1829 $sth->execute($userid);
1831 my ($bnumber) = $sth->fetchrow;
1838 sub ParseSearchHistoryCookie {
1840 my $search_cookie = $input->cookie('KohaOpacRecentSearches');
1841 return () unless $search_cookie;
1842 my $obj = eval { decode_json(uri_unescape($search_cookie)) };
1843 return () unless defined $obj;
1844 return () unless ref $obj eq 'ARRAY';
1848 END { } # module clean-up code here (global destructor)
1858 Crypt::Eksblowfish::Bcrypt(3)