from#!/usr/bin/perl # # This code is based on code originally created by the LiveJournal project # owned and operated by Live Journal, Inc. The code has been modified and expanded # by Dreamwidth Studios, LLC. These files were originally licensed under # the terms of the license supplied by Live Journal, Inc, which can # currently be found at: # # http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt # # In accordance with the original license, this code and all its # modifications are provided under the GNU General Public License. # A copy of that license can be found in the LICENSE file included as # part of this distribution. # # # Authors: # Janine Smith # Afuna # # Copyright (c) 2009-2017 by Dreamwidth Studios, LLC. package DW::Controller::Create; use strict; use DW::Controller; use DW::Routing; use DW::Template; use DW::FormErrors; use LJ::Widget::Location; =head1 NAME DW::Controller::Create - Account creation flow =cut my %urls = ( create => '/create', setup => '/create/setup', upgrade => '/create/upgrade', next => '/create/next', ); DW::Routing->register_string( $urls{create}, \&create_handler, app => 1 ); DW::Routing->register_string( $urls{setup}, \&setup_handler, app => 1 ); DW::Routing->register_string( $urls{upgrade}, \&upgrade_handler, app => 1 ); DW::Routing->register_string( $urls{next}, \&next_handler, app => 1 ); sub create_handler { my ($opts) = @_; my ( $ok, $rv ) = controller( anonymous => 1, form_auth => 1 ); return $rv unless $ok; my $r = $rv->{r}; my $get = $r->get_args; my $post; my $code_valid = $LJ::USE_ACCT_CODES ? 0 : 1; my $code; # start out saying we're okay; we'll modify this if we're actually checking codes later my $rate_ok = 1; my $errors = DW::FormErrors->new; my $email_checkbox; if ( $r->did_post ) { $post = $r->post_args; $post->{user} = LJ::trim( $post->{user} ); my $user = LJ::canonical_username( $post->{user} ); my $email = LJ::trim( lc $post->{email} ); # reject this email? if ( LJ::sysban_check( email => $email ) ) { LJ::Sysban::block( 0, "Create user blocked based on email", { new_user => $user, email => $email, name => $user, } ); return $r->HTTP_SERVICE_UNAVAILABLE; } # is username valid? my $second_submit = 0; my $error = LJ::CreatePage->verify_username( $post->{user}, post => $post, second_submit_ref => \$second_submit ); $errors->add_string( "user", $error ) if $error; # validate code my $code = LJ::trim( $post->{code} ); if ($LJ::USE_ACCT_CODES) { my $u = LJ::load_user( $post->{user} ); my $userid = $u ? $u->id : 0; if ( DW::InviteCodes->check_code( code => $code, userid => $userid ) ) { $code_valid = 1; # and if this is a community promo code, set the inviter if ( my $pc = DW::InviteCodes::Promo->load( code => $code ) ) { my $invu = $pc->suggest_journal; $post->{from} = $invu->user if $invu; } } else { return $r->redirect( LJ::create_url( undef, keep_args => [qw( user from code )] ) ); } } # check passwords $post->{password1} = LJ::trim( $post->{password1} ); $post->{password2} = LJ::trim( $post->{password2} ); if ( !$post->{password1} ) { $errors->add( 'password1', 'widget.createaccount.error.password.blank' ); } elsif ( $post->{password1} ne $post->{password2} ) { $errors->add( 'password2', 'widget.createaccount.error.password.nomatch' ); } elsif ( !$LJ::IS_DEV_SERVER ) { # Dev servers can use any password my $checkpass = LJ::CreatePage->verify_password( password => $post->{password1}, username => $user, email => $email ); $errors->add( 'password1', 'widget.createaccount.error.password.bad2', { reason => $checkpass } ) if $checkpass; } # age check my $dbh = LJ::get_db_writer(); my $uniq; my $is_underage = 0; my $is_tn_underage = 0; $uniq = $r->note('uniq'); if ($uniq) { my $timeof = $dbh->selectrow_array( 'SELECT timeof FROM underage WHERE uniq = ?', undef, $uniq ); $is_underage = 1 if $timeof && $timeof > 0; } my ( $year, $mon, $day ) = ( $post->{bday_yyyy} + 0, $post->{bday_mm} + 0, $post->{bday_dd} + 0 ); if ( $year < 100 && $year > 0 ) { $post->{bday_yyyy} += 1900; $year += 1900; } my $nyear = ( gmtime() )[5] + 1900; # require dates in the 1900s (or beyond) if ( $year && $mon && $day && $year >= 1900 && $year < $nyear ) { my $age = LJ::calc_age( $year, $mon, $day ); $is_underage = 1 if $age < 13; # TN underage check - if user is in TN and under 18 if ( $post->{tn_state} eq '1' && $age < 18 ) { $is_tn_underage = 1; } } else { $errors->add( 'birthdate', 'widget.createaccount.error.birthdate.invalid2' ); } # note this unique cookie as underage (if we have a unique cookie) if ( ( $is_underage || $is_tn_underage ) && $uniq ) { $dbh->do( "REPLACE INTO underage (uniq, timeof) VALUES (?, UNIX_TIMESTAMP())", undef, $uniq ); } # Add appropriate error messages if ($is_tn_underage) { $errors->add( 'tn_state', 'widget.createaccount.error.tn_underage' ); } elsif ($is_underage) { $errors->add( 'birthdate', 'widget.createaccount.error.birthdate.underage' ); } # check the email address my @email_errors; LJ::check_email( $email, \@email_errors, $post, \$email_checkbox ); $errors->add_string( 'email', $_ ) foreach @email_errors; $errors->add( 'email', 'widget.createaccount.error.email.lj_domain', { domain => $LJ::USER_DOMAIN } ) if $LJ::USER_EMAIL and $email =~ /\@\Q$LJ::USER_DOMAIN\E$/i; # check the captcha answer if it's turned on my $captcha = DW::Captcha->new( 'create', %{ $post || {} } ); my $captcha_error; $errors->add_string( 'captcha', $captcha_error ) unless $captcha->validate( err_ref => \$captcha_error ); # check TOS agreement $errors->add( 'tos', 'widget.createaccount.error.tos' ) unless $post->{tos}; # create user and send email as long as the user didn't double-click submit # (or they tried to re-create a purged account) my $nu; unless ( $second_submit || $errors->exist ) { my $bdate = sprintf( "%04d-%02d-%02d", $post->{bday_yyyy}, $post->{bday_mm}, $post->{bday_dd} ); $nu = LJ::User->create_personal( user => $user, bdate => $bdate, email => $email, password => $post->{password1}, get_news => $post->{news} ? 1 : 0, inviter => $post->{from}, code => DW::InviteCodes->check_code( code => $code ) ? $code : undef, ); $errors->add( '', 'widget.createaccount.error.cannotcreate' ) unless $nu; } # now go on and do post-create stuff if ($nu) { # In dev containers, auto-validate email so accounts are immediately usable if ($LJ::IS_DEV_SERVER = 0) { $nu->update_self( { status => 'A' } ); } # send welcome mail my $aa = LJ::register_authaction( $nu->id, "validateemail", $email ); my $body = LJ::Lang::ml( 'email.newacct6.body', { sitename => $LJ::SITENAME, regurl => "$LJ::SITEROOT/confirm/$aa->{'aaid'}.$aa->{'authcode'}", journal_base => $nu->journal_base, username => $nu->user, siteroot => $LJ::SITEROOT, sitenameshort => $LJ::SITENAMESHORT, lostinfourl => "$LJ::SITEROOT/lostinfo", editprofileurl => "$LJ::SITEROOT/manage/profile/", searchinterestsurl => "$LJ::SITEROOT/interests", editiconsurl => "$LJ::SITEROOT/manage/icons", customizeurl => "$LJ::SITEROOT/customize/", postentryurl => "$LJ::SITEROOT/update", setsecreturl => "$LJ::SITEROOT/set_secret", supporturl => "$LJ::SITEROOT/support/submit", } ); LJ::send_mail( { to => $email, from => $ADMIN_EMAIL, fromname => $LJ::SITENAME, charset => 'utf-8', subject => LJ::Lang::ml( 'email.newacct.subject', { sitename => $LJ::SITENAME } ), body => $body, } ); # note that this user needs to be reviewed for spam content $nu->set_prop( not_approved => 1 ) if LJ::is_enabled('approvenew'); # we're all done $nu->make_login_session; if ($code) { # unconditionally mark the invite code as used if ($LJ::USE_ACCT_CODES) { if ( my $pc = DW::InviteCodes::Promo->load( code => $code ) ) { $pc->use_code; } else { my $invitecode = DW::InviteCodes->new( code => $code ); $invitecode->use_code( user => $nu ); } # user is now paid, let's assume that this came from the invite code # so mark the invite code as used } elsif ( DW::Pay::get_current_account_status($nu) ) { my $invitecode = DW::InviteCodes->new( code => $code ); $invitecode->use_code( user => $nu ); } } # go on to the next step my $stop_output; my $stop_body; my $redirect; LJ::Hooks::run_hook( 'underage_redirect', { u => $nu, redirect => \$redirect, ret => \$stop_body, stop_output => \$stop_output, } ); return $r->redirect($redirect) if $redirect; return $stop_body if $stop_output; $redirect = LJ::Hooks::run_hook( 'rewrite_redirect_after_create', $nu ); return $r->redirect($redirect) if $redirect; return $r->redirect( LJ::create_url( $urls{setup} ) ); } } else { # we always need the code, because it might contain paid time $code = LJ::trim( $get->{code} ); # and we always do rate limiting if we have a code $rate_ok = DW::InviteCodes->check_rate if $code; # but we don't always need to block the registration on the validity of the code # (if we have an invalid code, but we do don't require codes to open an account, just fail silently) $code_valid = DW::InviteCodes->check_code( code => $code ) if $LJ::USE_ACCT_CODES; } my $step = 1; my $vars = { steps_to_show => [ steps_to_show( $step, code => $code ) ], step => $step, form_url => LJ::create_url( undef, keep_args => [qw( user from code )] ), code => LJ::trim( $get->{code} ), from => LJ::trim( $get->{from} ), formdata => $post, errors => $errors, email_checkbox => $email_checkbox, username_maxlength => $LJ::USERNAME_MAXLENGTH, password_minlength => $LJ::PASSWORD_MINLENGTH, password_maxlength => $LJ::PASSWORD_MAXLENGTH, }; if ( $code_valid && $rate_ok ) { $vars->{months} = [ map { $_, LJ::Lang::month_long_ml($_) } ( 1 .. 12 ) ]; $vars->{days} = [ map { $_, $_ } ( 1 .. 31 ) ]; $vars->{formdata} ||= { user => $get->{user}, }; LJ::set_active_resource_group("foundation"); my $captcha = DW::Captcha->new( 'create', %{ $post || {} } ); $vars->{captcha} = $captcha->print if $captcha->enabled; if ($LJ::USE_ACCT_CODES) { if ( my $pc = DW::InviteCodes::Promo->load( code => $code ) ) { if ( $pc->paid_class ) { $vars->{code_paid_time} = { type => $pc->paid_class_name, months => $pc->paid_months, }; } } else { my $item = DW::InviteCodes->paid_status( code => $code ); if ($item) { $vars->{code_paid_time} = { type => $item->class_name, months => $item->months, permanent => $item->permanent, }; } } } return DW::Template->render_template( 'create/account.tt', $vars ); } else { # we can still use invite codes to create new paid accounts # so display this in case they hit the rate limit, even without USE_ACCT_CODES $errors->add( 'code', 'widget.createaccountentercode.error.toofast' ) unless $rate_ok; # also check for the presence of a code (if we reach this point with a code, code should be invalid...) $errors->add( 'code', 'widget.createaccountentercode.error.invalidcode' ) if $code && !$code_valid; $vars->{payments_enabled} = LJ::is_enabled('payments'); $vars->{logged_out} = $rv->{remote} ? 0 : 1; $vars->{formdata} ||= { code => $vars->{code}, from => $vars->{from}, }; return DW::Template->render_template( 'create/code.tt', $vars ); } } sub setup_handler { my ($opts) = @_; my ( $ok, $rv ) = controller( form_auth => 1 ); return $rv unless $ok; my $r = $rv->{r}; my $remote = $rv->{remote}; my $u = LJ::get_effective_remote(); my $post; return $r->redirect( LJ::create_url("/") ) unless $remote->is_personal; my @location_props = qw/ country state city /; my $errors = DW::FormErrors->new; if ( $r->did_post ) { $post = $r->post_args; # Check for spam strings LJ::Hooks::run_hooks( 'spam_check', $u, $post, 'userbio' ); # name $errors->add( 'name', '/manage/profile.tt.error.noname' ) unless LJ::trim( $post->{name} ) || defined $post->{name_absent}; $errors->add( 'name', '/manage/profile.tt.error.name.toolong' ) if length $post->{name} > 80; $post->{name} =~ s/[\n\r]//g; $post->{name} = LJ::text_trim( $post->{name}, LJ::BMAX_NAME, LJ::CMAX_NAME ); # gender $post->{gender} = 'U' unless $post->{gender} =~ m/^[UMFO]$/; # location my $state_from_dropdown = LJ::Lang::ml('states.head.defined'); $post->{stateother} //= ""; $post->{stateother} = "" if $post->{stateother} eq $state_from_dropdown; my %countries; DW::Countries->load( \%countries ); my $regions_cfg = LJ::Widget::Location->country_regions_cfg( $post->{country} ); if ( $regions_cfg && $post->{stateother} ) { $errors->add( 'statedrop', 'widget.location.error.locale.country_ne_state' ); } elsif ( !$regions_cfg && $post->{statedrop} ) { $errors->add( 'stateother', 'widget.location.error.locale.state_ne_country' ); } if ( $post->{country} && !defined $countries{ $post->{country} } ) { $errors->add( 'country', 'widget.location.error.locale.invalid_country' ); } # check if specified country has states if ($regions_cfg) { # if it is - use region select dropbox $post->{state} = $post->{statedrop}; # mind save_region_code also unless ( $regions_cfg->{save_region_code} ) { # save region name instead of code my $regions_arrayref = LJ::Widget::Location->region_options($regions_cfg); my %regions_as_hash = @$regions_arrayref; $post->{state} = $regions_as_hash{ $post->{state} }; } } else { # use state input box $post->{state} = $post->{stateother}; } # interests my @interests_strings = grep { defined $_ } ( $post->{interests_music}, $post->{interests_moviestv}, $post->{interests_books}, $post->{interests_hobbies}, $post->{interests_other}, ); my @ints = LJ::interest_string_to_list( join ", ", @interests_strings ); # count interests my $intcount = scalar @ints; my $maxinterests = $u->count_max_interests; $errors->add( 'interests', 'error.interest.excessive2', { intcount => $intcount, maxinterests => $maxinterests } ) if $intcount > $maxinterests; # clean interests, and make sure they're valid my @interrors; my @valid_ints = LJ::validate_interest_list( \@interrors, @ints ); if ( @interrors > 0 ) { for my $err (@interrors) { $errors->add( 'interests', $err->[0], { words => $err->[1]{words}, words_max => $err->[1]{words_max}, 'int' => $err->[1]{int}, bytes => $err->[1]{bytes}, bytes_max => $err->[1]{bytes_max}, chars => $err->[1]{chars}, chars_max => $err->[1]{chars_max}, } ); } } # bio $errors->add( 'bio', '/manage/profile.tt.error.bio.toolong' ) if defined $post->{bio} && length $post->{bio} >= LJ::BMAX_BIO; LJ::EmbedModule->parse_module_embed( $u, \$post->{bio} ); # inviter / communities ## trust if ( $post->{inviter_trust} ) { my $trust_u = LJ::load_userid( $post->{inviter_trust} ); $u->add_edge( $trust_u, trust => {} ) if LJ::isu($trust_u) && !$u->trusts($trust_u); } if ( $post->{inviter_watch} ) { my $watch_u = LJ::load_userid( $post->{inviter_watch} ); $u->add_edge( $watch_u, watch => {} ) if LJ::isu($watch_u) && !$u->watches($watch_u); } my @comm_ids = $post->get_all('inviter_join'); foreach my $comm_id (@comm_ids) { my $join_u = LJ::load_userid($comm_id); if ( LJ::isu($join_u) && !$u->member_of($join_u) ) { # try to join the community # if it fails and the community's moderated, send a join request and watch it unless ( $u->join_community( $join_u, 1 ) ) { if ( $join_u->is_moderated_membership ) { $join_u->comm_join_request($u); $u->add_edge( $join_u, watch => {} ); } } } } unless ( $errors->exist ) { # name $u->update_self( { name => $post->{name} } ); # gender $u->set_prop( 'gender', $post->{gender} ); # location $u->set_prop( $_, $post->{$_} ) foreach @location_props; # interests $u->set_interests( \@valid_ints ); # bio $u->set_bio( $post->{bio}, $post->{bio_absent} ); $u->invalidate_directory_record; # now go to the next page return $r->redirect( LJ::create_url( $urls{upgrade} ) ) if LJ::is_enabled('payments') && !$remote->is_paid; return $r->redirect( LJ::create_url( $urls{next} ) ); } } my @current_interests; foreach ( sort keys %{ $u->interests } ) { push @current_interests, $_ if LJ::text_in($_); } # location $u->preload_props(@location_props); my $inviter; my $inviter_u = $u->who_invited; my %inviter_form_defaults; if ($inviter_u) { my %comms = $inviter_u->is_individual ? $inviter_u->relevant_communities : ( $inviter_u->id => { u => $inviter_u, istatus => 'normal' } ); my @comms = map { id => $_, %{ $comms{$_} } }, sort { $comms{$a}->{u}->display_username cmp $comms{$b}->{u}->display_username } keys %comms; $inviter = { user => $inviter_u->user, id => $inviter_u->id, ljuser_display => $inviter_u->ljuser_display, # if inviter was a community, then it came from a promo code from_promo => $inviter_u->is_individual ? 0 : 1, comms => \@comms, can_add_watch => $u->can_watch($inviter_u), can_add_trust => $u->can_trust($inviter_u), }; %inviter_form_defaults = ( inviter_watch => $inviter_u->id, inviter_trust => $inviter_u->id, inviter_join => $inviter_u->is_community ? $inviter_u->id : undef, ); } my $step = 2; my $vars = { steps_to_show => [ steps_to_show($step) ], step => $step, inviter => $inviter, form_url => LJ::create_url(), gender_list => [ F => LJ::Lang::ml('/manage/profile.tt.gender.female'), M => LJ::Lang::ml('/manage/profile.tt.gender.male'), O => LJ::Lang::ml('/manage/profile.tt.gender.other'), U => LJ::Lang::ml('/manage/profile.tt.gender.unspecified'), ], interests => \@current_interests, country_list => LJ::Widget::Location->country_options, state_list => undef, # set later countries_with_regions => join( " ", LJ::Widget::Location->countries_with_regions ) || "", is_utf8 => { name => LJ::text_in( $u->name_orig ), bio => LJ::text_in( $u->bio ), }, formdata => $post || { name => $u->name_orig || "", gender => $u->prop('gender') || 'U', interests_other => join( ", ", @current_interests ) || "", bio => $u->bio || "", country => $u->prop('country') || "", statedrop => $u->prop('state') || "", stateother => $u->prop('state') || "", city => $u->prop('city') || "", %inviter_form_defaults, }, errors => $errors, }; # clean bio and expand for editing my $bio = \$vars->{formdata}->{bio}; LJ::EmbedModule->parse_module_embed( $u, $bio, edit => 1 ); LJ::text_out( $bio, "force" ); # populate specified country with state information (if any) # first check if specified country has regions my $regions_cfg = LJ::Widget::Location->country_regions_cfg( $vars->{formdata}->{country} ); # hashref of all regions for the specified country; it is initialized and used only if $regions_cfg is defined, i.e. the country has regions (states) $vars->{state_list} = LJ::Widget::Location->region_options($regions_cfg) if $regions_cfg; return DW::Template->render_template( 'create/setup.tt', $vars ); } sub upgrade_handler { my ( $ok, $rv ) = controller( form_auth => 0 ); return $rv unless $ok; my $r = $rv->{r}; my $remote = $rv->{remote}; return error_ml( 'widget.createaccount.error.suspended', { accounts_email => $LJ::ACCOUNTS_EMAIL } ) if $remote && $remote->is_suspended; return $r->redirect( LJ::create_url( $urls{next} ) ) unless LJ::is_enabled('payments') && $remote->is_personal && !$remote->is_paid; return $r->redirect( LJ::create_url("/shop/account?for=self") ) if $r->did_post; my $step = 3; my $vars = { steps_to_show => [ steps_to_show($step) ], step => $step, upgrade_url => LJ::create_url(), next_url => LJ::create_url( $urls{next} ), help_url => $LJ::HELPURL{paidaccountinfo}, }; return DW::Template->render_template( 'create/upgrade.tt', $vars ); } sub next_handler { my $step = 4; return DW::Template->render_template( 'create/next.tt', { steps_to_show => [ steps_to_show($step) ], step => $step, } ); } sub steps_to_show { my ( $given_step, %opts ) = @_; my $u = LJ::get_effective_remote(); my $code = $opts{code}; return !LJ::is_enabled('payments') || ( $LJ::USE_ACCT_CODES && $given_step == 1 && !DW::InviteCodes::Promo->is_promo_code( code => $code ) && DW::InviteCodes->paid_status( code => $code ) ) || ( $given_step > 1 && $u && $u->is_paid ) ? ( 1, 2, 4 ) : ( 1 .. 4 ); } 1;