View | Details | Raw Unified | Return to bug 12617
Collapse All | Expand All

(-)a/C4/Installer/PerlDependencies.pm (+5 lines)
Lines 742-747 our $PERL_DEPS = { Link Here
742
        'required' => '0',
742
        'required' => '0',
743
        'min_ver'  => '5.836',
743
        'min_ver'  => '5.836',
744
    },
744
    },
745
    'App::Genpass' => {
746
        'usage'    => 'Member password generation',
747
        'required' => '1',
748
        'min_ver'  => '2.23',
749
    },
745
};
750
};
746
751
747
1;
752
1;
(-)a/C4/Members.pm (+102 lines)
Lines 41-46 use Koha::DateUtils; Link Here
41
use Koha::Borrower::Debarments qw(IsDebarred);
41
use Koha::Borrower::Debarments qw(IsDebarred);
42
use Text::Unaccent qw( unac_string );
42
use Text::Unaccent qw( unac_string );
43
use Koha::AuthUtils qw(hash_password);
43
use Koha::AuthUtils qw(hash_password);
44
use App::Genpass;
44
45
45
our ($VERSION,@ISA,@EXPORT,@EXPORT_OK,$debug);
46
our ($VERSION,@ISA,@EXPORT,@EXPORT_OK,$debug);
46
47
Lines 105-110 BEGIN { Link Here
105
        GetBorrowersWithEmail
106
        GetBorrowersWithEmail
106
107
107
        HasOverdues
108
        HasOverdues
109
110
        &GenMemberPasswordSuggestion
111
        &ValidateMemberPassword
108
    );
112
    );
109
113
110
    #Modify data
114
    #Modify data
