Project

General

Profile

1
#!/usr/bin/perl -w
2
#
3
#  '$RCSfile$'
4
#  Copyright: 2001 Regents of the University of California 
5
#
6
#   '$Author: tao $'
7
#     '$Date: 2014-10-06 17:42:25 -0700 (Mon, 06 Oct 2014) $'
8
# '$Revision: 8877 $' 
9
# 
10
# This program is free software; you can redistribute it and/or modify
11
# it under the terms of the GNU General Public License as published by
12
# the Free Software Foundation; either version 2 of the License, or
13
# (at your option) any later version.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# GNU General Public License for more details.
19
#
20
# You should have received a copy of the GNU General Public License
21
# along with this program; if not, write to the Free Software
22
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
23
#
24

    
25
#
26
# This is a web-based application for allowing users to register a new
27
# account for Metacat access.  We currently only support LDAP even
28
# though metacat could potentially support other types of directories.
29

    
30
use lib '../WEB-INF/lib';
31
use strict;             # turn on strict syntax checking
32
use Template;           # load the template-toolkit module
33
use CGI qw/:standard :html3/; # load the CGI module 
34
use Net::LDAP;          # load the LDAP net libraries
35
use Net::SMTP;          # load the SMTP net libraries
36
use Digest::SHA1;       # for creating the password hash
37
use MIME::Base64;       # for creating the password hash
38
use URI;                # for parsing URL syntax
39
use Config::Properties; # for parsing Java .properties files
40
use File::Basename;     # for path name parsing
41
use DateTime;			# for parsing dates
42
use DateTime::Duration; # for substracting
43
use Captcha::reCAPTCHA; # for protection against spams
44
use Cwd 'abs_path';
45
use Scalar::Util qw(looks_like_number);
46

    
47
# Global configuration paramters
48
# This entire block (including skin parsing) could be pushed out to a separate .pm file
49
my $cgiUrl = $ENV{'SCRIPT_FILENAME'};
50
my $workingDirectory = dirname($cgiUrl);
51
my $metacatProps = "${workingDirectory}/../WEB-INF/metacat.properties";
52
my $properties = new Config::Properties();
53
unless (open (METACAT_PROPERTIES, $metacatProps)) {
54
    print "Content-type: text/html\n\n";
55
    print "Unable to locate Metacat properties. Working directory is set as " . 
56
        $workingDirectory .", is this correct?";
57
    exit(0);
58
}
59

    
60
$properties->load(*METACAT_PROPERTIES);
61

    
62
# local directory configuration
63
my $skinsDir = "${workingDirectory}/../style/skins";
64
my $templatesDir = abs_path("${workingDirectory}/../style/common/templates");
65
my $tempDir = $properties->getProperty('application.tempDir');
66

    
67
# url configuration
68
my $server = $properties->splitToTree(qr/\./, 'server');
69
my $protocol = 'http://';
70
if ( $properties->getProperty('server.httpPort') eq '443' ) {
71
	$protocol = 'https://';
72
}
73
my $serverUrl = $protocol . $properties->getProperty('server.name');
74
if ($properties->getProperty('server.httpPort') ne '80') {
75
        $serverUrl = $serverUrl . ':' . $properties->getProperty('server.httpPort');
76
}
77
my $context = $properties->getProperty('application.context');
78
my $contextUrl = $serverUrl . '/' .  $context;
79

    
80
my $metacatUrl = $contextUrl . "/metacat";
81
my $cgiPrefix = "/" . $context . "/cgi-bin";
82
my $styleSkinsPath = $contextUrl . "/style/skins";
83
my $styleCommonPath = $contextUrl . "/style/common";
84
my $ldapServerCACertFile = $workingDirectory. "/../" . $properties->getProperty('ldap.server.ca.certificate');
85

    
86
#recaptcha key information
87
my $recaptchaPublicKey=$properties->getProperty('ldap.recaptcha.publickey');
88
my $recaptchaPrivateKey=$properties->getProperty('ldap.recaptcha.privatekey');
89

    
90
my @errorMessages;
91
my $error = 0;
92

    
93
my $emailVerification= 'emailverification';
94

    
95
 my $dn_store_next_uid=$properties->getProperty('ldap.nextuid.storing.dn');
96
 my $attribute_name_store_next_uid = $properties->getProperty('ldap.nextuid.storing.attributename');
97

    
98
# Import all of the HTML form fields as variables
99
import_names('FORM');
100

    
101
# Must have a config to use Metacat
102
my $skinName = "";
103
if ($FORM::cfg) {
104
    $skinName = $FORM::cfg;
105
} elsif ($ARGV[0]) {
106
    $skinName = $ARGV[0];
107
} else {
108
    debug("No configuration set.");
109
    print "Content-type: text/html\n\n";
110
    print 'LDAPweb Error: The registry requires a skin name to continue.';
111
    exit();
112
}
113

    
114
# Metacat isn't initialized, the registry will fail in strange ways.
115
if (!($metacatUrl)) {
116
    debug("No Metacat.");
117
    print "Content-type: text/html\n\n";
118
    'Registry Error: Metacat is not initialized! Make sure' .
119
        ' MetacatUrl is set correctly in ' .  $skinName . '.properties';
120
    exit();
121
}
122

    
123
my $skinProperties = new Config::Properties();
124
if (!($skinName)) {
125
    $error = "Application misconfigured.  Please contact the administrator.";
126
    push(@errorMessages, $error);
127
} else {
128
    my $skinProps = "$skinsDir/$skinName/$skinName.properties";
129
    unless (open (SKIN_PROPERTIES, $skinProps)) {
130
        print "Content-type: text/html\n\n";
131
        print "Unable to locate skin properties at $skinProps.  Is this path correct?";
132
        exit(0);
133
    }
134
    $skinProperties->load(*SKIN_PROPERTIES);
135
}
136

    
137
my $config = $skinProperties->splitToTree(qr/\./, 'registry.config');
138

    
139
# XXX HACK: this is a temporary fix to pull out the UCNRS password property from the
140
#           NRS skin instead of metacat.properties. The intent is to prevent editing
141
#           of our core properties file, which is manipulated purely through the web.
142
#           Once organizations are editable, this section should be removed as should
143
#           the properties within nrs/nrs.properties.
144
my $nrsProperties = new Config::Properties();
145
my $nrsProps = "$skinsDir/nrs/nrs.properties";
146
unless (open (NRS_PROPERTIES, $nrsProps)) {
147
    print "Content-type: text/html\n\n";
148
    print "Unable to locate skin properties at $nrsProps.  Is this path correct?";
149
    exit(0);
150
}
151
$nrsProperties->load(*NRS_PROPERTIES);
152

    
153
my $nrsConfig = $nrsProperties->splitToTree(qr/\./, 'registry.config');
154

    
155
# XXX END HACK
156

    
157

    
158
my $searchBase;
159
my $ldapUsername;
160
my $ldapPassword;
161
# TODO: when should we use surl instead? Is there a setting promoting one over the other?
162
# TODO: the default tree for accounts should be exposed somewhere, defaulting to unaffiliated
163
my $ldapurl = $properties->getProperty('auth.url');
164

    
165
# Java uses miliseconds, Perl expects whole seconds
166
my $timeout = $properties->getProperty('ldap.connectTimeLimit') / 1000;
167

    
168
# Get the CGI input variables
169
my $query = new CGI;
170
my $debug = 1;
171

    
172
#--------------------------------------------------------------------------80c->
173
# Set up the Template Toolkit to read html form templates
174

    
175
# templates hash, imported from ldap.templates tree in metacat.properties
176
my $templates = $properties->splitToTree(qr/\./, 'ldap.templates');
177
$$templates{'header'} = $skinProperties->getProperty("registry.templates.header");
178
$$templates{'footer'} = $skinProperties->getProperty("registry.templates.footer");
179

    
180
# set some configuration options for the template object
181
my $ttConfig = {
182
             INCLUDE_PATH => $templatesDir,
183
             INTERPOLATE  => 0,
184
             POST_CHOMP   => 1,
185
             DEBUG        => 1, 
186
             };
