3 # represents a registered user
6 @ISA = qw/Squirrel::Row/;
7 use Constants qw($SHOP_FROM);
9 use BSE::Util::SQL qw/now_datetime now_sqldate sql_normal_date sql_add_date_days/;
11 our $VERSION = "1.008";
13 use constant MAX_UNACKED_CONF_MSGS => 3;
14 use constant MIN_UNACKED_CONF_GAP => 2 * 24 * 60 * 60;
15 use constant OWNER_TYPE => "U";
18 return qw/id idUUID userId password password_type email whenRegistered
20 title name1 name2 street street2
21 suburb state postcode country
22 telephone facsimile mobile organization
23 confirmed confirmSecret waitingForConfirmation
24 textOnlyMail previousLogon
25 delivTitle delivEmail delivFirstName delivLastName delivStreet
26 delivStreet2 delivSuburb delivState delivPostCode delivCountry
27 delivTelephone delivFacsimile delivMobile delivOrganization
28 instructions adminNotes disabled flags
29 affiliate_name lost_today lost_date lost_id
30 customText1 customText2 customText3
31 customStr1 customStr2 customStr3
32 customInt1 customInt2 customWhen1
37 return "bse_siteusers";
41 require BSE::Util::SQL;
44 # idUUID handled by default_idUUID()
46 # password - required (and generates password and password_type)
47 # password_type - generated
49 whenRegistered => BSE::Util::SQL::now_datetime(),
50 lastLogon => BSE::Util::SQL::now_datetime(),
66 waitingForConfirmation => 0,
68 previousLogon => BSE::Util::SQL::now_datetime(),
82 delivOrganization => "",
105 my $ug = Data::UUID->new;
106 return $ug->create_str;
110 my ($class, $cfg, $admin) = @_;
114 email => { rules=>'email', description=>'Email Address',
116 title => { description => 'Title', rules => 'dh_one_line', maxlen => 127 },
117 name1 => { description=>'First Name', rules=>"dh_one_line", maxlen=>127 },
118 name2 => { description=>'Last Name', rules=>"dh_one_line", maxlen=>127 },
119 street => { description => 'Address', rules=>"dh_one_line", maxlen=>127 },
120 street2 => { description => 'Address', rules=>"dh_one_line", maxlen=>127 },
121 suburb => { description=>'City/Suburb', rules=>"dh_one_line", maxlen=>127 },
122 state => { description => 'State', rules=>"dh_one_line", maxlen=>40 },
123 postcode => { rules=>'postcode', description=>'Post Code', maxlen=>40 },
124 country => { description=>'Country', rules=>"dh_one_line", maxlen=>127 },
125 telephone => { rules=>'phone', description=>'Telephone', maxlen=>80 },
126 facsimile => { rules=>'phone', description=>'Facsimile', maxlen=>80 },
127 mobile => { description => "Mobile", rules=>"phone", maxlen => 80 },
128 organization => { description=>'Organization', rules=>"dh_one_line",
130 textOnlyEmail => { description => "Text Only Email", type=>"boolean" },
131 delivTitle => { description=>"Delivery Title",
132 rules=>"dh_one_line", maxlen=>127 },
133 delivEmail => { description => "Delivery Email", rules=>"email",
135 delivFirstName => { description=>"Delivery First Name",
136 rules=>"dh_one_line", maxlen=>127 },
137 delivLastName => { descriptin=>"Delivery Last Name", rules=>"dh_one_line" },
138 delivStreet => { description => "Delivery Street Address",
139 rules=>"dh_one_line", maxlen=>127 },
140 delivStreet2 => { description => 'Delivery Street Address 2',
141 rules => "dh_one_line", maxlen=> 127 },
142 delivSuburb => { description => "Delivery Suburb", rules=>"dh_one_line",
144 delivState => { description => "Delivery State", rules=>"dh_one_line",
146 delivPostCode => { description => "Delivery Post Code", rules=>"postcode",
148 delivCountry => { description => "Delivery Country", rules=>"dh_one_line",
150 delivTelephone => { description => "Delivery Phone", rules=>"phone",
152 delivFacsimile => { description => "Delivery Facsimie", rules=>"phone",
154 delivMobile => { description => "Delivery Mobile", rules=>"phone",
156 delivOrganization => { description => "Delivery Organization",
157 rules=>"dh_one_line", maxlen => 127 },
158 instructions => { description => "Delivery Instructions" },
159 customText1 => { description => "Custom Text 1" },
160 customText2 => { description => "Custom Text 2" },
161 customText3 => { description => "Custom Text 3" },
162 customStr1 => { description => "Custom String 1", rules=>"dh_one_line",
164 customStr2 => { description => "Custom String 2", rules=>"dh_one_line",
166 customStr3 => { description => "Custom String 3", rules=>"dh_one_line",
171 $fields{adminNotes} =
172 { description => "Administrator Notes" };
174 { description => "User Disabled", type=>"boolean" };
184 sub removeSubscriptions {
187 SiteUsers->doSpecial('removeSubscriptions', $self->{id});
190 sub removeSubscription {
191 my ($self, $subid) = @_;
193 SiteUsers->doSpecial('removeSub', $self->{id}, $subid);
197 my ($class, $checkemail) = @_;
199 # Build a generic form for the email - since an attacker could
200 # include comments or extra spaces or a bunch of other stuff.
201 # this isn't strictly correct, but it's good enough
202 1 while $checkemail =~ s/\([^)]\)//g;
203 if ($checkemail =~ /<([^>]+)>/) {
206 $checkemail = lc $checkemail;
207 $checkemail =~ s/\s+//g;
215 require BSE::SubscriptionTypes;
216 return BSE::SubscriptionTypes->getSpecial(userSubscribedTo => $self->{id});
219 sub send_conf_request {
220 my ($user, $cgi, $cfg, $rcode, $rmsg) = @_;
222 if ($user->is_disabled) {
223 $$rmsg = "User is disabled";
227 my $nopassword = $cfg->entryBool('site users', 'nopassword', 0);
229 # check for existing in-progress confirmations
230 my $checkemail = $user->generic_email($user->{email});
232 # check the blacklist
233 require BSE::EmailBlacklist;
235 # check that the from address has been configured
236 my $from = $cfg->entry('confirmations', 'from') ||
237 $cfg->entry('basic', 'emailfrom')|| $SHOP_FROM;
240 $$rmsg = "Configuration Error: The confirmations from address has not been configured";
244 my $blackentry = BSE::EmailBlacklist->getEntry($checkemail);
247 $$rcode = "blacklist";
248 $$rmsg = $blackentry->{why};
252 unless ($user->{confirmSecret}) {
253 use BSE::Util::Secure qw/make_secret/;
254 # print STDERR "Generating secret\n";
255 $user->{confirmSecret} = make_secret($cfg);
259 # check for existing confirmations
260 require BSE::EmailRequests;
261 my $confirm = BSE::EmailRequests->getBy(genEmail=>$checkemail);
263 if ($confirm->{unackedConfMsgs} >= MAX_UNACKED_CONF_MSGS) {
265 $$rmsg = "Too many confirmations have been sent to this email address";
268 use BSE::Util::SQL qw/sql_datetime_to_epoch/;
269 my $lastSentEpoch = sql_datetime_to_epoch($confirm->{lastConfSent});
270 if ($lastSentEpoch + MIN_UNACKED_CONF_GAP > time) {
272 $$rmsg = "The last confirmation was sent too recently, please wait before trying again";
278 my @cols = BSE::EmailRequest->columns;
280 $confirm{email} = $user->{email};
281 $confirm{genEmail} = $checkemail;
282 # prevents silliness on error
283 use BSE::Util::SQL qw(sql_datetime);
284 $confirm{lastConfSent} = sql_datetime(time - MIN_UNACKED_CONF_GAP);
285 $confirm{unackedConfMsgs} = 0;
286 $confirm = BSE::EmailRequests->add(@confirm{@cols});
289 # ok, now we can send the confirmation request
293 BSE::Util::Tags->basic(\%confacts, $cgi, $cfg),
294 user => sub { $user->{$_[0]} },
295 confirm => sub { $confirm->{$_[0]} },
296 remote_addr => sub { $ENV{REMOTE_ADDR} },
299 $nopassword ? 'user/email_confirm_nop' : 'user/email_confirm';
300 my $body = BSE::Template->get_page($email_template, $cfg, \%confacts);
303 my $mail = BSE::Mail->new(cfg=>$cfg);
304 my $subject = $cfg->entry('confirmations', 'subject')
305 || 'Subscription Confirmation';
306 unless ($mail->send(from=>$from, to=>$user->{email}, subject=>$subject,
308 # a problem sending the mail
310 $$rmsg = $mail->errstr;
313 ++$confirm->{unackedConfMsgs};
314 $confirm->{lastConfSent} = now_datetime;
323 require BSE::TB::Orders;
325 return BSE::TB::Orders->getBy(userId => $self->{userId});
328 sub _user_sub_entry {
329 my ($self, $sub) = @_;
331 my ($entry) = BSE::DB->query(userSubscribedEntry => $self->{id},
332 $sub->{subscription_id})
338 # check if the user is subscribed to the given subscription
340 my ($self, $sub) = @_;
342 my $entry = $self->_user_sub_entry($sub)
345 my $today = now_sqldate;
346 my $end_date = sql_normal_date($entry->{ends_at});
347 return $today le $end_date;
350 # check if the user is subscribed to the given subscription, and allow
351 # for the max_lapsed grace period
352 sub subscribed_to_grace {
353 my ($self, $sub) = @_;
355 my $entry = $self->_user_sub_entry($sub)
358 my $today = now_sqldate;
359 my $end_date = sql_add_date_days($entry->{ends_at}, $entry->{max_lapsed});
360 return $today le $end_date;
364 qw(siteuser_id image_id filename width height bytes content_type alt);
367 my ($self, $cfg) = @_;
370 my %ids = $cfg->entries('BSE Siteuser Images');
371 for my $id (keys %ids) {
372 my %image = ( id => $id );
374 my $sect = "BSE Siteuser Image $id";
375 for my $key (qw(description help minwidth minheight maxwidth maxheight
376 minratio maxratio properror
377 widthsmallerror heightsmallerror smallerror
378 widthlargeerror heightlargeerror largeerror
379 maxspace spaceerror)) {
380 my $value = $cfg->entry($sect, $key);
381 if (defined $value) {
382 $image{$key} = $value;
385 push @images, \%image;
394 BSE::DB->query(getBSESiteuserImages => $self->{id});
398 my ($self, $id) = @_;
400 my ($image) = BSE::DB->query(getBSESiteuserImage => $self->{id}, $id)
407 my ($self, $cfg, $id, $image) = @_;
410 $image{siteuser_id} = $self->{id};
411 my $old = $self->get_image($id);
415 BSE::DB->run(replaceBSESiteuserImage => @image{@image_cols});
418 my $image_dir = $cfg->entryVar('paths', 'siteuser_images');
419 unlink "$image_dir/$old->{filename}";
424 BSE::DB->run(addBSESiteuserImage => @image{@image_cols});
429 my ($self, $cfg, $id) = @_;
431 if (my $old = $self->get_image($id)) {
433 BSE::DB->run(deleteBSESiteuserImage => $self->{id}, $id);
436 my $image_dir = $cfg->entryVar('paths', 'siteuser_images');
437 unlink "$image_dir/$old->{filename}";
441 sub recalculate_subscriptions {
442 my ($self, $cfg) = @_;
444 require BSE::TB::Subscriptions;
445 my @subs = BSE::TB::Subscriptions->all;
446 for my $sub (@subs) {
447 $sub->update_user_expiry($self, $cfg);
451 sub subscribed_services {
454 BSE::DB->query(siteuserSubscriptions => $self->{id});
460 return $self->{disabled};
463 sub seminar_sessions_booked {
464 my ($self, $seminar_id) = @_;
466 return map $_->{session_id},
467 BSE::DB->query(userSeminarSessionBookings => $seminar_id, $self->{id});
471 my ($self, $group) = @_;
473 my $group_id = ref $group ? $group->{id} : $group;
475 my @result = BSE::DB->query(siteuserMemberOfGroup => $self->{id}, $group_id);
477 return scalar(@result);
483 map $_->{id}, BSE::DB->query(siteuserGroupsForUser => $self->{id});
486 sub allow_html_email {
489 !$self->{textOnlyMail};
492 sub seminar_bookings_detail {
495 BSE::DB->query(bse_siteuserSeminarBookingsDetail => $self->{id});
501 return Products->getSpecial(userWishlist => $self->{id});
506 return BSE::DB->query(bse_userWishlistOrder => $self->{id});
509 sub product_in_wishlist {
510 my ($self, $product) = @_;
512 grep $_->{product_id} == $product->{id}, $self->wishlist_order;
515 sub add_to_wishlist {
516 my ($self, $product) = @_;
520 BSE::DB->run(bse_addToWishlist => $self->{id}, $product->{id}, time());
525 sub remove_from_wishlist {
526 my ($self, $product) = @_;
528 BSE::DB->run(bse_removeFromWishlist => $self->{id}, $product->{id});
531 sub _set_wishlist_order {
532 my ($self, $product_id, $display_order) = @_;
534 print STDERR "_set_wishlist_order($product_id, $display_order)\n";
536 BSE::DB->run(bse_userWishlistReorder => $display_order, $self->{id}, $product_id);
539 sub _find_in_wishlist {
540 my ($self, $product_id) = @_;
542 my @order = $self->wishlist_order;
544 my ($index) = grep $order[$_]{product_id} == $product_id, 0 .. $#order
547 return \@order, $index;
550 sub move_to_wishlist_top {
551 my ($self, $product) = @_;
553 my ($order, $move_index) = $self->_find_in_wishlist($product->{id})
556 or return; # nothing to do
558 my $top_order = $order->[0]{display_order};
559 for my $index (0 .. $move_index-1) {
560 $self->_set_wishlist_order($order->[$index]{product_id}, $order->[$index+1]{display_order});
562 $self->_set_wishlist_order($product->{id}, $top_order);
565 sub move_to_wishlist_bottom {
566 my ($self, $product) = @_;
568 my ($order, $move_index) = $self->_find_in_wishlist($product->{id})
570 $move_index < $#$order
571 or return; # nothing to do
573 my $bottom_order = $order->[-1]{display_order};
574 for my $index (reverse($move_index+1 .. $#$order)) {
575 $self->_set_wishlist_order($order->[$index]{product_id}, $order->[$index-1]{display_order});
577 $self->_set_wishlist_order($product->{id}, $bottom_order);
580 sub move_down_wishlist {
581 my ($self, $product) = @_;
583 my ($order, $index) = $self->_find_in_wishlist($product->{id})
586 or return; # nothing to do
588 $self->_set_wishlist_order($product->{id}, $order->[$index+1]{display_order});
589 $self->_set_wishlist_order($order->[$index+1]{product_id}, $order->[$index]{display_order});
592 sub move_up_wishlist {
593 my ($self, $product) = @_;
595 my ($order, $index) = $self->_find_in_wishlist($product->{id})
598 or return; # nothing to do
600 $self->_set_wishlist_order($product->{id}, $order->[$index-1]{display_order});
601 $self->_set_wishlist_order($order->[$index-1]{product_id}, $order->[$index]{display_order});
604 # files owned specifically by this user
608 require BSE::TB::OwnedFiles;
609 return BSE::TB::OwnedFiles->getBy(owner_type => OWNER_TYPE,
610 owner_id => $self->id);
613 sub admin_group_files {
616 require BSE::TB::OwnedFiles;
617 return BSE::TB::OwnedFiles->getSpecial(userVisibleGroupFiles => $self->{id});
620 sub query_group_files {
621 my ($self, $cfg) = @_;
623 require BSE::TB::SiteUserGroups;
626 map $_->files, BSE::TB::SiteUserGroups->query_groups($cfg)
630 # files the user can see, both owned and owned by groups
632 my ($self, $cfg) = @_;
637 $self->admin_group_files,
638 $self->query_group_files($cfg)
642 sub file_owner_type {
646 sub subscribed_file_categories {
649 return map $_->{category}, BSE::DB->query(siteuserSubscribedFileCategories => $self->{id});
652 sub set_subscribed_file_categories {
653 my ($self, $cfg, @new) = @_;
655 require BSE::TB::OwnedFiles;
656 my %current = map { $_ => 1 } $self->subscribed_file_categories;
657 my %new = map { $_ => 1 } @new;
658 my @all = BSE::TB::OwnedFiles->categories($cfg);
660 if ($new{$cat->{id}} && !$current{$cat->{id}}) {
662 BSE::DB->run(siteuserAddFileCategory => $self->{id}, $cat->{id});
663 }; # a race condition might cause a duplicate key error here
665 elsif (!$new{$cat->{id}} && $current{$cat->{id}}) {
666 BSE::DB->run(siteuserRemoveFileCategory => $self->{id}, $cat->{id});
673 Returns a description of the user
680 return "Member: " . $self->userId;
685 Files that require payment that the user has paid for.
692 require BSE::TB::ArticleFiles;
693 return BSE::TB::ArticleFiles->getSpecial(userPaidFor => $self->id);
697 my ($self, $cfg) = @_;
699 $cfg or confess "Missing parameter cfg";
701 # remove any owned files
702 for my $file ($self->files) {
707 BSE::DB->run(bseRemoveUserFileSubs => $self->id);
710 BSE::DB->run(bseRemoveUserFileNotifies => $self->id);
713 BSE::DB->run(bseMarkUserFileAccessesAnon => $self->id);
715 # mark any orders owned by the user as anonymous
716 BSE::DB->run(bseMarkOwnedOrdersAnon => $self->id);
718 # newsletter subscriptions
719 BSE::DB->run(bseRemoveUserSubs => $self->id);
722 BSE::DB->run(bseRemoveUserWishlist => $self->id);
725 BSE::DB->run(bseRemoveUserMemberships => $self->id);
728 BSE::DB->run(bseRemoveUserBookings => $self->id);
731 BSE::DB->run(bseRemoveUserProdSubs => $self->id);
734 for my $im ($self->images) {
735 $self->remove_image($cfg, $im->{image_id});
738 $self->SUPER::remove();
744 return BSE::Cfg->single->admin_url(siteusers => { a_edit => 1, id => $self->id });
747 =item send_registration_notify(remote_addr => $ip_address)
749 Send an email to the customer with registration information.
751 Template: user/email_register
753 Basic static tags and:
759 host - IP address of the machine that registered the user.
763 user - the user registered.
769 sub send_registration_notify {
770 my ($self, %opts) = @_;
772 defined $opts{remote_addr}
773 or confess "Missing remote_addr parameter";
775 require BSE::ComposeMail;
776 require BSE::Util::Tags;
777 BSE::ComposeMail->send_simple
779 id => 'notify_register_customer',
780 template => 'user/email_register',
781 subject => 'Thank you for registering',
785 host => $opts{remote_addr},
786 user => [ \&BSE::Util::Tags::tag_hash_plain, $self ],
788 log_msg => "Registration email to " . $self->email,
789 log_component => "member:register:notifyuser",
794 my ($self, $password, $who, %log) = @_;
796 require BSE::Passwords;
798 my ($hash, $type) = BSE::Passwords->new_password_hash($password);
800 $self->set_password($hash);
801 $self->set_password_type($type);
803 require BSE::TB::AuditLog;
804 BSE::TB::AuditLog->log
806 component => "siteusers::changepw",
810 msg => "Change password",
818 my ($self, $password, $error) = @_;
820 require BSE::Passwords;
821 return BSE::Passwords->check_password_hash($self->password, $self->password_type, $password, $error);
826 Call to send a lost password email.
831 my ($self, $error) = @_;
833 my $cfg = BSE::Cfg->single;
834 require BSE::CfgInfo;
835 my $custom = BSE::CfgInfo::custom_class($cfg);
836 my $email_user = $self;
838 if ($custom->can('send_user_email_to')) {
840 $email_user = $custom->send_user_email_to($self, $cfg);
842 $to = $email_user->{email};
845 require BSE::Util::SQL;
846 my $lost_limit = $cfg->entry("lost password", "daily_limit", 3);
847 my $today = BSE::Util::SQL::now_sqldate();
850 && $self->lost_date eq $today) {
851 $lost_today = $self->lost_today;
853 if ($lost_today+1 > $lost_limit) {
854 $$error = "Too many password recovery attempts today, please try again tomorrow";
857 $self->set_lost_date($today);
858 $self->set_lost_today($lost_today+1);
859 $self->set_lost_id(BSE::Util::Secure::make_secret($cfg));
862 require BSE::ComposeMail;
863 my $mail = BSE::ComposeMail->new(cfg => $cfg);
865 require BSE::Util::Tags;
869 BSE::Util::Tags->mail_tags(),
870 user => [ \&BSE::Util::Tags::tag_object_plain, $self ],
871 host => $ENV{REMOTE_ADDR},
872 site => $cfg->entryErr('site', 'url'),
873 emailuser => [ \&BSE::Util::Tags::tag_hash_plain, $email_user ],
875 my $from = $cfg->entry('confirmations', 'from') ||
876 $cfg->entry('basic', 'emailfrom') || $SHOP_FROM;
877 my $nopassword = $cfg->entryBool('site users', 'nopassword', 0);
878 my $subject = $cfg->entry('basic', 'lostpasswordsubject')
879 || ($nopassword ? "Your options" : "Your password");
882 template => 'user/lostpwdemail',
887 log_msg => "Sending lost password recovery email",
888 log_component => "siteusers:lost:send",
891 $$error = $mail->errstr;
899 sub check_password_rules {
900 my ($class, %opts) = @_;
902 require BSE::Util::PasswordValidate;
904 my %rules = BSE::Cfg->single->entries("siteuser passwords");
906 return BSE::Util::PasswordValidate->validate
913 sub password_check_fields {
914 return qw(name1 name2);