mourningdove/cgi-bin/DW/Controller/Create.pm

739 lines
26 KiB
Perl
Raw Normal View History

2026-05-24 01:03:05 +00:00
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 <janine@netrophic.com>
# Afuna <coder.dw@afunamatata.com>
#
# 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;