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"));
464 return ( $template, $borrowernumber, $cookie, $flags);
469 ($userid, $cookie, $sessionID) = &checkauth($query, $noauth, $flagsrequired, $type);
471 Verifies that the user is authorized to run this script. If
472 the user is authorized, a (userid, cookie, session-id, flags)
473 quadruple is returned. If the user is not authorized but does
474 not have the required privilege (see $flagsrequired below), it
475 displays an error page and exits. Otherwise, it displays the
476 login page and exits.
478 Note that C<&checkauth> will return if and only if the user
479 is authorized, so it should be called early on, before any
480 unfinished operations (e.g., if you've opened a file, then
481 C<&checkauth> won't close it for you).
483 C<$query> is the CGI object for the script calling C<&checkauth>.
485 The C<$noauth> argument is optional. If it is set, then no
486 authorization is required for the script.
488 C<&checkauth> fetches user and session information from C<$query> and
489 ensures that the user is authorized to run scripts that require
492 The C<$flagsrequired> argument specifies the required privileges
493 the user must have if the username and password are correct.
494 It should be specified as a reference-to-hash; keys in the hash
495 should be the "flags" for the user, as specified in the Members
496 intranet module. Any key specified must correspond to a "flag"
497 in the userflags table. E.g., { circulate => 1 } would specify
498 that the user must have the "circulate" privilege in order to
499 proceed. To make sure that access control is correct, the
500 C<$flagsrequired> parameter must be specified correctly.
502 Koha also has a concept of sub-permissions, also known as
503 granular permissions. This makes the value of each key
504 in the C<flagsrequired> hash take on an additional
509 The user must have access to all subfunctions of the module
510 specified by the hash key.
514 The user must have access to at least one subfunction of the module
515 specified by the hash key.
517 specific permission, e.g., 'export_catalog'
519 The user must have access to the specific subfunction list, which
520 must correspond to a row in the permissions table.
522 The C<$type> argument specifies whether the template should be
523 retrieved from the opac or intranet directory tree. "opac" is
524 assumed if it is not specified; however, if C<$type> is specified,
525 "intranet" is assumed if it is not "opac".
527 If C<$query> does not have a valid session ID associated with it
528 (i.e., the user has not logged in) or if the session has expired,
529 C<&checkauth> presents the user with a login page (from the point of
530 view of the original script, C<&checkauth> does not return). Once the
531 user has authenticated, C<&checkauth> restarts the original script
532 (this time, C<&checkauth> returns).
534 The login page is provided using a HTML::Template, which is set in the
535 systempreferences table or at the top of this file. The variable C<$type>
536 selects which template to use, either the opac or the intranet
537 authentification template.
539 C<&checkauth> returns a user ID, a cookie, and a session ID. The
540 cookie should be sent back to the browser; it verifies that the user
549 # If Version syspref is unavailable, it means Koha is beeing installed,
550 # and so we must redirect to OPAC maintenance page or to the WebInstaller
551 # also, if OpacMaintenance is ON, OPAC should redirect to maintenance
552 if (C4::Context->preference('OpacMaintenance') && $type eq 'opac') {
553 warn "OPAC Install required, redirecting to maintenance";
554 print $query->redirect("/cgi-bin/koha/maintenance.pl");
557 unless ( $version = C4::Context->preference('Version') ) { # assignment, not comparison
558 if ( $type ne 'opac' ) {
559 warn "Install required, redirecting to Installer";
560 print $query->redirect("/cgi-bin/koha/installer/install.pl");
562 warn "OPAC Install required, redirecting to maintenance";
563 print $query->redirect("/cgi-bin/koha/maintenance.pl");
568 # check that database and koha version are the same
569 # there is no DB version, it's a fresh install,
570 # go to web installer
571 # there is a DB version, compare it to the code version
572 my $kohaversion=C4::Context::KOHAVERSION;
573 # remove the 3 last . to have a Perl number
574 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
575 $debug and print STDERR "kohaversion : $kohaversion\n";
576 if ($version < $kohaversion){
577 my $warning = "Database update needed, redirecting to %s. Database is $version and Koha is $kohaversion";
578 if ($type ne 'opac'){
579 warn sprintf($warning, 'Installer');
580 print $query->redirect("/cgi-bin/koha/installer/install.pl?step=3");
582 warn sprintf("OPAC: " . $warning, 'maintenance');
583 print $query->redirect("/cgi-bin/koha/maintenance.pl");
591 open my $fh, '>>', "/tmp/sessionlog" or warn "ERROR: Cannot append to /tmp/sessionlog";
592 printf $fh join("\n",@_);
596 sub _timeout_syspref {
597 my $timeout = C4::Context->preference('timeout') || 600;
598 # value in days, convert in seconds
599 if ($timeout =~ /(\d+)[dD]/) {
600 $timeout = $1 * 86400;
607 $debug and warn "Checking Auth";
608 # $authnotrequired will be set for scripts which will run without authentication
609 my $authnotrequired = shift;
610 my $flagsrequired = shift;
613 $type = 'opac' unless $type;
615 my $dbh = C4::Context->dbh;
616 my $timeout = _timeout_syspref();
618 _version_check($type,$query);
622 my ( $userid, $cookie, $sessionID, $flags, $barshelves, $pubshelves );
623 my $logout = $query->param('logout.x');
625 # This parameter is the name of the CAS server we want to authenticate against,
626 # when using authentication against multiple CAS servers, as configured in Auth_cas_servers.yaml
627 my $casparam = $query->param('cas');
629 if ( $userid = $ENV{'REMOTE_USER'} ) {
630 # Using Basic Authentication, no cookies required
631 $cookie = $query->cookie(
632 -name => 'CGISESSID',
640 # we dont want to set a session because we are being called by a persona callback
642 elsif ( $sessionID = $query->cookie("CGISESSID") )
643 { # assignment, not comparison
644 my $session = get_session($sessionID);
645 C4::Context->_new_userenv($sessionID);
646 my ($ip, $lasttime, $sessiontype);
648 C4::Context::set_userenv(
649 $session->param('number'), $session->param('id'),
650 $session->param('cardnumber'), $session->param('firstname'),
651 $session->param('surname'), $session->param('branch'),
652 $session->param('branchname'), $session->param('flags'),
653 $session->param('emailaddress'), $session->param('branchprinter'),
654 $session->param('persona')
656 C4::Context::set_shelves_userenv('bar',$session->param('barshelves'));
657 C4::Context::set_shelves_userenv('pub',$session->param('pubshelves'));
658 C4::Context::set_shelves_userenv('tot',$session->param('totshelves'));
659 $debug and printf STDERR "AUTH_SESSION: (%s)\t%s %s - %s\n", map {$session->param($_)} qw(cardnumber firstname surname branch) ;
660 $ip = $session->param('ip');
661 $lasttime = $session->param('lasttime');
662 $userid = $session->param('id');
663 $sessiontype = $session->param('sessiontype') || '';
665 if ( ( ($query->param('koha_login_context')) && ($query->param('userid') ne $session->param('id')) )
666 || ( $cas && $query->param('ticket') ) ) {
667 #if a user enters an id ne to the id in the current session, we need to log them in...
668 #first we need to clear the anonymous session...
669 $debug and warn "query id = " . $query->param('userid') . " but session id = " . $session->param('id');
672 C4::Context->_unset_userenv($sessionID);
677 # voluntary logout the user
680 C4::Context->_unset_userenv($sessionID);
681 #_session_log(sprintf "%20s from %16s logged out at %30s (manually).\n", $userid,$ip,(strftime "%c",localtime));
685 if ($cas and $caslogout) {
689 elsif ( $lasttime < time() - $timeout ) {
691 $info{'timed_out'} = 1;
692 $session->delete() if $session;
693 C4::Context->_unset_userenv($sessionID);
694 #_session_log(sprintf "%20s from %16s logged out at %30s (inactivity).\n", $userid,$ip,(strftime "%c",localtime));
698 elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) {
699 # Different ip than originally logged in from
700 $info{'oldip'} = $ip;
701 $info{'newip'} = $ENV{'REMOTE_ADDR'};
702 $info{'different_ip'} = 1;
704 C4::Context->_unset_userenv($sessionID);
705 #_session_log(sprintf "%20s from %16s logged out at %30s (ip changed to %16s).\n", $userid,$ip,(strftime "%c",localtime), $info{'newip'});
710 $cookie = $query->cookie(
711 -name => 'CGISESSID',
712 -value => $session->id,
715 $session->param( 'lasttime', time() );
716 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...
717 $flags = haspermission($userid, $flagsrequired);
721 $info{'nopermission'} = 1;
726 unless ($userid || $sessionID) {
728 #we initiate a session prior to checking for a username to allow for anonymous sessions...
729 my $session = get_session("") or die "Auth ERROR: Cannot get_session()";
730 my $sessionID = $session->id;
731 C4::Context->_new_userenv($sessionID);
732 $cookie = $query->cookie(
733 -name => 'CGISESSID',
734 -value => $session->id,
737 $userid = $query->param('userid');
738 if ( ( $cas && $query->param('ticket') )
740 || ( my $pki_field = C4::Context->preference('AllowPKIAuth') ) ne
743 my $password = $query->param('password');
745 my ( $return, $cardnumber );
746 if ( $cas && $query->param('ticket') ) {
748 ( $return, $cardnumber, $retuserid ) =
749 checkpw( $dbh, $userid, $password, $query );
750 $userid = $retuserid;
751 $info{'invalidCasLogin'} = 1 unless ($return);
755 my $value = $persona;
757 # If we're looking up the email, there's a chance that the person
758 # doesn't have a userid. So if there is none, we pass along the
759 # borrower number, and the bits of code that need to know the user
760 # ID will have to be smart enough to handle that.
762 my @users_info = C4::Members::GetBorrowersWithEmail($value);
765 # First the userid, then the borrowernum
766 $value = $users_info[0][1] || $users_info[0][0];
771 $return = $value ? 1 : 0;
776 ( $pki_field eq 'Common Name' && $ENV{'SSL_CLIENT_S_DN_CN'} )
777 || ( $pki_field eq 'emailAddress'
778 && $ENV{'SSL_CLIENT_S_DN_Email'} )
782 if ( $pki_field eq 'Common Name' ) {
783 $value = $ENV{'SSL_CLIENT_S_DN_CN'};
785 elsif ( $pki_field eq 'emailAddress' ) {
786 $value = $ENV{'SSL_CLIENT_S_DN_Email'};
788 # If we're looking up the email, there's a chance that the person
789 # doesn't have a userid. So if there is none, we pass along the
790 # borrower number, and the bits of code that need to know the user
791 # ID will have to be smart enough to handle that.
793 my @users_info = C4::Members::GetBorrowersWithEmail($value);
796 # First the userid, then the borrowernum
797 $value = $users_info[0][1] || $users_info[0][0];
804 $return = $value ? 1 : 0;
810 ( $return, $cardnumber, $retuserid ) =
811 checkpw( $dbh, $userid, $password, $query );
812 $userid = $retuserid if ( $retuserid ne '' );
815 #_session_log(sprintf "%20s from %16s logged in at %30s.\n", $userid,$ENV{'REMOTE_ADDR'},(strftime '%c', localtime));
816 if ( $flags = haspermission( $userid, $flagsrequired ) ) {
820 $info{'nopermission'} = 1;
821 C4::Context->_unset_userenv($sessionID);
823 my ($borrowernumber, $firstname, $surname, $userflags,
824 $branchcode, $branchname, $branchprinter, $emailaddress);
826 if ( $return == 1 ) {
828 SELECT borrowernumber, firstname, surname, flags, borrowers.branchcode,
829 branches.branchname as branchname,
830 branches.branchprinter as branchprinter,
833 LEFT JOIN branches on borrowers.branchcode=branches.branchcode
835 my $sth = $dbh->prepare("$select where userid=?");
836 $sth->execute($userid);
837 unless ($sth->rows) {
838 $debug and print STDERR "AUTH_1: no rows for userid='$userid'\n";
839 $sth = $dbh->prepare("$select where cardnumber=?");
840 $sth->execute($cardnumber);
842 unless ($sth->rows) {
843 $debug and print STDERR "AUTH_2a: no rows for cardnumber='$cardnumber'\n";
844 $sth->execute($userid);
845 unless ($sth->rows) {
846 $debug and print STDERR "AUTH_2b: no rows for userid='$userid' AS cardnumber\n";
851 ($borrowernumber, $firstname, $surname, $userflags,
852 $branchcode, $branchname, $branchprinter, $emailaddress) = $sth->fetchrow;
853 $debug and print STDERR "AUTH_3 results: " .
854 "$cardnumber,$borrowernumber,$userid,$firstname,$surname,$userflags,$branchcode,$emailaddress\n";
856 print STDERR "AUTH_3: no results for userid='$userid', cardnumber='$cardnumber'.\n";
859 # launch a sequence to check if we have a ip for the branch, i
860 # if we have one we replace the branchcode of the userenv by the branch bound in the ip.
862 my $ip = $ENV{'REMOTE_ADDR'};
863 # if they specify at login, use that
864 if ($query->param('branch')) {
865 $branchcode = $query->param('branch');
866 $branchname = GetBranchName($branchcode);
868 my $branches = GetBranches();
869 if (C4::Context->boolean_preference('IndependentBranches') && C4::Context->boolean_preference('Autolocation')){
870 # we have to check they are coming from the right ip range
871 my $domain = $branches->{$branchcode}->{'branchip'};
872 if ($ip !~ /^$domain/){
874 $info{'wrongip'} = 1;
879 foreach my $br ( keys %$branches ) {
880 # now we work with the treatment of ip
881 my $domain = $branches->{$br}->{'branchip'};
882 if ( $domain && $ip =~ /^$domain/ ) {
883 $branchcode = $branches->{$br}->{'branchcode'};
885 # new op dev : add the branchprinter and branchname in the cookie
886 $branchprinter = $branches->{$br}->{'branchprinter'};
887 $branchname = $branches->{$br}->{'branchname'};
890 $session->param('number',$borrowernumber);
891 $session->param('id',$userid);
892 $session->param('cardnumber',$cardnumber);
893 $session->param('firstname',$firstname);
894 $session->param('surname',$surname);
895 $session->param('branch',$branchcode);
896 $session->param('branchname',$branchname);
897 $session->param('flags',$userflags);
898 $session->param('emailaddress',$emailaddress);
899 $session->param('ip',$session->remote_addr());
900 $session->param('lasttime',time());
901 $debug and printf STDERR "AUTH_4: (%s)\t%s %s - %s\n", map {$session->param($_)} qw(cardnumber firstname surname branch) ;
903 elsif ( $return == 2 ) {
904 #We suppose the user is the superlibrarian
906 $session->param('number',0);
907 $session->param('id',C4::Context->config('user'));
908 $session->param('cardnumber',C4::Context->config('user'));
909 $session->param('firstname',C4::Context->config('user'));
910 $session->param('surname',C4::Context->config('user'));
911 $session->param('branch','NO_LIBRARY_SET');
912 $session->param('branchname','NO_LIBRARY_SET');
913 $session->param('flags',1);
914 $session->param('emailaddress', C4::Context->preference('KohaAdminEmailAddress'));
915 $session->param('ip',$session->remote_addr());
916 $session->param('lasttime',time());
919 $session->param('persona',1);
921 C4::Context::set_userenv(
922 $session->param('number'), $session->param('id'),
923 $session->param('cardnumber'), $session->param('firstname'),
924 $session->param('surname'), $session->param('branch'),
925 $session->param('branchname'), $session->param('flags'),
926 $session->param('emailaddress'), $session->param('branchprinter'),
927 $session->param('persona')
933 $info{'invalid_username_or_password'} = 1;
934 C4::Context->_unset_userenv($sessionID);
937 } # END if ( $userid = $query->param('userid') )
938 elsif ($type eq "opac") {
939 # if we are here this is an anonymous session; add public lists to it and a few other items...
940 # anonymous sessions are created only for the OPAC
941 $debug and warn "Initiating an anonymous session...";
943 # setting a couple of other session vars...
944 $session->param('ip',$session->remote_addr());
945 $session->param('lasttime',time());
946 $session->param('sessiontype','anon');
948 } # END unless ($userid)
950 # finished authentification, now respond
951 if ( $loggedin || $authnotrequired )
955 $cookie = $query->cookie(
956 -name => 'CGISESSID',
961 return ( $userid, $cookie, $sessionID, $flags );
966 # AUTH rejected, show the login/password template, after checking the DB.
970 # get the inputs from the incoming query
972 foreach my $name ( param $query) {
973 (next) if ( $name eq 'userid' || $name eq 'password' || $name eq 'ticket' );
974 my $value = $query->param($name);
975 push @inputs, { name => $name, value => $value };
978 my $LibraryNameTitle = C4::Context->preference("LibraryName");
979 $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
980 $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
982 my $template_name = ( $type eq 'opac' ) ? 'opac-auth.tmpl' : 'auth.tmpl';
983 my $template = C4::Templates::gettemplate($template_name, $type, $query );
985 branchloop => GetBranchesLoop(),
986 opaccolorstylesheet => C4::Context->preference("opaccolorstylesheet"),
987 opaclayoutstylesheet => C4::Context->preference("opaclayoutstylesheet"),
990 casAuthentication => C4::Context->preference("casAuthentication"),
991 suggestion => C4::Context->preference("suggestion"),
992 virtualshelves => C4::Context->preference("virtualshelves"),
993 LibraryName => "" . C4::Context->preference("LibraryName"),
994 LibraryNameTitle => "" . $LibraryNameTitle,
995 opacuserlogin => C4::Context->preference("opacuserlogin"),
996 OpacNav => C4::Context->preference("OpacNav"),
997 OpacNavRight => C4::Context->preference("OpacNavRight"),
998 OpacNavBottom => C4::Context->preference("OpacNavBottom"),
999 opaccredits => C4::Context->preference("opaccredits"),
1000 OpacFavicon => C4::Context->preference("OpacFavicon"),
1001 opacreadinghistory => C4::Context->preference("opacreadinghistory"),
1002 opacsmallimage => C4::Context->preference("opacsmallimage"),
1003 opaclanguagesdisplay => C4::Context->preference("opaclanguagesdisplay"),
1004 opacuserjs => C4::Context->preference("opacuserjs"),
1005 opacbookbag => "" . C4::Context->preference("opacbookbag"),
1006 OpacCloud => C4::Context->preference("OpacCloud"),
1007 OpacTopissue => C4::Context->preference("OpacTopissue"),
1008 OpacAuthorities => C4::Context->preference("OpacAuthorities"),
1009 OpacBrowser => C4::Context->preference("OpacBrowser"),
1010 opacheader => C4::Context->preference("opacheader"),
1011 TagsEnabled => C4::Context->preference("TagsEnabled"),
1012 OPACUserCSS => C4::Context->preference("OPACUserCSS"),
1013 intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),
1014 intranetstylesheet => C4::Context->preference("intranetstylesheet"),
1015 intranetbookbag => C4::Context->preference("intranetbookbag"),
1016 IntranetNav => C4::Context->preference("IntranetNav"),
1017 IntranetFavicon => C4::Context->preference("IntranetFavicon"),
1018 intranetuserjs => C4::Context->preference("intranetuserjs"),
1019 IndependentBranches=> C4::Context->preference("IndependentBranches"),
1020 AutoLocation => C4::Context->preference("AutoLocation"),
1021 wrongip => $info{'wrongip'},
1022 PatronSelfRegistration => C4::Context->preference("PatronSelfRegistration"),
1023 PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
1024 persona => C4::Context->preference("Persona"),
1025 opac_css_override => $ENV{'OPAC_CSS_OVERRIDE'},
1028 $template->param( OpacPublic => C4::Context->preference("OpacPublic"));
1029 $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1031 if($type eq 'opac'){
1032 my ($total, $pubshelves) = C4::VirtualShelves::GetSomeShelfNames(undef, 'MASTHEAD');
1034 pubshelves => $total->{pubtotal},
1035 pubshelvesloop => $pubshelves,
1041 # Is authentication against multiple CAS servers enabled?
1042 if (C4::Auth_with_cas::multipleAuth && !$casparam) {
1043 my $casservers = C4::Auth_with_cas::getMultipleAuth();
1045 foreach my $key (keys %$casservers) {
1046 push @tmplservers, {name => $key, value => login_cas_url($query, $key) . "?cas=$key" };
1049 casServersLoop => \@tmplservers
1053 casServerUrl => login_cas_url($query),
1058 invalidCasLogin => $info{'invalidCasLogin'}
1062 my $self_url = $query->url( -absolute => 1 );
1065 LibraryName => C4::Context->preference("LibraryName"),
1067 $template->param( %info );
1068 # $cookie = $query->cookie(CGISESSID => $session->id
1070 print $query->header(
1071 -type => 'text/html',
1072 -charset => 'utf-8',
1079 =head2 check_api_auth
1081 ($status, $cookie, $sessionId) = check_api_auth($query, $userflags);
1083 Given a CGI query containing the parameters 'userid' and 'password' and/or a session
1084 cookie, determine if the user has the privileges specified by C<$userflags>.
1086 C<check_api_auth> is is meant for authenticating users of web services, and
1087 consequently will always return and will not attempt to redirect the user
1090 If a valid session cookie is already present, check_api_auth will return a status
1091 of "ok", the cookie, and the Koha session ID.
1093 If no session cookie is present, check_api_auth will check the 'userid' and 'password
1094 parameters and create a session cookie and Koha session if the supplied credentials
1097 Possible return values in C<$status> are:
1101 =item "ok" -- user authenticated; C<$cookie> and C<$sessionid> have valid values.
1103 =item "failed" -- credentials are not correct; C<$cookie> and C<$sessionid> are undef
1105 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1107 =item "expired -- session cookie has expired; API user should resubmit userid and password
1113 sub check_api_auth {
1115 my $flagsrequired = shift;
1117 my $dbh = C4::Context->dbh;
1118 my $timeout = _timeout_syspref();
1120 unless (C4::Context->preference('Version')) {
1121 # database has not been installed yet
1122 return ("maintenance", undef, undef);
1124 my $kohaversion=C4::Context::KOHAVERSION;
1125 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1126 if (C4::Context->preference('Version') < $kohaversion) {
1127 # database in need of version update; assume that
1128 # no API should be called while databsae is in
1130 return ("maintenance", undef, undef);
1133 # FIXME -- most of what follows is a copy-and-paste
1134 # of code from checkauth. There is an obvious need
1135 # for refactoring to separate the various parts of
1136 # the authentication code, but as of 2007-11-19 this
1137 # is deferred so as to not introduce bugs into the
1138 # regular authentication code for Koha 3.0.
1140 # see if we have a valid session cookie already
1141 # however, if a userid parameter is present (i.e., from
1142 # a form submission, assume that any current cookie
1144 my $sessionID = undef;
1145 unless ($query->param('userid')) {
1146 $sessionID = $query->cookie("CGISESSID");
1148 if ($sessionID && not ($cas && $query->param('PT')) ) {
1149 my $session = get_session($sessionID);
1150 C4::Context->_new_userenv($sessionID);
1152 C4::Context::set_userenv(
1153 $session->param('number'), $session->param('id'),
1154 $session->param('cardnumber'), $session->param('firstname'),
1155 $session->param('surname'), $session->param('branch'),
1156 $session->param('branchname'), $session->param('flags'),
1157 $session->param('emailaddress'), $session->param('branchprinter')
1160 my $ip = $session->param('ip');
1161 my $lasttime = $session->param('lasttime');
1162 my $userid = $session->param('id');
1163 if ( $lasttime < time() - $timeout ) {
1166 C4::Context->_unset_userenv($sessionID);
1169 return ("expired", undef, undef);
1170 } elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) {
1171 # IP address changed
1173 C4::Context->_unset_userenv($sessionID);
1176 return ("expired", undef, undef);
1178 my $cookie = $query->cookie(
1179 -name => 'CGISESSID',
1180 -value => $session->id,
1183 $session->param('lasttime',time());
1184 my $flags = haspermission($userid, $flagsrequired);
1186 return ("ok", $cookie, $sessionID);
1189 C4::Context->_unset_userenv($sessionID);
1192 return ("failed", undef, undef);
1196 return ("expired", undef, undef);
1200 my $userid = $query->param('userid');
1201 my $password = $query->param('password');
1202 my ($return, $cardnumber);
1205 if ($cas && $query->param('PT')) {
1207 $debug and print STDERR "## check_api_auth - checking CAS\n";
1208 # In case of a CAS authentication, we use the ticket instead of the password
1209 my $PT = $query->param('PT');
1210 ($return,$cardnumber,$userid) = check_api_auth_cas($dbh, $PT, $query); # EXTERNAL AUTH
1212 # User / password auth
1213 unless ($userid and $password) {
1214 # caller did something wrong, fail the authenticateion
1215 return ("failed", undef, undef);
1217 ( $return, $cardnumber ) = checkpw( $dbh, $userid, $password, $query );
1220 if ($return and haspermission( $userid, $flagsrequired)) {
1221 my $session = get_session("");
1222 return ("failed", undef, undef) unless $session;
1224 my $sessionID = $session->id;
1225 C4::Context->_new_userenv($sessionID);
1226 my $cookie = $query->cookie(
1227 -name => 'CGISESSID',
1228 -value => $sessionID,
1231 if ( $return == 1 ) {
1233 $borrowernumber, $firstname, $surname,
1234 $userflags, $branchcode, $branchname,
1235 $branchprinter, $emailaddress
1239 "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=?"
1241 $sth->execute($userid);
1243 $borrowernumber, $firstname, $surname,
1244 $userflags, $branchcode, $branchname,
1245 $branchprinter, $emailaddress
1246 ) = $sth->fetchrow if ( $sth->rows );
1248 unless ($sth->rows ) {
1249 my $sth = $dbh->prepare(
1250 "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=?"
1252 $sth->execute($cardnumber);
1254 $borrowernumber, $firstname, $surname,
1255 $userflags, $branchcode, $branchname,
1256 $branchprinter, $emailaddress
1257 ) = $sth->fetchrow if ( $sth->rows );
1259 unless ( $sth->rows ) {
1260 $sth->execute($userid);
1262 $borrowernumber, $firstname, $surname, $userflags,
1263 $branchcode, $branchname, $branchprinter, $emailaddress
1264 ) = $sth->fetchrow if ( $sth->rows );
1268 my $ip = $ENV{'REMOTE_ADDR'};
1269 # if they specify at login, use that
1270 if ($query->param('branch')) {
1271 $branchcode = $query->param('branch');
1272 $branchname = GetBranchName($branchcode);
1274 my $branches = GetBranches();
1276 foreach my $br ( keys %$branches ) {
1277 # now we work with the treatment of ip
1278 my $domain = $branches->{$br}->{'branchip'};
1279 if ( $domain && $ip =~ /^$domain/ ) {
1280 $branchcode = $branches->{$br}->{'branchcode'};
1282 # new op dev : add the branchprinter and branchname in the cookie
1283 $branchprinter = $branches->{$br}->{'branchprinter'};
1284 $branchname = $branches->{$br}->{'branchname'};
1287 $session->param('number',$borrowernumber);
1288 $session->param('id',$userid);
1289 $session->param('cardnumber',$cardnumber);
1290 $session->param('firstname',$firstname);
1291 $session->param('surname',$surname);
1292 $session->param('branch',$branchcode);
1293 $session->param('branchname',$branchname);
1294 $session->param('flags',$userflags);
1295 $session->param('emailaddress',$emailaddress);
1296 $session->param('ip',$session->remote_addr());
1297 $session->param('lasttime',time());
1298 } elsif ( $return == 2 ) {
1299 #We suppose the user is the superlibrarian
1300 $session->param('number',0);
1301 $session->param('id',C4::Context->config('user'));
1302 $session->param('cardnumber',C4::Context->config('user'));
1303 $session->param('firstname',C4::Context->config('user'));
1304 $session->param('surname',C4::Context->config('user'));
1305 $session->param('branch','NO_LIBRARY_SET');
1306 $session->param('branchname','NO_LIBRARY_SET');
1307 $session->param('flags',1);
1308 $session->param('emailaddress', C4::Context->preference('KohaAdminEmailAddress'));
1309 $session->param('ip',$session->remote_addr());
1310 $session->param('lasttime',time());
1312 C4::Context::set_userenv(
1313 $session->param('number'), $session->param('id'),
1314 $session->param('cardnumber'), $session->param('firstname'),
1315 $session->param('surname'), $session->param('branch'),
1316 $session->param('branchname'), $session->param('flags'),
1317 $session->param('emailaddress'), $session->param('branchprinter')
1319 return ("ok", $cookie, $sessionID);
1321 return ("failed", undef, undef);
1326 =head2 check_cookie_auth
1328 ($status, $sessionId) = check_api_auth($cookie, $userflags);
1330 Given a CGISESSID cookie set during a previous login to Koha, determine
1331 if the user has the privileges specified by C<$userflags>.
1333 C<check_cookie_auth> is meant for authenticating special services
1334 such as tools/upload-file.pl that are invoked by other pages that
1335 have been authenticated in the usual way.
1337 Possible return values in C<$status> are:
1341 =item "ok" -- user authenticated; C<$sessionID> have valid values.
1343 =item "failed" -- credentials are not correct; C<$sessionid> are undef
1345 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1347 =item "expired -- session cookie has expired; API user should resubmit userid and password
1353 sub check_cookie_auth {
1355 my $flagsrequired = shift;
1357 my $dbh = C4::Context->dbh;
1358 my $timeout = _timeout_syspref();
1360 unless (C4::Context->preference('Version')) {
1361 # database has not been installed yet
1362 return ("maintenance", undef);
1364 my $kohaversion=C4::Context::KOHAVERSION;
1365 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1366 if (C4::Context->preference('Version') < $kohaversion) {
1367 # database in need of version update; assume that
1368 # no API should be called while databsae is in
1370 return ("maintenance", undef);
1373 # FIXME -- most of what follows is a copy-and-paste
1374 # of code from checkauth. There is an obvious need
1375 # for refactoring to separate the various parts of
1376 # the authentication code, but as of 2007-11-23 this
1377 # is deferred so as to not introduce bugs into the
1378 # regular authentication code for Koha 3.0.
1380 # see if we have a valid session cookie already
1381 # however, if a userid parameter is present (i.e., from
1382 # a form submission, assume that any current cookie
1384 unless (defined $cookie and $cookie) {
1385 return ("failed", undef);
1387 my $sessionID = $cookie;
1388 my $session = get_session($sessionID);
1389 C4::Context->_new_userenv($sessionID);
1391 C4::Context::set_userenv(
1392 $session->param('number'), $session->param('id'),
1393 $session->param('cardnumber'), $session->param('firstname'),
1394 $session->param('surname'), $session->param('branch'),
1395 $session->param('branchname'), $session->param('flags'),
1396 $session->param('emailaddress'), $session->param('branchprinter')
1399 my $ip = $session->param('ip');
1400 my $lasttime = $session->param('lasttime');
1401 my $userid = $session->param('id');
1402 if ( $lasttime < time() - $timeout ) {
1405 C4::Context->_unset_userenv($sessionID);
1408 return ("expired", undef);
1409 } elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) {
1410 # IP address changed
1412 C4::Context->_unset_userenv($sessionID);
1415 return ("expired", undef);
1417 $session->param('lasttime',time());
1418 my $flags = haspermission($userid, $flagsrequired);
1420 return ("ok", $sessionID);
1423 C4::Context->_unset_userenv($sessionID);
1426 return ("failed", undef);
1430 return ("expired", undef);
1437 my $session = get_session($sessionID);
1439 Given a session ID, retrieve the CGI::Session object used to store
1440 the session's state. The session object can be used to store
1441 data that needs to be accessed by different scripts during a
1444 If the C<$sessionID> parameter is an empty string, a new session
1450 my $sessionID = shift;
1451 my $storage_method = C4::Context->preference('SessionStorage');
1452 my $dbh = C4::Context->dbh;
1454 if ($storage_method eq 'mysql'){
1455 $session = new CGI::Session("driver:MySQL;serializer:yaml;id:md5", $sessionID, {Handle=>$dbh});
1457 elsif ($storage_method eq 'Pg') {
1458 $session = new CGI::Session("driver:PostgreSQL;serializer:yaml;id:md5", $sessionID, {Handle=>$dbh});
1460 elsif ($storage_method eq 'memcached' && C4::Context->ismemcached){
1461 $session = new CGI::Session("driver:memcached;serializer:yaml;id:md5", $sessionID, { Memcached => C4::Context->memcached } );
1464 # catch all defaults to tmp should work on all systems
1465 $session = new CGI::Session("driver:File;serializer:yaml;id:md5", $sessionID, {Directory=>'/tmp'});
1470 # Using Bcrypt method for hashing. This can be changed to something else in future, if needed.
1472 my $password = shift;
1474 # Generate a salt if one is not passed
1475 my $settings = shift;
1476 unless( defined $settings ){ # if there are no settings, we need to create a salt and append settings
1477 # Set the cost to 8 and append a NULL
1478 $settings = '$2a$08$'.en_base64(generate_salt('weak', 16));
1481 return bcrypt($password, $settings);
1484 =head2 generate_salt
1487 my $salt = C4::Auth::generate_salt($strength, $length);
1493 For general password salting a C<$strength> of C<weak> is recommend,
1494 For generating a server-salt a C<$strength> of C<strong> is recommended
1496 'strong' uses /dev/random which may block until sufficient entropy is acheived.
1497 'weak' uses /dev/urandom and is non-blocking.
1501 C<$length> is a positive integer which specifies the desired length of the returned string
1508 # the implementation of generate_salt is loosely based on Crypt::Random::Provider::File
1510 # strength is 'strong' or 'weak'
1511 # length is number of bytes to read, positive integer
1512 my ($strength, $length) = @_;
1517 die "non-positive strength of '$strength' passed to C4::Auth::generate_salt\n";
1520 if( $strength eq "strong" ){
1521 $source = '/dev/random'; # blocking
1523 unless( $strength eq 'weak' ){
1524 warn "unsuppored strength of '$strength' passed to C4::Auth::generate_salt, defaulting to 'weak'\n";
1526 $source = '/dev/urandom'; # non-blocking
1529 sysopen SOURCE, $source, O_RDONLY
1530 or die "failed to open source '$source' in C4::Auth::generate_salt\n";
1532 # $bytes is the bytes just read
1533 # $string is the concatenation of all the bytes read so far
1534 my( $bytes, $string ) = ("", "");
1536 # keep reading until we have $length bytes in $strength
1537 while( length($string) < $length ){
1538 # return the number of bytes read, 0 (EOF), or -1 (ERROR)
1539 my $return = sysread SOURCE, $bytes, $length - length($string);
1541 # if no bytes were read, keep reading (if using /dev/random it is possible there was insufficient entropy so this may block)
1542 next unless $return;
1543 if( $return == -1 ){
1544 die "error while reading from $source in C4::Auth::generate_salt\n";
1556 my ( $dbh, $userid, $password, $query ) = @_;
1559 $debug and print STDERR "## checkpw - checking LDAP\n";
1560 my ($retval,$retcard,$retuserid) = checkpw_ldap(@_); # EXTERNAL AUTH
1561 ($retval) and return ($retval,$retcard,$retuserid);
1564 if ($cas && $query && $query->param('ticket')) {
1565 $debug and print STDERR "## checkpw - checking CAS\n";
1566 # In case of a CAS authentication, we use the ticket instead of the password
1567 my $ticket = $query->param('ticket');
1568 my ($retval,$retcard,$retuserid) = checkpw_cas($dbh, $ticket, $query); # EXTERNAL AUTH
1569 ($retval) and return ($retval,$retcard,$retuserid);
1573 return checkpw_internal(@_)
1576 sub checkpw_internal {
1577 my ( $dbh, $userid, $password ) = @_;
1581 "select password,cardnumber,borrowernumber,userid,firstname,surname,branchcode,flags from borrowers where userid=?"
1583 $sth->execute($userid);
1585 my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
1586 $surname, $branchcode, $flags )
1589 if ( checkpw_hash($password, $stored_hash) ) {
1591 C4::Context->set_userenv( "$borrowernumber", $userid, $cardnumber,
1592 $firstname, $surname, $branchcode, $flags );
1593 return 1, $cardnumber, $userid;
1598 "select password,cardnumber,borrowernumber,userid, firstname,surname,branchcode,flags from borrowers where cardnumber=?"
1600 $sth->execute($userid);
1602 my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
1603 $surname, $branchcode, $flags )
1606 if ( checkpw_hash($password, $stored_hash) ) {
1608 C4::Context->set_userenv( $borrowernumber, $userid, $cardnumber,
1609 $firstname, $surname, $branchcode, $flags );
1610 return 1, $cardnumber, $userid;
1613 if ( $userid && $userid eq C4::Context->config('user')
1614 && "$password" eq C4::Context->config('pass') )
1617 # Koha superuser account
1618 # C4::Context->set_userenv(0,0,C4::Context->config('user'),C4::Context->config('user'),C4::Context->config('user'),"",1);
1621 if ( $userid && $userid eq 'demo'
1622 && "$password" eq 'demo'
1623 && C4::Context->config('demo') )
1626 # DEMO => the demo user is allowed to do everything (if demo set to 1 in koha.conf
1627 # some features won't be effective : modify systempref, modify MARC structure,
1634 my ( $password, $stored_hash ) = @_;
1636 return if $stored_hash eq '!';
1638 # check what encryption algorithm was implemented: Bcrypt - if the hash starts with '$2' it is Bcrypt else md5
1640 if ( substr($stored_hash,0,2) eq '$2') {
1641 $hash = hash_password($password, $stored_hash);
1643 $hash = md5_base64($password);
1645 return $hash eq $stored_hash;
1650 my $authflags = getuserflags($flags, $userid, [$dbh]);
1652 Translates integer flags into permissions strings hash.
1654 C<$flags> is the integer userflags value ( borrowers.userflags )
1655 C<$userid> is the members.userid, used for building subpermissions
1656 C<$authflags> is a hashref of permissions
1663 my $dbh = @_ ? shift : C4::Context->dbh;
1666 # I don't want to do this, but if someone logs in as the database
1667 # user, it would be preferable not to spam them to death with
1668 # numeric warnings. So, we make $flags numeric.
1669 no warnings 'numeric';
1672 my $sth = $dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
1675 while ( my ( $bit, $flag, $defaulton ) = $sth->fetchrow ) {
1676 if ( ( $flags & ( 2**$bit ) ) || $defaulton ) {
1677 $userflags->{$flag} = 1;
1680 $userflags->{$flag} = 0;
1684 # get subpermissions and merge with top-level permissions
1685 my $user_subperms = get_user_subpermissions($userid);
1686 foreach my $module (keys %$user_subperms) {
1687 next if $userflags->{$module} == 1; # user already has permission for everything in this module
1688 $userflags->{$module} = $user_subperms->{$module};
1694 =head2 get_user_subpermissions
1696 $user_perm_hashref = get_user_subpermissions($userid);
1698 Given the userid (note, not the borrowernumber) of a staff user,
1699 return a hashref of hashrefs of the specific subpermissions
1700 accorded to the user. An example return is
1704 export_catalog => 1,
1705 import_patrons => 1,
1709 The top-level hash-key is a module or function code from
1710 userflags.flag, while the second-level key is a code
1713 The results of this function do not give a complete picture
1714 of the functions that a staff user can access; it is also
1715 necessary to check borrowers.flags.
1719 sub get_user_subpermissions {
1722 my $dbh = C4::Context->dbh;
1723 my $sth = $dbh->prepare("SELECT flag, user_permissions.code
1724 FROM user_permissions
1725 JOIN permissions USING (module_bit, code)
1726 JOIN userflags ON (module_bit = bit)
1727 JOIN borrowers USING (borrowernumber)
1729 $sth->execute($userid);
1731 my $user_perms = {};
1732 while (my $perm = $sth->fetchrow_hashref) {
1733 $user_perms->{$perm->{'flag'}}->{$perm->{'code'}} = 1;
1738 =head2 get_all_subpermissions
1740 my $perm_hashref = get_all_subpermissions();
1742 Returns a hashref of hashrefs defining all specific
1743 permissions currently defined. The return value
1744 has the same structure as that of C<get_user_subpermissions>,
1745 except that the innermost hash value is the description
1746 of the subpermission.
1750 sub get_all_subpermissions {
1751 my $dbh = C4::Context->dbh;
1752 my $sth = $dbh->prepare("SELECT flag, code, description
1754 JOIN userflags ON (module_bit = bit)");
1758 while (my $perm = $sth->fetchrow_hashref) {
1759 $all_perms->{$perm->{'flag'}}->{$perm->{'code'}} = $perm->{'description'};
1764 =head2 haspermission
1766 $flags = ($userid, $flagsrequired);
1768 C<$userid> the userid of the member
1769 C<$flags> is a hashref of required flags like C<$borrower-<{authflags}>
1771 Returns member's flags or 0 if a permission is not met.
1776 my ($userid, $flagsrequired) = @_;
1777 my $sth = C4::Context->dbh->prepare("SELECT flags FROM borrowers WHERE userid=?");
1778 $sth->execute($userid);
1779 my $flags = getuserflags($sth->fetchrow(), $userid);
1780 if ( $userid eq C4::Context->config('user') ) {
1781 # Super User Account from /etc/koha.conf
1782 $flags->{'superlibrarian'} = 1;
1784 elsif ( $userid eq 'demo' && C4::Context->config('demo') ) {
1785 # Demo user that can do "anything" (demo=1 in /etc/koha.conf)
1786 $flags->{'superlibrarian'} = 1;
1789 return $flags if $flags->{superlibrarian};
1791 foreach my $module ( keys %$flagsrequired ) {
1792 my $subperm = $flagsrequired->{$module};
1793 if ($subperm eq '*') {
1794 return 0 unless ( $flags->{$module} == 1 or ref($flags->{$module}) );
1796 return 0 unless ( $flags->{$module} == 1 or
1797 ( ref($flags->{$module}) and
1798 exists $flags->{$module}->{$subperm} and
1799 $flags->{$module}->{$subperm} == 1
1805 #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
1809 sub getborrowernumber {
1811 my $userenv = C4::Context->userenv;
1812 if ( defined( $userenv ) && ref( $userenv ) eq 'HASH' && $userenv->{number} ) {
1813 return $userenv->{number};
1815 my $dbh = C4::Context->dbh;
1816 for my $field ( 'userid', 'cardnumber' ) {
1818 $dbh->prepare("select borrowernumber from borrowers where $field=?");
1819 $sth->execute($userid);
1821 my ($bnumber) = $sth->fetchrow;
1828 sub ParseSearchHistoryCookie {
1830 my $search_cookie = $input->cookie('KohaOpacRecentSearches');
1831 return () unless $search_cookie;
1832 my $obj = eval { decode_json(uri_unescape($search_cookie)) };
1833 return () unless defined $obj;
1834 return () unless ref $obj eq 'ARRAY';
1838 END { } # module clean-up code here (global destructor)
1848 Crypt::Eksblowfish::Bcrypt(3)