187

    
188
# create an instance of the template
189
my $template = Template->new($ttConfig) || handleGeneralServerFailure($Template::ERROR);
190

    
191
# custom LDAP properties hash
192
my $ldapCustom = $properties->splitToTree(qr/\./, 'ldap');
193

    
194
# This is a hash which has the keys of the organization's properties 'name', 'base', 'organization'.
195
my $orgProps = $properties->splitToTree(qr/\./, 'organization');
196

    
197
#This is a hash which has the keys of the ldap sub tree names of the organizations, such as 'NCEAS', 'LTER' and 'KU', and values are real name of the organization.
198
my $orgNames = $properties->splitToTree(qr/\./, 'organization.name');
199
# pull out properties available e.g. 'name', 'base'
200
my @orgData = keys(%$orgProps);
201

    
202
my @orgList; #An array has the names (i.e, sub tree names, such as 'NCEAS', 'LTER' and 'KU')  of the all organizations in the metacat.properties. 
203
while (my ($oKey, $oVal) = each(%$orgNames)) {
204
    push(@orgList, $oKey);
205
}
206

    
207
my $authBase = $properties->getProperty("auth.base");
208
my $ldapConfig;
209
foreach my $o (@orgList) {
210
    foreach my $d (@orgData) {
211
        $ldapConfig->{$o}{$d} = $properties->getProperty("organization.$d.$o");
212
    }
213

    
214
    # XXX hack, remove after 1.9
215
    if ($o eq 'UCNRS') {
216
        $ldapConfig->{'UCNRS'}{'base'} = $nrsConfig->{'base'};
217
        $ldapConfig->{'UCNRS'}{'user'} = $nrsConfig->{'username'};
218
        $ldapConfig->{'UCNRS'}{'password'} = $nrsConfig->{'password'};
219
    }
220

    
221
    # set default base
222
    if (!$ldapConfig->{$o}{'base'}) {
223
        $ldapConfig->{$o}{'base'} = $authBase;
224
    }
225

    
226
    # include filter information. By default, our filters are 'o=$name', e.g. 'o=NAPIER'
227
    # these can be overridden by specifying them in metacat.properties. Non-default configs
228
    # such as UCNRS must specify all LDAP properties.
229
    if ($ldapConfig->{$o}{'base'} eq $authBase) {
230
        my $filter = "o=$o";
231
        if (!$ldapConfig->{$o}{'org'}) {
232
            $ldapConfig->{$o}{'org'} = $filter;
233
        }
234
        if (!$ldapConfig->{$o}{'filter'}) {
235
            #$ldapConfig->{$o}{'filter'} = $filter;
236
            $ldapConfig->{$o}{'filter'} = $ldapConfig->{$o}{'org'};
237
        }
238
        # also include DN, which is just org + base
239
        if ($ldapConfig->{$o}{'org'}) {
240
            $ldapConfig->{$o}{'dn'} = $ldapConfig->{$o}{'org'} . "," . $ldapConfig->{$o}{'base'};
241
        }
242
    } else {
243
        $ldapConfig->{$o}{'dn'} = $ldapConfig->{$o}{'base'};
244
    }
245
    
246
    # set LDAP administrator user account
247
    if (!$ldapConfig->{$o}{'user'}) {
248
        $ldapConfig->{$o}{'user'} = $ldapConfig->{'unaffiliated'}{'user'};
249
    }
250
    # check for a fully qualified LDAP name. If it doesn't exist, append base.
251
    my @userParts = split(',', $ldapConfig->{$o}{'user'});
252
    if (scalar(@userParts) == 1) {
253
        $ldapConfig->{$o}{'user'} = $ldapConfig->{$o}{'user'} . "," . $ldapConfig->{$o}{'base'};
254
    }
255

    
256
    if (!$ldapConfig->{$o}{'password'}) {
257
        $ldapConfig->{$o}{'password'} = $ldapConfig->{'unaffiliated'}{'password'};
258
    }
259
}
260

    
261
### Determine the display organization list (such as NCEAS, Account ) in the ldap template files
262
my $displayOrgListStr;
263
$displayOrgListStr = $skinProperties->getProperty("ldap.templates.organizationList") or $displayOrgListStr = $properties->getProperty('ldap.templates.organizationList');
264
debug("the string of the org from properties : " . $displayOrgListStr);
265
my @displayOrgList = split(';', $displayOrgListStr);
266

    
267
my @validDisplayOrgList; #this array contains the org list which will be shown in the templates files.
268

    
269
my %orgNamesHash = %$orgNames;
270
foreach my $element (@displayOrgList) {
271
    if(exists $orgNamesHash{$element}) {
272
         my $label = $ldapConfig->{$element}{'label'};
273
         my %displayHash;
274
         $displayHash{$element} = $label;
275
         debug("push a hash containing the key " . $element . "with the value label" . $label . " into the display array");
276
         #if the name is found in the organization part of metacat.properties, put it into the valid array
277
         push(@validDisplayOrgList, \%displayHash);
278
    } 
279
    
280
}
281

    
282
if(!@validDisplayOrgList) {
283
     my $sender;
284
     $sender = $skinProperties->getProperty("email.sender") or $sender = $properties->getProperty('email.sender');
285
    print "Content-type: text/html\n\n";
286
    print "The value of property ldap.templates.organizationList in " 
287
     . $skinName . ".properties file or metacat.properties file (if the property doesn't exist in the " 
288
     . $skinName . ".properties file) is invalid. Please send the information to ". $sender;
289
    exit(0);
290
}
291

    
292

    
293
#--------------------------------------------------------------------------80c->
294
# Define the main program logic that calls subroutines to do the work
295
#--------------------------------------------------------------------------80c->
296

    
297
# The processing step we are handling
298
my $stage = $query->param('stage') || $templates->{'stage'};
299

    
300
my $cfg = $query->param('cfg');
301
debug("started with stage $stage, cfg $cfg");
302

    
303
# define the possible stages
304
my %stages = (
305
              'initregister'      => \&handleInitRegister,
306
              'register'          => \&handleRegister,
307
              'registerconfirmed' => \&handleRegisterConfirmed,
308
              'simplesearch'      => \&handleSimpleSearch,
309
              'initaddentry'      => \&handleInitAddEntry,
310
              'addentry'          => \&handleAddEntry,
311
              'initmodifyentry'   => \&handleInitModifyEntry,
312
              'modifyentry'       => \&handleModifyEntry,
313
              'changepass'        => \&handleChangePassword,
314
              'initchangepass'    => \&handleInitialChangePassword,
315
              'resetpass'         => \&handleResetPassword,
316
              'initresetpass'     => \&handleInitialResetPassword,
317
              'emailverification' => \&handleEmailVerification,
318
              'lookupname'        => \&handleLookupName,
319
              'searchnamesbyemail'=> \&handleSearchNameByEmail,
320
              #'getnextuid'        => \&getExistingHighestUidNum,
321
             );
