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 22:32:45 -0700 (Mon, 06 Oct 2014) $'
8
# '$Revision: 8880 $' 
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

    
588
    # Search LDAP for matching entries that already exist
589
    # Some forms use a single text search box, whereas others search per
590
    # attribute.
591
    my $filter;
592
    if ($query->param('searchField')) {
593

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

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

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

    
623
    exit();
624
}
625

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

    
647
#
648
# change a user's password upon request
649
#
650
sub handleChangePassword {
651

    
652
    print "Content-type: text/html\n\n";
653

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

    
665

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

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

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

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

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

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

    
727
#
728
# reset a user's password upon request
729
#
730
sub handleResetPassword {
731

    
732
    print "Content-type: text/html\n\n";
733

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

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

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

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

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

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

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

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

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

    
833
    my $searchBase = $ldapConfig->{$o}{'base'};
834

    
835
    my $errorMessage = 0;
836
    my $ldap;
837

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

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

    
867
    return $errorMessage;
868
}
869

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

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

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

    
886
    return $hashedPasswd;
887
}
888

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

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

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

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

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

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

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

    
981
            Username: $username
982
        Organization: $org
983
        New Password: $newPass
984

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

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

    
1015
    my $foundAccounts = 0;
1016

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

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

    
1054
    my $foundAccounts = 0;
1055

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

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

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

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

    
1130
    return $foundAccounts;
1131
}
1132

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

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

    
1148
    return $allValid;
1149
}
1150

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

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

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

    
1275
    
1276
    #$$additions[$#$additions + 1] = 'o';
1277
    #$$additions[$#$additions + 1] = $org;
1278
    my $dn='uid=' . $query->param('uid') . ',' . $ldapConfig->{$org}{'org'} . ',' . $tmpSearchBase;
1279
    createItem($dn, $ldapUsername, $ldapPassword, $additions, $tmp, $allParams);
1280
    
1281
    
1282
    ####################send the verification email to the user
1283
    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.
1284
    
1285
    my $overrideURL;
1286
    $overrideURL = $skinProperties->getProperty("email.overrideURL");
1287
    debug("the overrideURL is $overrideURL");
1288
    if (defined($overrideURL) && !($overrideURL eq '')) {
1289
    	$link = $serverUrl . $overrideURL . $link;
1290
    } else {
1291
    	$link = $serverUrl . $link;
1292
    }
1293
    
1294
    my $mailhost = $properties->getProperty('email.mailhost');
1295
    my $sender;
1296
    $sender = $skinProperties->getProperty("email.sender") or $sender = $properties->getProperty('email.sender');
1297
    debug("the sender is " . $sender);
1298
    my $recipient = $query->param('mail');
1299
    # Send the email message to them
1300
    my $smtp = Net::SMTP->new($mailhost) or do {  
1301
                                                  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 " . 
1302
                                                  $skinProperties->getProperty("email.recipient") . "." });  
1303
                                                  exit(0);
1304
                                               };
1305
    $smtp->mail($sender);
1306
    $smtp->to($recipient);
1307

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

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

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

    
1384

    
1385

    
1386

    
1387

    
1388

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

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

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

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

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

    
1468
}
1469

    
1470
sub handleResponseMessage {
1471

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

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

    
1487
    my $o = $query->param('o');
1488

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

    
1492
    print "Content-type: text/html\n\n";
1493

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

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

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

    
1528
    my $found = searchDirectory($ldapurl, $searchBase, $filter, \@attrs);
1529

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

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

    
1543
    exit();
1544
}
1545

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

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

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

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

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

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

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

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

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

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

    
1776
}
1777

    
1778

    
(10-10/14)