Lines 329-334 sub GetMemberDetails { Link Here
329
            SELECT borrowers.*,
333
            SELECT borrowers.*,
330
                   category_type,
334
                   category_type,
331
                   categories.description,
335
                   categories.description,
336
                   categories.passwordpolicy,
332
                   categories.BlockExpiredPatronOpacActions,
337
                   categories.BlockExpiredPatronOpacActions,
333
                   reservefee,
338
                   reservefee,
334
                   enrolmentperiod
339
                   enrolmentperiod
Lines 343-348 sub GetMemberDetails { Link Here
343
            SELECT borrowers.*,
348
            SELECT borrowers.*,
344
                   category_type,
349
                   category_type,
345
                   categories.description,
350
                   categories.description,
351
                   categories.passwordpolicy,
346
                   categories.BlockExpiredPatronOpacActions,
352
                   categories.BlockExpiredPatronOpacActions,
347
                   reservefee,
353
                   reservefee,
348
                   enrolmentperiod
354
                   enrolmentperiod
Lines 2613-2618 sub HasOverdues { Link Here
2613
    return $count;
2619
    return $count;
2614
}
2620
}
2615
2621
2622
=head2 GenMemberPasswordSuggestion
2623
2624
    $password = GenMemberPasswordSuggestion($borrowernumber);
2625
2626
Returns a new patron password suggestion based on borrowers password policy from categories
2627
2628
=cut
2629
2630
sub GenMemberPasswordSuggestion {
2631
    my $borrowernumber = shift;
2632
    my $minpasslength = C4::Context->preference('minPasswordLength');
2633
2634
    my $borrowerinfo = GetMemberDetails($borrowernumber);
2635
    my $policycode = $borrowerinfo->{'passwordpolicy'};
2636
2637
    my $pass;
2638
    my $length = int(rand(3)) + $minpasslength;
2639
    if ($policycode) {
2640
        # Generates complex passwords with numbers and uppercase, lowercase and special characters
2641
        if ($policycode eq "complex") {
2642
            my $specials = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_'];
2643
            $pass = App::Genpass->new(specials => $specials, readable => 0, length => $length);
2644
        }
2645
2646
        # Generates simple passwords with numbers and uppercase and lowercase characters
2647
        elsif ($policycode eq "alphanumeric") {
2648
            $pass = App::Genpass->new(length => $length);
2649
        }
2650
2651
        # Generates passwords with digits 0-9 only
2652
        else {
2653
            # verify => 0 due to a bug in generate() (workaround)
2654
            $pass = App::Genpass->new(lowercase => [], uppercase => [], verify => 0, length => $length);
2655
        }
2656
    }
2657
    # Defaults to empty constuctor which gives readable complex passwords
2658
    else {
2659
        $pass = App::Genpass->new(length => $length);
2660
    }
2661
2662
    return $pass->generate();
2663
}
2664
2665
=head2 ValidateMemberPassword
2666
2667
    ($success, $errorcode, $errormessage) = ValidateMemberPassword($borrowernumber, $newpassword1, $newpassword2);
2668
2669
Validates a member's password based on category password policy and/or minPasswordLength
2670
2671
=cut
2672
2673
sub ValidateMemberPassword {
2674
    my ($borrowernumber, $newpassword1, $newpassword2) = @_;
2675
    my $minpasslength = C4::Context->preference('minPasswordLength');
2676
2677
    if (!$borrowernumber) {
2678
        return (0, ,"NOBORROWER", "No borrowernumber given");
2679
    }
2680
2681
    if ((!$newpassword1 || !$newpassword2) || ($newpassword1 ne $newpassword2)) {
2682
        return (0, "NOMATCH", "The passwords do not match");
2683
    }
2684
2685
    if (length($newpassword1) < $minpasslength) {
2686
        return (0, "SHORTPASSWORD", "The password is too short");
2687
    }
2688
2689
    my $memberinfo = GetMemberDetails($borrowernumber);
2690
    my $passwordpolicy = $memberinfo->{'passwordpolicy'};
2691
2692
    if ($passwordpolicy) {
2693
        if ($passwordpolicy eq "simplenumeric") {
2694
           if ($newpassword1 !~ /[0-9]+/) {
2695
                return (0, "NOPOLICYMATCH", "Password policy: password can only contain digits 0-9");
2696
           }
2697
        }
2698
        elsif ($passwordpolicy eq "alphanumeric") {
2699
            unless ($newpassword1 =~ /[0-9]/
2700
                    && $newpassword1 =~ /[a-zA-ZöäåÖÄÅ]/
2701
                    && $newpassword1 !~ /\W/
2702
                    && $newpassword1 !~ /[_-]/) {
2703
                return (0, "NOPOLICYMATCH", "Password policy: password must contain both numbers and non-special characters (at least one of both)");
2704
            }
2705
        }
2706
        else {
2707
            unless ($newpassword1 =~ /[0-9]/
2708
                    && $newpassword1 =~ /[a-zåäö]/
2709
                    && $newpassword1 =~ /[A-ZÅÄÖ]/
2710
                    && $newpassword1 =~ /[\|\[\]\{\}!@#\$%\^&\*\(\)_\-\+\?]/) {
2711
                return (0, "NOPOLICYMATCH", "Password policy: password must contain numbers, characters and special characters (at least one of each)");
2712
            }
2713
        }
2714
    }
2715
    return 1;
2716
}
2717
2616
END { }    # module clean-up code here (global destructor)
2718
END { }    # module clean-up code here (global destructor)
2617
2719
2618
1;
2720
1;
(-)a/admin/categorie.pl (-1 / +8 lines)
Lines 45-50 use C4::Branch; Link Here
45
use C4::Output;
45
use C4::Output;
46
use C4::Dates;
46
use C4::Dates;
47
use C4::Form::MessagingPreferences;
47
use C4::Form::MessagingPreferences;
48
use C4::Members;
48
49
49
sub StringSearch  {
50
sub StringSearch  {
50
	my ($searchstring,$type)=@_;
51
	my ($searchstring,$type)=@_;
Lines 139-144 if ($op eq 'add_form') { Link Here
139
                SMSSendDriver => C4::Context->preference("SMSSendDriver"),
140
                SMSSendDriver => C4::Context->preference("SMSSendDriver"),
140
                TalkingTechItivaPhone => C4::Context->preference("TalkingTechItivaPhoneNotification"),
141
                TalkingTechItivaPhone => C4::Context->preference("TalkingTechItivaPhoneNotification"),
141
				"type_".$data->{'category_type'} => 1,
142
				"type_".$data->{'category_type'} => 1,
143
                selectedpasswordpolicy  => $data->{'passwordpolicy'},
142
                branches_loop           => \@branches_loop,
144
                branches_loop           => \@branches_loop,
143
                BlockExpiredPatronOpacActions => $data->{'BlockExpiredPatronOpacActions'},
145
                BlockExpiredPatronOpacActions => $data->{'BlockExpiredPatronOpacActions'},
144
				);
146
				);
Lines 169-174 if ($op eq 'add_form') { Link Here
169
                    hidelostitems=?,
171
                    hidelostitems=?,
170
                    overduenoticerequired=?,
172
                    overduenoticerequired=?,
171
                    category_type=?,
173
                    category_type=?,
174
                    passwordpolicy=?,
172
                    BlockExpiredPatronOpacActions=?
175
                    BlockExpiredPatronOpacActions=?
173
                WHERE categorycode=?"
176
                WHERE categorycode=?"
174
            );
177
            );
Lines 184-189 if ($op eq 'add_form') { Link Here
184
                    'hidelostitems',
187
                    'hidelostitems',
185
                    'overduenoticerequired',
188
                    'overduenoticerequired',
186
                    'category_type',
189
                    'category_type',
190
                    'password-policy',
187
                    'block_expired',
191
                    'block_expired',
188
                    'categorycode'
192
                    'categorycode'
189
                )
193
                )
Lines 219-227 if ($op eq 'add_form') { Link Here
219
                hidelostitems,
223
                hidelostitems,
220
                overduenoticerequired,
224
                overduenoticerequired,
221
                category_type,
225
                category_type,
226
                passwordpolicy,
222
                BlockExpiredPatronOpacActions
227
                BlockExpiredPatronOpacActions
223
            )
228
            )