322

    
323
# call the appropriate routine based on the stage
324
if ( $stages{$stage} ) {
325
  $stages{$stage}->();
326
} else {
327
  &handleResponseMessage();
328
}
329

    
330
#--------------------------------------------------------------------------80c->
331
# Define the subroutines to do the work
332
#--------------------------------------------------------------------------80c->
333

    
334
sub clearTemporaryAccounts {
335
	
336
    #search accounts that have expired
337
	my $org = $query->param('o'); 
338
    my $ldapUsername = $ldapConfig->{$org}{'user'};
339
    my $ldapPassword = $ldapConfig->{$org}{'password'};
340
    my $orgAuthBase = $ldapConfig->{$org}{'base'};
341
    my $orgExpiration = $ldapConfig->{$org}{'expiration'};
342
    my $tmpSearchBase = 'dc=tmp,' . $orgAuthBase; 
343
	
344
	my $dt = DateTime->now;
345
	$dt->subtract( hours => $orgExpiration );
346
	my $expirationDate = $dt->ymd("") . $dt->hms("") . "Z";
347
    my $filter = "(&(objectClass=inetOrgPerson)(createTimestamp<=" . $expirationDate . "))";
348
    debug("Clearing expired accounts with filter: " . $filter . ", base: " . $tmpSearchBase);    
349
    my @attrs = [ 'uid', 'o', 'ou', 'cn', 'mail', 'telephoneNumber', 'title' ];
350

    
351
    my $ldap;
352
    my $mesg;
353
    
354
    my $dn;
355

    
356
    #if main ldap server is down, a html file containing warning message will be returned
357
    debug("clearTemporaryAccounts: connecting to $ldapurl, $timeout");
358
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
359
    if ($ldap) {
360
    	$ldap->start_tls( verify => 'require',
361
                      cafile => $ldapServerCACertFile);
362
        $ldap->bind( version => 3, dn => $ldapUsername, password => $ldapPassword ); 
363
		$mesg = $ldap->search (
364
			base   => $tmpSearchBase,
365
			filter => $filter,
366
			attrs => \@attrs,
367
		);
368
	    if ($mesg->count() > 0) {
369
			my $entry;
370
			foreach $entry ($mesg->all_entries) { 
371
            	$dn = $entry->dn();
372
            	# remove the entry
373
   				debug("Removing expired account: " . $dn);
374
            	$ldap->delete($dn);
375
			}
376
        }
377
    	$ldap->unbind;   # take down session
378
    }
379

    
380
    return 0;
381
}
382

    
383
sub fullTemplate {
384
    my $templateList = shift;
385
    my $templateVars = setVars(shift);
386
    my $c = Captcha::reCAPTCHA->new;
387
    my $captcha = 'captcha';
388
    #my $error=null;
389
    my $use_ssl= 1;
390
    #my $options=null;
391
    # use the AJAX style, only need to provide the public key to the template
392
    $templateVars->{'recaptchaPublicKey'} = $recaptchaPublicKey;
393
    #$templateVars->{$captcha} = $c->get_html($recaptchaPublicKey,undef, $use_ssl, undef);
394
    $template->process( $templates->{'header'}, $templateVars );
395
    foreach my $tmpl (@{$templateList}) {
396
        $template->process( $templates->{$tmpl}, $templateVars );
397
    }
398
    $template->process( $templates->{'footer'}, $templateVars );
399
}
400

    
401

    
402
#
403
# Initialize a form for a user to request the account name associated with an email address
404
#
405
sub handleLookupName {
406
    
407
    print "Content-type: text/html\n\n";
408
    # process the template files:
409
    fullTemplate(['lookupName']); 
410
    exit();
411
}
412

    
413
#
414
# Handle the user's request to look up account names with a specified email address.
415
# This relates to "Forget your user name"
416
#
417
sub handleSearchNameByEmail{
418

    
419
    print "Content-type: text/html\n\n";
420
   
421
    my $allParams = {'mail' => $query->param('mail')};
422
    my @requiredParams = ('mail');
423
    if (! paramsAreValid(@requiredParams)) {
424
        my $errorMessage = "Required information is missing. " .
425
            "Please fill in all required fields and resubmit the form.";
426
        fullTemplate(['lookupName'], { allParams => $allParams,
427
                                     errorMessage => $errorMessage });
428
        exit();
429
    }
430
    my $mail = $query->param('mail');
431
    
432
    #search accounts with the specified emails 
433
    $searchBase = $authBase; 
434
    my $filter = "(mail=" . $mail . ")";
435
    my @attrs = [ 'uid', 'o', 'ou', 'cn', 'mail', 'telephoneNumber', 'title' ];
436
    my $notHtmlFormat = 1;
437
    my $found = findExistingAccounts($ldapurl, $searchBase, $filter, \@attrs, $notHtmlFormat);
438
    my $accountInfo;
439
    if ($found) {
440
        $accountInfo = $found;
441
    } else {
442
        $accountInfo = "There are no accounts associated with the email " . $mail . ".\n";
443
    }
444

    
445
    my $mailhost = $properties->getProperty('email.mailhost');
446
    my $sender;
447
    $sender = $skinProperties->getProperty("email.sender") or $sender = $properties->getProperty('email.sender');
448
    debug("the sender is " . $sender);
449
    my $recipient = $query->param('mail');
450
    # Send the email message to them
451
    my $smtp = Net::SMTP->new($mailhost) or do {  
452
                                                  fullTemplate( ['lookupName'], {allParams => $allParams, 
453
                                                                errorMessage => "Our mail server currently is experiencing some difficulties. Please contact " . 
454
                                                                $skinProperties->getProperty("email.recipient") . "." });  
455
                                                  exit(0);
456
                                               };
457
    $smtp->mail($sender);
458
    $smtp->to($recipient);
459

    
460
    my $message = <<"     ENDOFMESSAGE";
461
    To: $recipient
462
    From: $sender
463
    Subject: Your Account Information
464
        
465
    Somebody (hopefully you) looked up the account information associated with the email address.  
466
    Here is the account information:
467
    
468
    $accountInfo
469

    
470
    Thanks,
471
        $sender
472
    
473
     ENDOFMESSAGE
474
     $message =~ s/^[ \t\r\f]+//gm;
475
    
476
     $smtp->data($message);
477
     $smtp->quit;
478
     fullTemplate( ['lookupNameSuccess'] );
479
    
480
}
481

    
482

    
483
#
484
# create the initial registration form 
485
#
486
sub handleInitRegister {
487
  my $vars = shift;
488
  print "Content-type: text/html\n\n";
489
  # process the template files:
490
  fullTemplate(['register'], {stage => "register"}); 
491
  exit();
492
}
493

    
494

    
495

    
496
#
497
# process input from the register stage, which occurs when
498
# a user submits form data to create a new account
499
#
500
sub handleRegister {
501
    
502
    #print "Content-type: text/html\n\n";
503
    if ($query->param('o') =~ "LTER") {
504
      print "Content-type: text/html\n\n";
505
      fullTemplate( ['registerLter'] );
506
      exit(0);
507
    } 
508
    
509
    my $allParams = { 'givenName' => $query->param('givenName'), 
510
                      'sn' => $query->param('sn'),
511
                      'o' => $query->param('o'), 
512
                      'mail' => $query->param('mail'), 
513
                      'uid' => $query->param('uid'), 
514
                      'userPassword' => $query->param('userPassword'), 
515
                      'userPassword2' => $query->param('userPassword2'), 
516
                      'title' => $query->param('title'), 
517
                      'telephoneNumber' => $query->param('telephoneNumber') };
518
    
519
    # Check the recaptcha
520
    my $c = Captcha::reCAPTCHA->new;
521
    my $challenge = $query->param('recaptcha_challenge_field');
522
    my $response = $query->param('recaptcha_response_field');
523
    # Verify submission
524
    my $result = $c->check_answer(
525
        $recaptchaPrivateKey, $ENV{'REMOTE_ADDR'},
526
        $challenge, $response
527
    );
528

    
529
    if ( $result->{is_valid} ) {
530
        #print "Yes!";
531
        #exit();
532
    }
533
    else {
534
        print "Content-type: text/html\n\n";
535
        my $errorMessage = "The verification code is wrong. Please input again.";
536
        fullTemplate(['register'], { stage => "register",
537
                                     allParams => $allParams,
538
                                     errorMessage => $errorMessage });
539
        exit();
540
    }
541
    
542
    
543
    # Check that all required fields are provided and not null
544
    my @requiredParams = ( 'givenName', 'sn', 'o', 'mail', 
545
                           'uid', 'userPassword', 'userPassword2');
546
    if (! paramsAreValid(@requiredParams)) {
547
        print "Content-type: text/html\n\n";
548
        my $errorMessage = "Required information is missing. " .
549
            "Please fill in all required fields and resubmit the form.";
550
        fullTemplate(['register'], { stage => "register",
551
                                     allParams => $allParams,
552
                                     errorMessage => $errorMessage });
553
        exit();
554
    } else {
555
         if ($query->param('userPassword') ne $query->param('userPassword2')) {
556
            print "Content-type: text/html\n\n";
557
            my $errorMessage = "The passwords do not match. Try again.";
558
            fullTemplate( ['registerFailed', 'register'], { stage => "register",
559
                                                            allParams => $allParams,
560
                                                            errorMessage => $errorMessage });
561
            exit();
562
        }
563
        my $o = $query->param('o');    
564
        $searchBase = $ldapConfig->{$o}{'base'};  
565
    }
566
    
567
    # Remove any expired temporary accounts for this subtree before continuing
568
    clearTemporaryAccounts();
569
    
570
    # Check if the uid was taken in the production space
571
    my @attrs = [ 'uid', 'o', 'ou', 'cn', 'mail', 'telephoneNumber', 'title' ];
572
    my $uidExists;
573
    my $uid=$query->param('uid');
574
    my $uidFilter = "uid=" . $uid;
575
    my $newSearchBase = $ldapConfig->{$query->param('o')}{'org'} . "," .  $searchBase;
576
    debug("the new search base is $newSearchBase");
577
    $uidExists = uidExists($ldapurl, $newSearchBase, $uidFilter, \@attrs);
578
    debug("the result of uidExists $uidExists");
579
    if($uidExists) {
580
         print "Content-type: text/html\n\n";
581
            my $errorMessage = $uidExists;
582
            fullTemplate( ['registerFailed', 'register'], { stage => "register",
583
                                                            allParams => $allParams,
584
                                                            errorMessage => $errorMessage });
585
            exit();
586
    }
587
    # Search LDAP for matching entries that already exist
588
    # Some forms use a single text search box, whereas others search per
589
    # attribute.
590
    my $filter;
591
    if ($query->param('searchField')) {
592

    
593
      $filter = "(|" . 
594
                "(uid=" . $query->param('searchField') . ") " .
595
                "(mail=" . $query->param('searchField') . ")" .
596
                "(&(sn=" . $query->param('searchField') . ") " . 
597
                "(givenName=" . $query->param('searchField') . "))" . 
598
                ")";
599
    } else {
600
      $filter = "(|" . 
601
                "(uid=" . $query->param('uid') . ") " .
602
                "(mail=" . $query->param('mail') . ")" .
603
                "(&(sn=" . $query->param('sn') . ") " . 
604
                "(givenName=" . $query->param('givenName') . "))" . 
605
                ")";
606
    }
607

    
608
    my $found = findExistingAccounts($ldapurl, $searchBase, $filter, \@attrs);
609

    
610
    # If entries match, send back a request to confirm new-user creation
611
    if ($found) {
612
      print "Content-type: text/html\n\n";
613
      fullTemplate( ['registerMatch', 'register'], { stage => "registerconfirmed",
614
                                                     allParams => $allParams,
615
                                                     foundAccounts => $found });
616
    # Otherwise, create a new user in the LDAP directory
617
    } else {
618
        createTemporaryAccount($allParams);
619
    }
620

    
621
    exit();
622
}
623

    
624
#
625
# process input from the registerconfirmed stage, which occurs when
626
# a user chooses to create an account despite similarities to other
627
# existing accounts
628
#
629
sub handleRegisterConfirmed {
630
  
631
    my $allParams = { 'givenName' => $query->param('givenName'), 
632
                      'sn' => $query->param('sn'),
633
                      'o' => $query->param('o'), 
634
                      'mail' => $query->param('mail'), 
635
                      'uid' => $query->param('uid'), 
636
                      'userPassword' => $query->param('userPassword'), 
637
                      'userPassword2' => $query->param('userPassword2'), 
638
                      'title' => $query->param('title'), 
639
                      'telephoneNumber' => $query->param('telephoneNumber') };
640
    #print "Content-type: text/html\n\n";
641
    createTemporaryAccount($allParams);
642
    exit();
643
}
644

    
645
#
646
# change a user's password upon request
647
#
648
sub handleChangePassword {
649

    
650
    print "Content-type: text/html\n\n";
651

    
652
    my $allParams = { 'test' => "1", };
653
    if ($query->param('uid')) {
654
        $$allParams{'uid'} = $query->param('uid');
655
    }
656
    if ($query->param('o')) {
657
        $$allParams{'o'} = $query->param('o');
658
        my $o = $query->param('o');
659
        
660
        $searchBase = $ldapConfig->{$o}{'base'};
661
    }
662

    
663

    
664
    # Check that all required fields are provided and not null
665
    my @requiredParams = ( 'uid', 'o', 'oldpass', 
666
                           'userPassword', 'userPassword2');
667
    if (! paramsAreValid(@requiredParams)) {
668
        my $errorMessage = "Required information is missing. " .
669
            "Please fill in all required fields and submit the form.";
670
        fullTemplate( ['changePass'], { stage => "changepass",
671
                                        allParams => $allParams,
672
                                        errorMessage => $errorMessage });
673
        exit();
674
    }
675

    
676
    # We have all of the info we need, so try to change the password
677
    if ($query->param('userPassword') =~ $query->param('userPassword2')) {
678

    
679
        my $o = $query->param('o');
680
        $searchBase = $ldapConfig->{$o}{'base'};
681
        $ldapUsername = $ldapConfig->{$o}{'user'};
682
        $ldapPassword = $ldapConfig->{$o}{'password'};
683

    
684
        my $dn = "uid=" . $query->param('uid') . "," . $ldapConfig->{$o}{'dn'};;
685
        if ($query->param('o') =~ "LTER") {
686
            fullTemplate( ['registerLter'] );
687
        } else {
688
            my $errorMessage = changePassword(
689
                    $dn, $query->param('userPassword'), 
690
                    $dn, $query->param('oldpass'), $query->param('o'));
691
            if ($errorMessage) {
692
                fullTemplate( ['changePass'], { stage => "changepass",
693
                                                allParams => $allParams,
694
                                                errorMessage => $errorMessage });
695
                exit();
696
            } else {
697
                fullTemplate( ['changePassSuccess'], { stage => "changepass",
698
                                                       allParams => $allParams });
699
                exit();
700
            }
701
        }
702
    } else {
703
        my $errorMessage = "The passwords do not match. Try again.";
704
        fullTemplate( ['changePass'], { stage => "changepass",
705
                                        allParams => $allParams,
706
                                        errorMessage => $errorMessage });
707
        exit();
708
    }
709
}
710

    
711
#
712
# change a user's password upon request - no input params
713
# only display chagepass template without any error
714
#
715
sub handleInitialChangePassword {
716
    print "Content-type: text/html\n\n";
717

    
718
    my $allParams = { 'test' => "1", };
719
    my $errorMessage = "";
720
    fullTemplate( ['changePass'], { stage => "changepass",
721
                                    errorMessage => $errorMessage });
722
    exit();
723
}
724

    
725
#
726
# reset a user's password upon request
727
#
728
sub handleResetPassword {
729

    
730
    print "Content-type: text/html\n\n";
731

    
732
    my $allParams = { 'test' => "1", };
733
    if ($query->param('uid')) {
734
        $$allParams{'uid'} = $query->param('uid');
735
    }
736
    if ($query->param('o')) {
737
        $$allParams{'o'} = $query->param('o');
738
        my $o = $query->param('o');
739
        
740
        $searchBase = $ldapConfig->{$o}{'base'};
741
        $ldapUsername = $ldapConfig->{$o}{'user'};
742
        $ldapPassword = $ldapConfig->{$o}{'password'};
743
    }
744

    
745
    # Check that all required fields are provided and not null
746
    my @requiredParams = ( 'uid', 'o' );
747
    if (! paramsAreValid(@requiredParams)) {
748
        my $errorMessage = "Required information is missing. " .
749
            "Please fill in all required fields and submit the form.";
750
        fullTemplate( ['resetPass'],  { stage => "resetpass",
751
                                        allParams => $allParams,
752
                                        errorMessage => $errorMessage });
753
        exit();
754
    }
755

    
756
    # We have all of the info we need, so try to change the password
757
    my $o = $query->param('o');
758
    my $dn = "uid=" . $query->param('uid') . "," . $ldapConfig->{$o}{'dn'};
759
    debug("handleResetPassword: dn: $dn");
760
    if ($query->param('o') =~ "LTER") {
761
        fullTemplate( ['registerLter'] );
762
        exit();
763
    } else {
764
        my $errorMessage = "";
765
        my $recipient;
766
        my $userPass;
767
        my $entry = getLdapEntry($ldapurl, $searchBase, 
768
                $query->param('uid'), $query->param('o'));
769

    
770
        if ($entry) {
771
            $recipient = $entry->get_value('mail');
772
            $userPass = getRandomPassword();
773
            $errorMessage = changePassword($dn, $userPass, $ldapUsername, $ldapPassword, $query->param('o'));
774
        } else {
775
            $errorMessage = "User not found in database.  Please try again.";
776
        }
777

    
778
        if ($errorMessage) {
779
            fullTemplate( ['resetPass'], { stage => "resetpass",
780
                                           allParams => $allParams,
781
                                           errorMessage => $errorMessage });
782
            exit();
783
        } else {
784
            my $errorMessage = sendPasswordNotification($query->param('uid'),
785
                    $query->param('o'), $userPass, $recipient, $cfg);
786
            fullTemplate( ['resetPassSuccess'], { stage => "resetpass",
787
                                                  allParams => $allParams,
788
                                                  errorMessage => $errorMessage });
789
            exit();
790
        }
791
    }
792
}
793

    
794
#
795
# reset a user's password upon request- no initial params
796
# only display resetpass template without any error
797
#
798
sub handleInitialResetPassword {
799
    print "Content-type: text/html\n\n";
800
    my $errorMessage = "";
801
    fullTemplate( ['resetPass'], { stage => "resetpass",
802
                                   errorMessage => $errorMessage });
803
    exit();
804
}
805

    
806
#
807
# Construct a random string to use for a newly reset password
808
#
809
sub getRandomPassword {
810
    my $length = shift;
811
    if (!$length) {
812
        $length = 8;
813
    }
814
    my $newPass = "";
815

    
816
    my @chars = ( "A" .. "Z", "a" .. "z", 0 .. 9, qw(! @ $ ^) );
817
    $newPass = join("", @chars[ map { rand @chars } ( 1 .. $length ) ]);
818
    return $newPass;
819
}
820

    
821
#
822
# Change a password to a new value, binding as the provided user
823
#
824
sub changePassword {
825
    my $userDN = shift;
826
    my $userPass = shift;
827
    my $bindDN = shift;
828
    my $bindPass = shift;
829
    my $o = shift;
830

    
831
    my $searchBase = $ldapConfig->{$o}{'base'};
832

    
833
    my $errorMessage = 0;
834
    my $ldap;
835

    
836
    #if main ldap server is down, a html file containing warning message will be returned
837
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
838
    
839
    if ($ldap) {
840
        $ldap->start_tls( verify => 'require',
841
                      cafile => $ldapServerCACertFile);
842
        debug("changePassword: attempting to bind to $bindDN");
843
        my $bindresult = $ldap->bind( version => 3, dn => $bindDN, 
844
                                  password => $bindPass );
845
        if ($bindresult->code) {
846
            $errorMessage = "Failed to log in. Are you sure your connection credentails are " .
847
                            "correct? Please correct and try again...";
848
            return $errorMessage;
849
        }
850

    
851
    	# Find the user here and change their entry
852
    	my $newpass = createSeededPassHash($userPass);
853
    	my $modifications = { userPassword => $newpass };
854
      debug("changePass: setting password for $userDN to $newpass");
855
    	my $result = $ldap->modify( $userDN, replace => { %$modifications });
856
    
857
    	if ($result->code()) {
858
            debug("changePass: error changing password: " . $result->error);
859
        	$errorMessage = "There was an error changing the password:" .
860
                           "<br />\n" . $result->error;
861
    	} 
862
    	$ldap->unbind;   # take down session
863
    }
864

    
865
    return $errorMessage;
866
}
867

    
868
#
869
# generate a Seeded SHA1 hash of a plaintext password
870
#
871
sub createSeededPassHash {
872
    my $secret = shift;
873

    
874
    my $salt = "";
875
    for (my $i=0; $i < 4; $i++) {
876
        $salt .= int(rand(10));
877
    }
878

    
879
    my $ctx = Digest::SHA1->new;
880
    $ctx->add($secret);
881
    $ctx->add($salt);
882
    my $hashedPasswd = '{SSHA}' . encode_base64($ctx->digest . $salt ,'');
883

    
884
    return $hashedPasswd;
885
}
886

    
887
#
888
# Look up an ldap entry for a user
889
#
890
sub getLdapEntry {
891
    my $ldapurl = shift;
892
    my $base = shift;
893
    my $username = shift;
894
    my $org = shift;
895

    
896
    my $entry = "";
897
    my $mesg;
898
    my $ldap;
899
    debug("ldap server: $ldapurl");
900

    
901
    #if main ldap server is down, a html file containing warning message will be returned
902
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
903
    
904
    if ($ldap) {
905
        $ldap->start_tls( verify => 'none');
906
        #$ldap->start_tls( verify => 'require',
907
        #              cafile => $ldapServerCACertFile);
908
    	my $bindresult = $ldap->bind;
909
    	if ($bindresult->code) {
910
        	return $entry;
911
    	}
912

    
913
        $base = $ldapConfig->{$org}{'org'} . ',' . $base;
914
        debug("getLdapEntry, searching for $base, (uid=$username)");
915
        $mesg = $ldap->search ( base   => $base, filter => "(uid=$username)");
916
    	#if($ldapConfig->{$org}{'filter'}){
917
            #debug("getLdapEntry: filter set, searching for base=$base, " .
918
                  #"(&(uid=$username)($ldapConfig->{$org}{'filter'}))");
919
        	#$mesg = $ldap->search ( base   => $base,
920
                #filter => "(&(uid=$username)($ldapConfig->{$org}{'filter'}))");
921
    	#} else {
922
            #debug("getLdapEntry: no filter, searching for $base, (uid=$username)");
923
        	#$mesg = $ldap->search ( base   => $base, filter => "(uid=$username)");
924
    	#}
925
    
926
    	if ($mesg->count > 0) {
927
        	$entry = $mesg->pop_entry;
928
        	$ldap->unbind;   # take down session
929
    	} else {
930
        	$ldap->unbind;   # take down session
931
        	# Follow references by recursive call to self
932
        	my @references = $mesg->references();
933
        	for (my $i = 0; $i <= $#references; $i++) {
934
            	my $uri = URI->new($references[$i]);
935
            	my $host = $uri->host();
936
            	my $path = $uri->path();
937
            	$path =~ s/^\///;
938
            	$entry = &getLdapEntry($host, $path, $username, $org);
939
            	if ($entry) {
940
                    debug("getLdapEntry: recursion found $host, $path, $username, $org");
941
                	return $entry;
942
            	}
943
        	}
944
    	}
945
    }
946
    return $entry;
947
}
948

    
949
# 
950
# send an email message notifying the user of the pw change
951
#
952
sub sendPasswordNotification {
953
    my $username = shift;
954
    my $org = shift;
955
    my $newPass = shift;
956
    my $recipient = shift;
957
    my $cfg = shift;
958

    
959
    my $errorMessage = "";
960
    if ($recipient) {
961
    
962
        my $mailhost = $properties->getProperty('email.mailhost');
963
        my $sender;
964
        $sender = $skinProperties->getProperty("email.sender") or $sender = $properties->getProperty('email.sender');
965
        # Send the email message to them
966
        my $smtp = Net::SMTP->new($mailhost);
967
        $smtp->mail($sender);
968
        $smtp->to($recipient);
969

    
970
        my $message = <<"        ENDOFMESSAGE";
971
        To: $recipient
972
        From: $sender
973
        Subject: Your Account Password Reset
974
        
975
        Somebody (hopefully you) requested that your account password be reset.  
976
        Your temporary password is below. Please change it as soon as possible 
977
        at: $contextUrl/style/skins/account/.
978

    
979
            Username: $username
980
        Organization: $org
981
        New Password: $newPass
982

    
983
        Thanks,
984
            $sender
985
    
986
        ENDOFMESSAGE
987
        $message =~ s/^[ \t\r\f]+//gm;
988
    
989
        $smtp->data($message);
990
        $smtp->quit;
991
    } else {
992
        $errorMessage = "Failed to send password because I " .
993
                        "couldn't find a valid email address.";
994
    }
995
    return $errorMessage;
996
}
997

    
998
#
999
# search the LDAP production space to see if a uid already exists
1000
#
1001
sub uidExists {
1002
    my $ldapurl = shift;
1003
    debug("the ldap ulr is $ldapurl");
1004
    my $base = shift;
1005
    debug("the base is $base");
1006
    my $filter = shift;
1007
    debug("the filter is $filter");
1008
    my $attref = shift;
1009
  
1010
    my $ldap;
1011
    my $mesg;
1012

    
1013
    my $foundAccounts = 0;
1014

    
1015
    #if main ldap server is down, a html file containing warning message will be returned
1016
    debug("uidExists: connecting to $ldapurl, $timeout");
1017
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
1018
    if ($ldap) {
1019
        $ldap->start_tls( verify => 'none');
1020
        #$ldap->start_tls( verify => 'require',
1021
        #              cafile => $ldapServerCACertFile);
1022
        $ldap->bind( version => 3, anonymous => 1);
1023
        $mesg = $ldap->search (
1024
            base   => $base,
1025
            filter => $filter,
1026
            attrs => @$attref,
1027
        );
1028
        debug("the message count is " . $mesg->count());
1029
        if ($mesg->count() > 0) {
1030
            $foundAccounts = "The username has been taken already by another user. Please choose a different one.";
1031
           
1032
        }
1033
        $ldap->unbind;   # take down session
1034
    } else {
1035
        $foundAccounts = "The ldap server is not running";
1036
    }
1037
    return $foundAccounts;
1038
}
1039

    
1040
#
1041
# search the LDAP directory to see if a similar account already exists
1042
#
1043
sub findExistingAccounts {
1044
    my $ldapurl = shift;
1045
    my $base = shift;
1046
    my $filter = shift;
1047
    my $attref = shift;
1048
    my $notHtmlFormat = shift;
1049
    my $ldap;
1050
    my $mesg;
1051

    
1052
    my $foundAccounts = 0;
1053

    
1054
    #if main ldap server is down, a html file containing warning message will be returned
1055
    debug("findExistingAccounts: connecting to $ldapurl, $timeout");
1056
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
1057
    if ($ldap) {
1058
    	$ldap->start_tls( verify => 'none');
1059
    	#$ldap->start_tls( verify => 'require',
1060
        #              cafile => $ldapServerCACertFile);
1061
    	$ldap->bind( version => 3, anonymous => 1);
1062
		$mesg = $ldap->search (
1063
			base   => $base,
1064
			filter => $filter,
1065
			attrs => @$attref,
1066
		);
1067

    
1068
	    if ($mesg->count() > 0) {
1069
			$foundAccounts = "";
1070
			my $entry;
1071
			foreach $entry ($mesg->all_entries) { 
1072
                # a fix to ignore 'ou=Account' properties which are not usable accounts within Metacat.
1073
                # this could be done directly with filters on the LDAP connection, instead.
1074
                #if ($entry->dn !~ /ou=Account/) {
1075
                    if($notHtmlFormat) {
1076
                        $foundAccounts .= "\nAccount: ";
1077
                    } else {
1078
                        $foundAccounts .= "<p>\n<b><u>Account:</u> ";
1079
                    }
1080
                    $foundAccounts .= $entry->dn();
1081
                    if($notHtmlFormat) {
1082
                        $foundAccounts .= "\n";
1083
                    } else {
1084
                        $foundAccounts .= "</b><br />\n";
1085
                    }
1086
                    foreach my $attribute ($entry->attributes()) {
1087
                        my $value = $entry->get_value($attribute);
1088
                        $foundAccounts .= "$attribute: ";
1089
                        $foundAccounts .= $value;
1090
                         if($notHtmlFormat) {
1091
                            $foundAccounts .= "\n";
1092
                        } else {
1093
                            $foundAccounts .= "<br />\n";
1094
                        }
1095
                    }
1096
                    if($notHtmlFormat) {
1097
                        $foundAccounts .= "\n";
1098
                    } else {
1099
                        $foundAccounts .= "</p>\n";
1100
                    }
1101
                    
1102
                #}
1103
			}
1104
        }
1105
    	$ldap->unbind;   # take down session
1106

    
1107
    	# Follow references
1108
    	my @references = $mesg->references();
1109
    	for (my $i = 0; $i <= $#references; $i++) {
1110
        	my $uri = URI->new($references[$i]);
1111
        	my $host = $uri->host();
1112
        	my $path = $uri->path();
1113
        	$path =~ s/^\///;
1114
        	my $refFound = &findExistingAccounts($host, $path, $filter, $attref, $notHtmlFormat);
1115
        	if ($refFound) {
1116
            	$foundAccounts .= $refFound;
1117
        	}
1118
    	}
1119
    }
1120

    
1121
    #print "<p>Checking referrals...</p>\n";
1122
    #my @referrals = $mesg->referrals();
1123
    #print "<p>Referrals count: ", scalar(@referrals), "</p>\n";
1124
    #for (my $i = 0; $i <= $#referrals; $i++) {
1125
        #print "<p>Referral: ", $referrals[$i], "</p>\n";
1126
    #}
1127

    
1128
    return $foundAccounts;
1129
}
1130

    
1131
#
1132
# Validate that we have the proper set of input parameters
1133
#
1134
sub paramsAreValid {
1135
    my @pnames = @_;
1136

    
1137
    my $allValid = 1;
1138
    foreach my $parameter (@pnames) {
1139
        if (!defined($query->param($parameter)) || 
1140
            ! $query->param($parameter) ||
1141
            $query->param($parameter) =~ /^\s+$/) {
1142
            $allValid = 0;
1143
        }
1144
    }
1145

    
1146
    return $allValid;
1147
}
1148

    
1149
#
1150
# Create a temporary account for a user and send an email with a link which can click for the
1151
# verification. This is used to protect the ldap server against spams.
1152
#
1153
sub createTemporaryAccount {
1154
    my $allParams = shift;
1155
    my $org = $query->param('o'); 
1156
    my $ldapUsername = $ldapConfig->{$org}{'user'};
1157
    my $ldapPassword = $ldapConfig->{$org}{'password'};
1158
    my $tmp = 1;
1159

    
1160
    ################## Search LDAP to see if the dc=tmp which stores the inactive accounts exist or not. If it doesn't exist, it will be generated
1161
    my $orgAuthBase = $ldapConfig->{$org}{'base'};
1162
    my $tmpSearchBase = 'dc=tmp,' . $orgAuthBase; 
1163
    my $tmpFilter = "dc=tmp";
1164
    my @attributes=['dc'];
1165
    my $foundTmp = searchDirectory($ldapurl, $orgAuthBase, $tmpFilter, \@attributes);
1166
    if (!$foundTmp) {
1167
        my $dn = $tmpSearchBase;
1168
        my $additions = [ 
1169
                    'dc' => 'tmp',
1170
                    'o'  => 'tmp',
1171
                    'objectclass' => ['top', 'dcObject', 'organization']
1172
                    ];
1173
        createItem($dn, $ldapUsername, $ldapPassword, $additions, $tmp, $allParams);
1174
    } else {
1175
     debug("found the tmp space");
1176
    }
1177
    
1178
    ################## Search LDAP for matching o or ou under the dc=tmp that already exist. If it doesn't exist, it will be generated
1179
    my $filter = $ldapConfig->{$org}{'filter'};   
1180
    
1181
    debug("search filer " . $filter);
1182
    debug("ldap server ". $ldapurl);
1183
    debug("sesarch base " . $tmpSearchBase);
1184
    #print "Content-type: text/html\n\n";
1185
    my @attrs = ['o', 'ou' ];
1186
    my $found = searchDirectory($ldapurl, $tmpSearchBase, $filter, \@attrs);
1187

    
1188
    my @organizationInfo = split('=', $ldapConfig->{$org}{'org'}); #split 'o=NCEAS' or something like that
1189
    my $organization = $organizationInfo[0]; # This will be 'o' or 'ou'
1190
    my $organizationName = $organizationInfo[1]; # This will be 'NCEAS' or 'Account'
1191
        
1192
    if(!$found) {
1193
        debug("generate the subtree in the dc=tmp===========================");
1194
        #need to generate the subtree o or ou
1195
        my $additions;
1196
            if($organization eq 'ou') {
1197
                $additions = [ 
1198
                    $organization   => $organizationName,
1199
                    'objectclass' => ['top', 'organizationalUnit']
1200
                    ];
1201
            
1202
            } else {
1203
                $additions = [ 
1204
                    $organization   => $organizationName,
1205
                    'objectclass' => ['top', 'organization']
1206
                    ];
1207
            
1208
            } 
1209
        my $dn=$ldapConfig->{$org}{'org'} . ',' . $tmpSearchBase;
1210
        createItem($dn, $ldapUsername, $ldapPassword, $additions, $tmp, $allParams);
1211
    } 
1212
    
1213
    ################create an account under tmp subtree 
1214
    
1215
     my $dn_store_next_uid=$properties->getProperty('ldap.nextuid.storing.dn');
1216
    my $attribute_name_store_next_uid = $properties->getProperty('ldap.nextuid.storing.attributename');
1217
    #get the next avaliable uid number. If it fails, the program will exist.
1218
    my $nextUidNumber = getNextUidNumber($ldapUsername, $ldapPassword);
1219
    if(!$nextUidNumber) {
1220
        print "Content-type: text/html\n\n";
1221
         my $sender;
1222
        $sender = $skinProperties->getProperty("email.recipient") or $sender = $properties->getProperty('email.recipient');
1223
        my $errorMessage = "The Identity Service can't get the next avaliable uid number. Please try again.  If the issue persists, please contact the administrator - $sender.
1224
                           The possible reasons are: the dn - $dn_store_next_uid or its attribute - $attribute_name_store_next_uid don't exist; the value of the attribute - $attribute_name_store_next_uid
1225
                           is not a number; or lots of users were registering and you couldn't get a lock on the dn - $dn_store_next_uid.";
1226
        fullTemplate(['register'], { stage => "register",
1227
                                     allParams => $allParams,
1228
                                     errorMessage => $errorMessage });
1229
        exit(0);
1230
    }
1231
    my $cn = join(" ", $query->param('givenName'), $query->param('sn')); 
1232
    #generate a randomstr for matching the email.
1233
    my $randomStr = getRandomPassword(16);
1234
    # Create a hashed version of the password
1235
    my $shapass = createSeededPassHash($query->param('userPassword'));
1236
    my $additions = [ 
1237
                'uid'   => $query->param('uid'),
1238
                'cn'   => $cn,
1239
                'sn'   => $query->param('sn'),
1240
                'givenName'   => $query->param('givenName'),
1241
                'mail' => $query->param('mail'),
1242
                'userPassword' => $shapass,
1243
                'employeeNumber' => $randomStr,
1244
                'uidNumber' => $nextUidNumber,
1245
                'gidNumber' => $nextUidNumber,
1246
                'loginShell' => '/sbin/nologin',
1247
                'homeDirectory' => '/dev/null',
1248
                'objectclass' => ['top', 'person', 'organizationalPerson', 
1249
                                'inetOrgPerson', 'posixAccount', 'shadowAccount' ],
1250
                $organization   => $organizationName
1251
                ];
1252
    my $gecos;
1253
    if (defined($query->param('telephoneNumber')) && 
1254
                $query->param('telephoneNumber') &&
1255
                ! $query->param('telephoneNumber') =~ /^\s+$/) {
1256
                $$additions[$#$additions + 1] = 'telephoneNumber';
1257
                $$additions[$#$additions + 1] = $query->param('telephoneNumber');
1258
                $gecos = $cn . ',,'. $query->param('telephoneNumber'). ',';
1259
    } else {
1260
        $gecos = $cn . ',,,';
1261
    }
1262
    
1263
    $$additions[$#$additions + 1] = 'gecos';
1264
    $$additions[$#$additions + 1] = $gecos;
1265
    
1266
    if (defined($query->param('title')) && 
1267
                $query->param('title') &&
1268
                ! $query->param('title') =~ /^\s+$/) {
1269
                $$additions[$#$additions + 1] = 'title';
1270
                $$additions[$#$additions + 1] = $query->param('title');
1271
    }
1272

    
1273
    
1274
    #$$additions[$#$additions + 1] = 'o';
1275
    #$$additions[$#$additions + 1] = $org;
1276
    my $dn='uid=' . $query->param('uid') . ',' . $ldapConfig->{$org}{'org'} . ',' . $tmpSearchBase;
1277
    createItem($dn, $ldapUsername, $ldapPassword, $additions, $tmp, $allParams);
1278
    
1279
    
1280
    ####################send the verification email to the user
1281
    my $link = '/' . $context . '/cgi-bin/ldapweb.cgi?cfg=' . $skinName . '&' . 'stage=' . $emailVerification . '&' . 'dn=' . $dn . '&' . 'hash=' . $randomStr . '&o=' . $org . '&uid=' . $query->param('uid'); #even though we use o=something. The emailVerification will figure the real o= or ou=something.
1282
    
1283
    my $overrideURL;
1284
    $overrideURL = $skinProperties->getProperty("email.overrideURL");
1285
    debug("the overrideURL is $overrideURL");
1286
    if (defined($overrideURL) && !($overrideURL eq '')) {
1287
    	$link = $serverUrl . $overrideURL . $link;
1288
    } else {
1289
    	$link = $serverUrl . $link;
1290
    }
1291
    
1292
    my $mailhost = $properties->getProperty('email.mailhost');
1293
    my $sender;
1294
    $sender = $skinProperties->getProperty("email.sender") or $sender = $properties->getProperty('email.sender');
1295
    debug("the sender is " . $sender);
1296
    my $recipient = $query->param('mail');
1297
    # Send the email message to them
1298
    my $smtp = Net::SMTP->new($mailhost) or do {  
1299
                                                  fullTemplate( ['registerFailed'], {errorMessage => "The temporary account " . $dn . " was created successfully. However, the vertification email can't be sent to you because the email server has some issues. Please contact " . 
1300
                                                  $skinProperties->getProperty("email.recipient") . "." });  
1301
                                                  exit(0);
1302
                                               };
1303
    $smtp->mail($sender);
1304
    $smtp->to($recipient);
1305

    
1306
    my $message = <<"     ENDOFMESSAGE";
1307
    To: $recipient
1308
    From: $sender
1309
    Subject: New Account Activation
1310
        
1311
    Somebody (hopefully you) registered an account on $contextUrl/style/skins/account/.  
1312
    Please click the following link to activate your account.
1313
    If the link doesn't work, please copy the link to your browser:
1314
    
1315
    $link
1316

    
1317
    Thanks,
1318
        $sender
1319
    
1320
     ENDOFMESSAGE
1321
     $message =~ s/^[ \t\r\f]+//gm;
1322
    
1323
     $smtp->data($message);
1324
     $smtp->quit;
1325
    debug("the link is " . $link);
1326
    fullTemplate( ['success'] );
1327
    
1328
}
1329

    
1330
#
1331
# Bind to LDAP and create a new item (a user or subtree) using the information provided
1332
# by the user
1333
#
1334
sub createItem {
1335
    my $dn = shift;
1336
    my $ldapUsername = shift;
1337
    my $ldapPassword = shift;
1338
    my $additions = shift;
1339
    my $temp = shift; #if it is for a temporary account.
1340
    my $allParams = shift;
1341
    
1342
    my @failureTemplate;
1343
    if($temp){
1344
        @failureTemplate = ['registerFailed', 'register'];
1345
    } else {
1346
        @failureTemplate = ['registerFailed'];
1347
    }
1348
    print "Content-type: text/html\n\n";
1349
    debug("the dn is " . $dn);
1350
    debug("LDAP connection to $ldapurl...");    
1351
    #if main ldap server is down, a html file containing warning message will be returned
1352
    my $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
1353
    if ($ldap) {
1354
            $ldap->start_tls( verify => 'require',
1355
                      cafile => $ldapServerCACertFile);
1356
            debug("Attempting to bind to LDAP server with dn = $ldapUsername, pwd = $ldapPassword");
1357
            $ldap->bind( version => 3, dn => $ldapUsername, password => $ldapPassword ); 
1358
            my $result = $ldap->add ( 'dn' => $dn, 'attr' => [@$additions ]);
1359
            if ($result->code()) {
1360
                fullTemplate(@failureTemplate, { stage => "register",
1361
                                                            allParams => $allParams,
1362
                                                            errorMessage => $result->error });
1363
                exist(0);
1364
                # TODO SCW was included as separate errors, test this
1365
                #$templateVars    = setVars({ stage => "register",
1366
                #                     allParams => $allParams });
1367
                #$template->process( $templates->{'register'}, $templateVars);
1368
            } else {
1369
                #fullTemplate( ['success'] );
1370
            }
1371
            $ldap->unbind;   # take down session
1372
            
1373
    } else {   
1374
         fullTemplate(@failureTemplate, { stage => "register",
1375
                                                            allParams => $allParams,
1376
                                                            errorMessage => "The ldap server is not available now. Please try it later"});
1377
         exit(0);
1378
    }
1379
  
1380
}
1381

    
1382

    
1383

    
1384

    
1385

    
1386

    
1387
#
1388
# This subroutine will handle a email verification:
1389
# If the hash string matches the one store in the ldap, the account will be
1390
# copied from the temporary space to the permanent tree and the account in 
1391
# the temporary space will be removed.
1392
sub handleEmailVerification {
1393

    
1394
    my $cfg = $query->param('cfg');
1395
    my $dn = $query->param('dn');
1396
    my $hash = $query->param('hash');
1397
    my $org = $query->param('o');
1398
    my $uid = $query->param('uid');
1399
    
1400
    my $ldapUsername;
1401
    my $ldapPassword;
1402
    #my $orgAuthBase;
1403

    
1404
    $ldapUsername = $ldapConfig->{$org}{'user'};
1405
    $ldapPassword = $ldapConfig->{$org}{'password'};
1406
    #$orgAuthBase = $ldapConfig->{$org}{'base'};
1407
    
1408
    debug("LDAP connection to $ldapurl...");    
1409
    
1410

    
1411
   print "Content-type: text/html\n\n";
1412
   #if main ldap server is down, a html file containing warning message will be returned
1413
   my $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
1414
   if ($ldap) {
1415
        $ldap->start_tls( verify => 'require',
1416
                      cafile => $ldapServerCACertFile);
1417
        $ldap->bind( version => 3, dn => $ldapUsername, password => $ldapPassword );
1418
        my $mesg = $ldap->search(base => $dn, scope => 'base', filter => '(objectClass=*)'); #This dn is with the dc=tmp. So it will find out the temporary account registered in registration step.
1419
        my $max = $mesg->count;
1420
        debug("the count is " . $max);
1421
        if($max < 1) {
1422
            $ldap->unbind;   # take down session
1423
            fullTemplate( ['verificationFailed'], {errorMessage => "No record matched the dn " . $dn . " for the activation. You probably already activated the account."});
1424
            #handleLDAPBindFailure($ldapurl);
1425
            exit(0);
1426
        } else {
1427
            #check if the hash string match
1428
            my $entry = $mesg->entry (0);
1429
            my $hashStrFromLdap = $entry->get_value('employeeNumber');
1430
            if( $hashStrFromLdap eq $hash) {
1431
                #my $additions = [ ];
1432
                #foreach my $attr ( $entry->attributes ) {
1433
                    #if($attr ne 'employeeNumber') {
1434
                        #$$additions[$#$additions + 1] = $attr;
1435
                        #$$additions[$#$additions + 1] = $entry->get_value( $attr );
1436
                    #}
1437
                #}
1438

    
1439
                
1440
                my $orgDn = $ldapConfig->{$org}{'dn'}; #the DN for the organization.
1441
                $mesg = $ldap->moddn(
1442
                            dn => $dn,
1443
                            deleteoldrdn => 1,
1444
                            newrdn => "uid=" . $uid,
1445
                            newsuperior  =>  $orgDn);
1446
                $ldap->unbind;   # take down session
1447
                if($mesg->code()) {
1448
                    fullTemplate( ['verificationFailed'], {errorMessage => "Cannot move the account from the inactive area to the ative area since " . $mesg->error()});
1449
                    exit(0);
1450
                } else {
1451
                    fullTemplate( ['verificationSuccess'] );
1452
                }
1453
                #createAccount2($dn, $ldapUsername, $ldapPassword, $additions, $tmp, $allParams);
1454
            } else {
1455
                $ldap->unbind;   # take down session
1456
                fullTemplate( ['verificationFailed'], {errorMessage => "The hash string " . $hash . " from your link doesn't match our record."});
1457
                exit(0);
1458
            }
1459
            
1460
        }
1461
    } else {   
1462
        handleLDAPBindFailure($ldapurl);
1463
        exit(0);
1464
    }
1465

    
1466
}
1467

    
1468
sub handleResponseMessage {
1469

    
1470
  print "Content-type: text/html\n\n";
1471
  my $errorMessage = "You provided invalid input to the script. " .
1472
                     "Try again please.";
1473
  fullTemplate( [], { stage => $templates->{'stage'},
1474
                      errorMessage => $errorMessage });
1475
  exit();
1476
}
1477

    
1478
#
1479
# perform a simple search against the LDAP database using 
1480
# a small subset of attributes of each dn and return it
1481
# as a table to the calling browser.
1482
#
1483
sub handleSimpleSearch {
1484

    
1485
    my $o = $query->param('o');
1486

    
1487
    my $ldapurl = $ldapConfig->{$o}{'url'};
1488
    my $searchBase = $ldapConfig->{$o}{'base'};
1489

    
1490
    print "Content-type: text/html\n\n";
1491

    
1492
    my $allParams = { 
1493
                      'cn' => $query->param('cn'),
1494
                      'sn' => $query->param('sn'),
1495
                      'gn' => $query->param('gn'),
1496
                      'o'  => $query->param('o'),
1497
                      'facsimiletelephonenumber' 
1498
                      => $query->param('facsimiletelephonenumber'),
1499
                      'mail' => $query->param('cmail'),
1500
                      'telephonenumber' => $query->param('telephonenumber'),
1501
                      'title' => $query->param('title'),
1502
                      'uid' => $query->param('uid'),
1503
                      'ou' => $query->param('ou'),
1504
                    };
1505

    
1506
    # Search LDAP for matching entries that already exist
1507
    my $filter = "(" . 
1508
                 $query->param('searchField') . "=" .
1509
                 "*" .
1510
                 $query->param('searchValue') .
1511
                 "*" .
1512
                 ")";
1513

    
1514
    my @attrs = [ 'sn', 
1515
                  'gn', 
1516
                  'cn', 
1517
                  'o', 
1518
                  'facsimiletelephonenumber', 
1519
                  'mail', 
1520
                  'telephoneNumber', 
1521
                  'title', 
1522
                  'uid', 
1523
                  'labeledURI', 
1524
                  'ou' ];
1525

    
1526
    my $found = searchDirectory($ldapurl, $searchBase, $filter, \@attrs);
1527

    
1528
    # Send back the search results
1529
    if ($found) {
1530
      fullTemplate( ('searchResults'), { stage => "searchresults",
1531
                                         allParams => $allParams,
1532
                                         foundAccounts => $found });
1533
    } else {
1534
      $found = "No entries matched your criteria.  Please try again\n";
1535

    
1536
      fullTemplate( ('searchResults'), { stage => "searchresults",
1537
                                         allParams => $allParams,
1538
                                         foundAccounts => $found });
1539
    }
1540

    
1541
    exit();
1542
}
1543

    
1544
#
1545
# search the LDAP directory to see if a similar account already exists
1546
#
1547
sub searchDirectory {
1548
    my $ldapurl = shift;
1549
    my $base = shift;
1550
    my $filter = shift;
1551
    my $attref = shift;
1552

    
1553
	my $mesg;
1554
    my $foundAccounts = 0;
1555
    
1556
    #if ldap server is down, a html file containing warning message will be returned
1557
    my $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
1558
    
1559
    if ($ldap) {
1560
    	$ldap->start_tls( verify => 'require',
1561
                      cafile => $ldapServerCACertFile);
1562
    	$ldap->bind( version => 3, anonymous => 1);
1563
    	my $mesg = $ldap->search (
1564
        	base   => $base,
1565
        	filter => $filter,
1566
        	attrs => @$attref,
1567
    	);
1568

    
1569
    	if ($mesg->count() > 0) {
1570
        	$foundAccounts = "";
1571
        	my $entry;
1572
        	foreach $entry ($mesg->sorted(['sn'])) {
1573
          		$foundAccounts .= "<tr>\n<td class=\"main\">\n";
1574
          		$foundAccounts .= "<a href=\"" unless 
1575
                    (!$entry->get_value('labeledURI'));
1576
         		 $foundAccounts .= $entry->get_value('labeledURI') unless
1577
                    (!$entry->get_value('labeledURI'));
1578
          		$foundAccounts .= "\">\n" unless 
1579
                    (!$entry->get_value('labeledURI'));
1580
          		$foundAccounts .= $entry->get_value('givenName');
1581
          		$foundAccounts .= "</a>\n" unless 
1582
                    (!$entry->get_value('labeledURI'));
1583
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1584
          		$foundAccounts .= "<a href=\"" unless 
1585
                    (!$entry->get_value('labeledURI'));
1586
          		$foundAccounts .= $entry->get_value('labeledURI') unless
1587
                    (!$entry->get_value('labeledURI'));
1588
          		$foundAccounts .= "\">\n" unless 
1589
                    (!$entry->get_value('labeledURI'));
1590
          		$foundAccounts .= $entry->get_value('sn');
1591
          		$foundAccounts .= "</a>\n";
1592
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1593
          		$foundAccounts .= $entry->get_value('mail');
1594
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1595
          		$foundAccounts .= $entry->get_value('telephonenumber');
1596
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1597
          		$foundAccounts .= $entry->get_value('title');
1598
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1599
          		$foundAccounts .= $entry->get_value('ou');
1600
          		$foundAccounts .= "\n</td>\n";
1601
          		$foundAccounts .= "</tr>\n";
1602
        	}
1603
    	}
1604
    	$ldap->unbind;   # take down session
1605
    }
1606
    return $foundAccounts;
1607
}
1608

    
1609
sub debug {
1610
    my $msg = shift;
1611
    
1612
    if ($debug) {
1613
        print STDERR "LDAPweb: $msg\n";
1614
    }
1615
}
1616

    
1617
sub handleLDAPBindFailure {
1618
    my $ldapAttemptUrl = shift;
1619
    my $primaryLdap =  $properties->getProperty('auth.url');
1620

    
1621
    if ($ldapAttemptUrl eq  $primaryLdap) {
1622
        handleGeneralServerFailure("The main LDAP server $ldapurl is down!");
1623
    } else {
1624
        debug("attempted to bind to nonresponsive LDAP server $ldapAttemptUrl, skipped.");
1625
    }
1626
}
1627

    
1628
sub handleGeneralServerFailure {
1629
    my $errorMessage = shift;
1630
    fullTemplate( ['mainServerFailure'], { errorMessage => $errorMessage });
1631
    exit(0);   
1632
   }
1633
    
1634
sub setVars {
1635
    my $paramVars = shift;
1636
    # initialize default parameters 
1637
    my $templateVars = { cfg => $cfg,
1638
                         styleSkinsPath => $contextUrl . "/style/skins",
1639
                         styleCommonPath => $contextUrl . "/style/common",
1640
                         contextUrl => $contextUrl,
1641
                         cgiPrefix => $cgiPrefix,
1642
                         orgList => \@validDisplayOrgList,
1643
                         config  => $config,
1644
    };
1645
    
1646
    # append customized params
1647
    while (my ($k, $v) = each (%$paramVars)) {
1648
        $templateVars->{$k} = $v;
1649
    }
1650
    
1651
    return $templateVars;
1652
} 
1653

    
1654
#Method to get the next avaliable uid number. We use the mechanism - http://www.rexconsulting.net/ldap-protocol-uidNumber.html
1655
sub getNextUidNumber {
1656

    
1657
    my $maxAttempt = $properties->getProperty('ldap.nextuid.maxattempt');
1658
    
1659
    my $ldapUsername = shift;
1660
    my $ldapPassword = shift;
1661
    
1662
    my $realUidNumber;
1663
    my $uidNumber;
1664
    my $entry;
1665
    my $mesg;
1666
    my $ldap;
1667
    
1668
    debug("ldap server: $ldapurl");
1669
    
1670
    #if main ldap server is down, a html file containing warning message will be returned
1671
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
1672
    
1673
    if ($ldap) {
1674
    	my $existingHighUid=getExistingHighestUidNum($ldapUsername, $ldapPassword);
1675
        $ldap->start_tls( verify => 'require',
1676
                      cafile => $ldapServerCACertFile);
1677
        my $bindresult = $ldap->bind( version => 3, dn => $ldapUsername, password => $ldapPassword);
1678
        #read the uid value stored in uidObject class
1679
        for(my $index=0; $index<$maxAttempt; $index++) {
1680
            $mesg = $ldap->search(base  => $dn_store_next_uid, filter => '(objectClass=*)');
1681
            if ($mesg->count() > 0) {
1682
                debug("Find the cn - $dn_store_next_uid");
1683
                $entry = $mesg->pop_entry;
1684
                $uidNumber = $entry->get_value($attribute_name_store_next_uid);
1685
                if($uidNumber) {
1686
                    if (looks_like_number($uidNumber)) {
1687
                        debug("uid number is $uidNumber");
1688
                        #remove the uid attribute with the read value
1689
                        my $delMesg = $ldap->modify($dn_store_next_uid, delete => { $attribute_name_store_next_uid => $uidNumber});
1690
                        if($delMesg->is_error()) {
1691
                            my $error=$delMesg->error();
1692
                            my $errorName = $delMesg->error_name();
1693
                            debug("can't remove the attribute - $error");
1694
                            debug("can't remove the attribute and the error name - $errorName");
1695
                            #can't remove the attribute with the specified value - that means somebody modify the value in another route, so try it again
1696
                        } else {
1697
                            debug("Remove the attribute successfully and write a new increased value back");
1698
                            if($existingHighUid) {
1699
                            	debug("exiting high uid exists =======================================");
1700
                            	if($uidNumber <= $existingHighUid ) {
1701
                            		debug("The stored uidNumber $uidNumber is less than or equals the used uidNumber $existingHighUid, so we will use the new number which is $existingHighUid+1");
1702
                            		$uidNumber = $existingHighUid +1;
1703
                            	} 
1704
                            }                  
1705
                            my $newValue = $uidNumber +1;
1706
                            $delMesg = $ldap->modify($dn_store_next_uid, add => {$attribute_name_store_next_uid => $newValue});
1707
                            $realUidNumber = $uidNumber;
1708
                            last;
1709
                        }
1710
                    }
1711
                    
1712
               } else {
1713
                 debug("can't find the attribute - $attribute_name_store_next_uid in the $dn_store_next_uid and we will try again");
1714
               }
1715
            } 
1716
        }
1717
        $ldap->unbind;   # take down session
1718
    }
1719
    return $realUidNumber;
1720
}
1721

    
1722
#Method to get the existing high uidNumber in the account tree.
1723
sub getExistingHighestUidNum {
1724
    my $ldapUsername = shift;
1725
    my $ldapPassword = shift;
1726
   
1727
    my $high;
1728
    my $ldap;
1729
    my $storedUidNumber;
1730
    
1731
    
1732
    #if main ldap server is down, a html file containing warning message will be returned
1733
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
1734
    if ($ldap) {
1735
        $ldap->start_tls( verify => 'require',
1736
                      cafile => $ldapServerCACertFile);
1737
        my $bindresult = $ldap->bind( version => 3, dn => $ldapUsername, password => $ldapPassword);
1738
        my $mesg = $ldap->search(base  => $dn_store_next_uid, filter => '(objectClass=*)');
1739
         if ($mesg->count() > 0) {
1740
                debug("Find the cn - $dn_store_next_uid");
1741
                my  $entry = $mesg->pop_entry;
1742
                $storedUidNumber = $entry->get_value($attribute_name_store_next_uid);
1743
        }
1744
        my $authBase = $properties->getProperty("auth.base");
1745
        my $uids = $ldap->search(
1746
                        base => $authBase,
1747
                        scope => "sub",
1748
                        filter => "uidNumber=*", 
1749
                        attrs   => [ 'uidNumber' ],
1750
                        );
1751
       return unless $uids->count;
1752
  	    my @uids;
1753
        if ($uids->count > 0) {
1754
                foreach my $uid ($uids->all_entries) {
1755
                		if($storedUidNumber) {
1756
                			if( $uid->get_value('uidNumber') >= $storedUidNumber) {
1757
                				push @uids, $uid->get_value('uidNumber');
1758
                			}
1759
                		} else {
1760
                        	push @uids, $uid->get_value('uidNumber');
1761
                        }
1762
                }
1763
        }       
1764
        
1765
        if(@uids) {
1766
        	@uids = sort { $b <=> $a } @uids;
1767
        	$high = $uids[0];   
1768
        }    
1769
        debug("the highest exiting uidnumber is $high");
1770
        $ldap->unbind;   # take down session
1771
    }
1772
    return $high;
1773

    
1774
}
1775

    
1776

    
(10-10/14)