224
            VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
229
            VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
225
        $sth->execute(
230
        $sth->execute(
226
            map { $input->param($_) } (
231
            map { $input->param($_) } (
227
                'categorycode',
232
                'categorycode',
Lines 235-240 if ($op eq 'add_form') { Link Here
235
                'hidelostitems',
240
                'hidelostitems',
236
                'overduenoticerequired',
241
                'overduenoticerequired',
237
                'category_type',
242
                'category_type',
243
                'password-policy',
238
                'block_expired'
244
                'block_expired'
239
            )
245
            )
240
        );
246
        );
Lines 331-336 if ($op eq 'add_form') { Link Here
331
				category_type           => $results->[$i]{'category_type'},
337
				category_type           => $results->[$i]{'category_type'},
332
                "type_".$results->[$i]{'category_type'} => 1,
338
                "type_".$results->[$i]{'category_type'} => 1,
333
                branches                => \@selected_branches,
339
                branches                => \@selected_branches,
340
                passwordpolicy          => $results->[$i]{'passwordpolicy'}
334
        );
341
        );
335
        if (C4::Context->preference('EnhancedMessagingPreferences')) {
342
        if (C4::Context->preference('EnhancedMessagingPreferences')) {
336
            my $brief_prefs = _get_brief_messaging_prefs($results->[$i]{'categorycode'});
343
            my $brief_prefs = _get_brief_messaging_prefs($results->[$i]{'categorycode'});
(-)a/installer/data/mysql/kohastructure.sql (+1 lines)
Lines 466-471 CREATE TABLE `categories` ( -- this table shows information related to Koha patr Link Here
466
  `reservefee` decimal(28,6) default NULL, -- cost to place holds
466
  `reservefee` decimal(28,6) default NULL, -- cost to place holds
467
  `hidelostitems` tinyint(1) NOT NULL default '0', -- are lost items shown to this category (1 for yes, 0 for no)
467
  `hidelostitems` tinyint(1) NOT NULL default '0', -- are lost items shown to this category (1 for yes, 0 for no)
468
  `category_type` varchar(1) NOT NULL default 'A', -- type of Koha patron (Adult, Child, Professional, Organizational, Statistical, Staff)
468
  `category_type` varchar(1) NOT NULL default 'A', -- type of Koha patron (Adult, Child, Professional, Organizational, Statistical, Staff)
469
  `passwordpolicy` varchar(40) default NULL,
469
  `BlockExpiredPatronOpacActions` tinyint(1) NOT NULL default '-1', -- wheither or not a patron of this category can renew books or place holds once their card has expired. 0 means they can, 1 means they cannot, -1 means use syspref BlockExpiredPatronOpacActions
470
  `BlockExpiredPatronOpacActions` tinyint(1) NOT NULL default '-1', -- wheither or not a patron of this category can renew books or place holds once their card has expired. 0 means they can, 1 means they cannot, -1 means use syspref BlockExpiredPatronOpacActions
470
  PRIMARY KEY  (`categorycode`),
471
  PRIMARY KEY  (`categorycode`),
471
  UNIQUE KEY `categorycode` (`categorycode`)
472
  UNIQUE KEY `categorycode` (`categorycode`)
(-)a/installer/data/mysql/updatedatabase.pl (+11 lines)
Lines 8624-8629 if (CheckVersion($DBversion)) { Link Here
8624
    SetVersion($DBversion);
8624
    SetVersion($DBversion);
8625
}
8625
}
8626
8626
8627
$DBversion = "3.16.00.XXX";
8628
if ( CheckVersion($DBversion) ) {
8629
8630
    $dbh->do("ALTER TABLE categories
8631
              ADD COLUMN passwordpolicy VARCHAR(40) DEFAULT NULL
8632
    ");
8633
    print "Upgrade to KD-156 done \n";
8634
    SetVersion ($DBversion);
8635
}
8636
8637
8627
=head1 FUNCTIONS
8638
=head1 FUNCTIONS
8628
8639
8629
=head2 TableExists($table)
8640
=head2 TableExists($table)
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categorie.tt (+20 lines)
Lines 25-30 Link Here
25
    if ( $("#branches option:selected").length < 1 ) {
25
    if ( $("#branches option:selected").length < 1 ) {
26
        $("#branches option:first").attr("selected", "selected");
26
        $("#branches option:first").attr("selected", "selected");
27
    }
27
    }
28
29
    var selectedPassPolicy = "[% selectedpasswordpolicy %]";
30
    if (selectedPassPolicy) {
31
        $("#password-policy").val(selectedPassPolicy);
32
    }
28
});
33
});
29
	function isNotNull(f,noalert) {
34
	function isNotNull(f,noalert) {
30
		if (f.value.length ==0) {
35
		if (f.value.length ==0) {
Lines 189-194 Link Here
189
        <span>Select All if this category type must to be displayed all the time. Otherwise select librairies you want to associate with this value.
194
        <span>Select All if this category type must to be displayed all the time. Otherwise select librairies you want to associate with this value.
190
        </span>
195
        </span>
191
    </li>
196
    </li>
197
    <li>
198
        <label for="password-policy">Category password policy</label>
199
        <select name="password-policy" id="password-policy">
200
            <option value=""></option>
201
            <option value="complex">Complex</option>
202
            <option value="alphanumeric">Alphanumeric</option>
203
            <option value="simplenumeric">Numbers only</option>
204
        </select>
205
        <span>
206
            Selecting a password policy for a category affects both automatically created suggested passwords and enfo$
207
            of rules.
208
        </span>
209
    </li>
192
    <li><label for="block_expired">Block expired patrons</label>
210
    <li><label for="block_expired">Block expired patrons</label>
193
        <select name="block_expired" id="block_expired">
211
        <select name="block_expired" id="block_expired">
194
            [% IF ( BlockExpiredPatronOpacActions == -1  ) %]
212
            [% IF ( BlockExpiredPatronOpacActions == -1  ) %]
Lines 309-314 Confirm deletion of category [% categorycode |html %][% END %]</legend> Link Here
309
            <th scope="col">Messaging</th>
327
            <th scope="col">Messaging</th>
310
            [% END %]
328
            [% END %]
311
            <th scope="col">Branches limitations</th>
329
            <th scope="col">Branches limitations</th>
330
            <th scope="col">Password policy</th>
312
            <th scope="col">&nbsp; </th>
331
            <th scope="col">&nbsp; </th>
313
            <th scope="col">&nbsp; </th>
332
            <th scope="col">&nbsp; </th>
314
        </tr>
333
        </tr>
Lines 382-387 Confirm deletion of category [% categorycode |html %][% END %]</legend> Link Here
382
                                No limitation
401
                                No limitation
383
                            [% END %]
402
                            [% END %]
384
                        </td>
403
                        </td>
404
                        <td>[% loo.passwordpolicy %]</td>
385
                        <td><a href="[% loo.script_name %]?op=add_form&amp;categorycode=[% loo.categorycode |uri %]">Edit</a></td>
405
                        <td><a href="[% loo.script_name %]?op=add_form&amp;categorycode=[% loo.categorycode |uri %]">Edit</a></td>
386
                        <td><a href="[% loo.script_name %]?op=delete_confirm&amp;categorycode=[% loo.categorycode |uri %]">Delete</a></td>
406
                        <td><a href="[% loo.script_name %]?op=delete_confirm&amp;categorycode=[% loo.categorycode |uri %]">Delete</a></td>
387
		</tr>
407
		</tr>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member-password.tt (-4 / +7 lines)
Lines 48-65 Link Here
48
		<div class="dialog alert">
48
		<div class="dialog alert">
49
		<h4>The following errors have occurred:</h4>
49
		<h4>The following errors have occurred:</h4>
50
		<ul>
50
		<ul>
51
		[% IF ( BADUSERID ) %]
51
		[% IF ( errors.BADUSERID ) %]
52
        <li>You have entered a username that already exists. Please choose another one.</li>
52
        <li>You have entered a username that already exists. Please choose another one.</li>
53
		[% END %]
53
		[% END %]
54
		[% IF ( SHORTPASSWORD ) %]
54
		[% IF ( errors.SHORTPASSWORD ) %]
55
		<li><strong>The password entered is too short</strong>. Password must be at least [% minPasswordLength %] characters.</li>
55
		<li><strong>The password entered is too short</strong>. Password must be at least [% minPasswordLength %] characters.</li>
56
		[% END %]
56
		[% END %]
57
		[% IF ( NOPERMISSION ) %]
57
		[% IF ( errors.NOPERMISSION ) %]
58
		<li>You do not have permission to edit this patron's login information.</li>
58
		<li>You do not have permission to edit this patron's login information.</li>
59
		[% END %]
59
		[% END %]
60
		[% IF ( NOMATCH ) %]
60
		[% IF ( errors.NOMATCH ) %]
61
		<li><strong>The passwords entered do not match</strong>. Please re-enter the new password.</li>
61
		<li><strong>The passwords entered do not match</strong>. Please re-enter the new password.</li>
62
		[% END %]
62
		[% END %]
63
        [% IF ( errors.NOPOLICYMATCH ) %]
64
        <li><strong>[% errors.NOPOLICYMATCH %]</strong>. Please re-enter the new password.</li>
65
        [% END %]
63
		</ul>
66
		</ul>
64
		</div>
67
		</div>
65
	[% END %]
68
	[% END %]
(-)a/members/member-password.pl (-22 / +14 lines)
Lines 41-61 $flagsrequired->{borrowers}=1; Link Here
41
my $member=$input->param('member');
41
my $member=$input->param('member');
42
my $cardnumber = $input->param('cardnumber');
42
my $cardnumber = $input->param('cardnumber');
43
my $destination = $input->param('destination');
43
my $destination = $input->param('destination');
44
my @errors;
44
my %errors;
45
my ($bor)=GetMember('borrowernumber' => $member);
45
my ($bor)=GetMember('borrowernumber' => $member);
46
if(( $member ne $loggedinuser ) && ($bor->{'category_type'} eq 'S' ) ) {
46
if(( $member ne $loggedinuser ) && ($bor->{'category_type'} eq 'S' ) ) {
47
	push(@errors,'NOPERMISSION') unless($staffflags->{'superlibrarian'} || $staffflags->{'staffaccess'} );
47
	$errors{'NOPERMISSION'} = 1 unless($staffflags->{'superlibrarian'} || $staffflags->{'staffaccess'} );
48
	# need superlibrarian for koha-conf.xml fakeuser.
48
	# need superlibrarian for koha-conf.xml fakeuser.
49
}
49
}
50
my $newpassword = $input->param('newpassword');
50
my $newpassword = $input->param('newpassword');
51
my $newpassword2 = $input->param('newpassword2');
51
my $newpassword2 = $input->param('newpassword2');
52
52
53
push(@errors,'NOMATCH') if ( ( $newpassword && $newpassword2 ) && ($newpassword ne $newpassword2) );
53
if ($newpassword) {
54
54
    my ($success, $errorcode, $errormessage) = ValidateMemberPassword($member, $newpassword, $newpassword2);
55
my $minpw = C4::Context->preference('minPasswordLength');
55
    if ($errorcode) {
56
push(@errors,'SHORTPASSWORD') if( $newpassword && $minpw && (length($newpassword) < $minpw ) );
56
        $errors{$errorcode} = $errormessage;
57
    }
58
}
57
59
58
if ( $newpassword  && !scalar(@errors) ) {
60
if ( $newpassword  && !scalar(keys %errors) ) {
59
    my $digest=Koha::AuthUtils::hash_password($input->param('newpassword'));
61
    my $digest=Koha::AuthUtils::hash_password($input->param('newpassword'));
60
    my $uid = $input->param('newuserid');
62
    my $uid = $input->param('newuserid');
61
    my $dbh=C4::Context->dbh;
63
    my $dbh=C4::Context->dbh;
Lines 67-84 if ( $newpassword && !scalar(@errors) ) { Link Here
67
		    print $input->redirect("/cgi-bin/koha/members/moremember.pl?borrowernumber=$member");
69
		    print $input->redirect("/cgi-bin/koha/members/moremember.pl?borrowernumber=$member");
68
		}
70
		}
69
    } else {
71
    } else {
70
			push(@errors,'BADUSERID');
72
            $errors{'BADUSERID'} = 1;
71
    }
73
    }
72
} else {
74
} else {
73
    my $userid = $bor->{'userid'};
75
    my $userid = $bor->{'userid'};
74
76
75
    my $chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
77
    my $defaultnewpassword = GenMemberPasswordSuggestion($member);
76
    my $length=int(rand(2))+C4::Context->preference("minPasswordLength");
77
    my $defaultnewpassword='';
78
    for (my $i=0; $i<$length; $i++) {
79
	$defaultnewpassword.=substr($chars, int(rand(length($chars))),1);
80
    }
81
82
	$template->param( defaultnewpassword => $defaultnewpassword );
78
	$template->param( defaultnewpassword => $defaultnewpassword );
83
}
79
}
84
    if ( $bor->{'category_type'} eq 'C') {
80
    if ( $bor->{'category_type'} eq 'C') {
Lines 122-137 if (C4::Context->preference('ExtendedPatronAttributes')) { Link Here
122
	    destination => $destination,
118
	    destination => $destination,
123
		is_child        => ($bor->{'category_type'} eq 'C'),
119
		is_child        => ($bor->{'category_type'} eq 'C'),
124
		activeBorrowerRelationship => (C4::Context->preference('borrowerRelationship') ne ''),
120
		activeBorrowerRelationship => (C4::Context->preference('borrowerRelationship') ne ''),
125
        minPasswordLength => $minpw,
121
        minPasswordLength => C4::Context->preference('minPasswordLength'),
126
        RoutingSerials => C4::Context->preference('RoutingSerials'),
122
        RoutingSerials => C4::Context->preference('RoutingSerials'),
123
        errors => \%errors
127
	);
124
	);
128
125
129
if( scalar(@errors )){
126
if( scalar(keys %errors )){
130
	$template->param( errormsg => 1 );
127
	$template->param( errormsg => 1 );
131
	foreach my $error (@errors) {
132
        $template->param($error) || $template->param( $error => 1);
133
	}
134
135
}
128
}
136
129
137
output_html_with_http_headers $input, $cookie, $template->output;
130
output_html_with_http_headers $input, $cookie, $template->output;
138
- 

Return to bug 12617