diff --git a/lib/NGCP/Panel/Role/API/Subscribers.pm b/lib/NGCP/Panel/Role/API/Subscribers.pm index 1f52126e27..637fe96a7d 100644 --- a/lib/NGCP/Panel/Role/API/Subscribers.pm +++ b/lib/NGCP/Panel/Role/API/Subscribers.pm @@ -47,189 +47,29 @@ sub get_form { sub resource_from_item { my ($self, $c, $item, $form, $patch_mode) = @_; - my $pref; - - my $bill_resource = { $item->get_inflated_columns }; - my $prov_resource = { $item->provisioning_voip_subscriber->get_inflated_columns }; - my $customer = $self->get_customer($c, $item->contract_id); - delete $prov_resource->{domain_id}; - delete $prov_resource->{account_id}; - my %resource = %{ merge($bill_resource, $prov_resource) }; - $resource{administrative} = delete $resource{admin}; - - unless($customer->product->class eq 'pbxaccount') { - delete $resource{is_pbx_group}; - delete $resource{is_pbx_pilot}; - delete $resource{pbx_extension}; - } - unless(is_true($resource{is_pbx_group})) { - delete $resource{pbx_hunt_policy}; - delete $resource{cloud_pbx_hunt_policy}; - delete $resource{pbx_hunt_timeout}; - delete $resource{cloud_pbx_hunt_timeout}; - delete $resource{pbx_hunt_cancel_mode}; - delete $resource{cloud_pbx_hunt_cancel_mode}; - } - delete $resource{contact_id}; - if($item->contact) { - $resource{email} = $item->contact->email; - $resource{timezone} = $item->contact->timezone; - } else { - $resource{email} = undef; - $resource{timezone} = undef; - } - my $sippassword = $resource{password}; - my $webpassword = $resource{webpassword}; - if(!$form){ + if (!$form) { ($form) = $self->get_form($c); } - # form validation during PATCH causes - # fields to be removed from the %resource - # and then apply_patch() removes the fields - # that were not a part the PATCH ops from - # the database, therefore a copy of the resource - # is validated instead, preserving the original one - # when $patch_mode is enabled - my %validate_resource = %resource; - last unless $self->validate_form( + return NGCP::Panel::Utils::Subscriber::resource_from_item( c => $c, - resource => $patch_mode ? \%validate_resource : \%resource, - form => $form, - run => 0, + item => $item, + patch_mode => $patch_mode, + get_customer_code => sub { + my ($cid) = @_; + return $self->get_customer($c, $cid); + }, + validate_code => sub { + my ($resource) = @_; + return $self->validate_form( + c => $c, + resource => $resource, + form => $form, + run => 0, + ); + }, ); - - $resource{_password} = $sippassword; - $resource{_webpassword} = $webpassword; - - if($customer->product->class eq 'pbxaccount') { - if ($resource{administrative} == 1) { - $resource{ext_range_min} = $customer->voip_contract_preferences->search( - { - 'attribute.attribute' => 'ext_range_min' - }, - { - join => 'attribute', - } - )->get_column('value')->first; - - $resource{ext_range_max} = $customer->voip_contract_preferences->search( - { - 'attribute.attribute' => 'ext_range_max' - }, - { - join => 'attribute', - } - )->get_column('value')->first; - } - $resource{pbx_group_ids} = []; - foreach my $group($item->provisioning_voip_subscriber->voip_pbx_groups->search_rs(undef,{'order_by' => 'me.id'})->all) { - push @{ $resource{pbx_group_ids} }, int($group->group->voip_subscriber->id); - } - if($item->provisioning_voip_subscriber->is_pbx_group) { - $resource{pbx_groupmember_ids} = []; - foreach my $member($item->provisioning_voip_subscriber->voip_pbx_group_members->search_rs(undef,{'order_by' => 'me.id'})->all) { - push @{ $resource{pbx_groupmember_ids} }, int($member->subscriber->voip_subscriber->id); - } - } - } - - if($item->primary_number) { - $resource{primary_number}->{cc} = $item->primary_number->cc; - $resource{primary_number}->{ac} = $item->primary_number->ac; - $resource{primary_number}->{sn} = $item->primary_number->sn; - $resource{primary_number}->{number_id} = int($item->primary_number->id); - } - - my @aliases = (); - if($item->voip_numbers->first) { - foreach my $n ($item->voip_numbers->search(undef,{ - prefetch => 'voip_dbalias', - })->all) { - my $alias = { - cc => $n->cc, - ac => $n->ac, - sn => $n->sn, - number_id => int($n->id), - }; - next if ($resource{primary_number} && $resource{primary_number}->{number_id} == $alias->{number_id}); - $alias->{is_devid} = 0; - if (defined $n->voip_dbalias) { - $alias->{is_devid} = $n->voip_dbalias->is_devid; - } - $alias->{is_devid} = bool $alias->{is_devid}; - push(@aliases, $alias); - } - } - $resource{alias_numbers} = \@aliases; - - $pref = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( - c => $c, attribute => 'display_name', - prov_subscriber => $item->provisioning_voip_subscriber); - if($pref->first && $pref->first->value) { - $resource{display_name} = $pref->first->value; - } else { - $resource{display_name} = undef; - } - - $resource{id} = int($item->id); - $resource{domain} = $item->domain->domain; - - # don't leak internal info to subscribers via API for those fields - # not filtered via forms - my $contract_id = int(delete $resource{contract_id}); - if ($c->user->roles eq "admin" || $c->user->roles eq "reseller" || - $c->user->roles eq "ccareadmin" || $c->user->roles eq "ccare") { - $resource{customer_id} = $contract_id; - $resource{uuid} = $item->uuid; - - my $pref = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( - c => $c, attribute => 'lock', - prov_subscriber => $item->provisioning_voip_subscriber); - $resource{lock} = 0; - if($pref->first and length($pref->first->value) > 0) { - #cast to Numeric accordingly to the form field type and customer note in the ticket #10313 - $resource{lock} = $pref->first->value; - }else{ - $resource{lock} = undef; - } - if ($c->user->show_passwords) { - foreach my $k(qw/password webpassword/) { - eval { - if (not NGCP::Panel::Utils::Auth::is_salted_hash($resource{$k})) { - $resource{'_' . $k} = NGCP::Panel::Utils::Encryption::encrypt_rsa($c,$resource{$k}); - } else { - delete $resource{'_' . $k}; - } - }; - if ($@) { - $c->error("Failed to encrypt $k: " . $@); - delete $resource{'_' . $k}; - } - } - } else { - foreach my $k(qw/password webpassword/) { - delete $resource{'_' . $k}; - } - } - } else { - delete $resource{'password'} if $c->user->roles eq 'subscriber'; - - if ($c->user->roles eq "subscriberadmin") { - $resource{customer_id} = $contract_id; - if ($item->id != $c->user->voip_subscriber->id) { - if (!$c->config->{security}->{password}->{sip_expose_subadmin}) { - delete $resource{_password}; - } - if (!$c->config->{security}->{password}->{web_expose_subadmin}) { - delete $resource{_webpassword}; - } - } - } - } - - return \%resource; } sub hal_from_item { @@ -418,208 +258,19 @@ sub update_item { return unless $self->check_write_access($c, $item->id); - my $subscriber = $item; - my $customer = $full_resource->{customer}; - my $alias_numbers = $full_resource->{alias_numbers}; - my $preferences = $full_resource->{preferences}; - my $groups = $full_resource->{groups}; - my $groupmembers = $full_resource->{groupmembers}; - my $prov_subscriber = $subscriber->provisioning_voip_subscriber; - $self->process_form_resource($c, $item, $full_resource, $resource, $form); - if($subscriber->provisioning_voip_subscriber->is_pbx_pilot && !is_true($resource->{is_pbx_pilot})) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Cannot revoke is_pbx_pilot status from a subscriber."); - return; - } - - if($resource->{customer_id} && ( $resource->{customer_id} != $subscriber->contract->id ) ){ - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "customer_id can't be changed."); - return; - } - - if ($resource->{timezone} && !NGCP::Panel::Utils::DateTime::is_valid_timezone_name($resource->{timezone})) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "invalid timezone name."); - return; - } - - if($subscriber->status ne $resource->{status}) { - if($resource->{status} eq 'locked') { - $resource->{lock} = 4; - } elsif($subscriber->status eq 'locked' && $resource->{status} eq 'active') { - $resource->{lock} ||= 0; - } elsif($resource->{status} eq 'terminated') { - try { - NGCP::Panel::Utils::Subscriber::terminate(c => $c, subscriber => $subscriber); - return $subscriber; - } catch($e) { - $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to terminate subscriber", - "failed to terminate subscriber id ".$subscriber->id, $e); - return; - } - } - } - try { - NGCP::Panel::Utils::Subscriber::lock_provisoning_voip_subscriber( - c => $c, - prov_subscriber => $prov_subscriber, - level => $resource->{lock} || 0, - ); - } catch($e) { - $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to update subscriber lock", - "failed to lock subscriber id ".$subscriber->id." with level ".$resource->{lock}, $e); - return; - }; - - my ($error,$profile_set,$profile) = NGCP::Panel::Utils::Subscriber::check_profile_set_and_profile($c, $resource, $subscriber); - if ($error) { - $self->error($c, $error->{response_code}, $error->{description}, $error->{error}); - return; - } - - if($resource->{email} || $resource->{timezone}) { - my $contact = $subscriber->contact; - unless ($contact) { - $contact = $schema->resultset('contacts')->create({ - reseller_id => $subscriber->contract->contact->reseller_id, - }); - } - if(not $contact->email or ($contact->email ne $resource->{email})) { - $contact->update({ - email => $resource->{email}, - }); - } - if(not $contact->timezone or ($contact->timezone ne $resource->{timezone})) { - $contact->update({ - timezone => $resource->{timezone}, - }); - } - $resource->{contact_id} = $contact->id; - } elsif($subscriber->contact) { - try { - $c->log->debug("delete contact id ".$subscriber->contact->id); - $subscriber->contact->delete; - } catch($e) { - $c->log->debug("contact still in use: ".$e); - } - $resource->{contact_id} = undef; # mark for clearance - } - delete $resource->{email}; - delete $resource->{timezone}; - - my $aliases_before = NGCP::Panel::Utils::Events::get_aliases_snapshot( + return NGCP::Panel::Utils::Subscriber::update_subscriber( c => $c, schema => $schema, - subscriber => $subscriber, - ); - - try { - NGCP::Panel::Utils::Subscriber::update_subscriber_numbers( - c => $c, - schema => $schema, - primary_number => $resource->{e164}, - alias_numbers => $alias_numbers, - reseller_id => $customer->contact->reseller_id, - subscriber_id => $subscriber->id, - ); - } catch(DBIx::Class::Exception $e where { /Duplicate entry '([^']+)' for key 'number_idx'/ }) { - $e =~ /Duplicate entry '([^']+)' for key 'number_idx'/; - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Number '" . $1 . "' already exists.", "Number already exists.", - "failed to update subscriber, number " . $c->qs($1) . " already exists"); - return; - } catch ($e where { /alias \d+ already exists/ }) { - $e =~ /alias (\d+) already exists/; - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Number '" . $1 . "' already exists.", "Number already exists.", - "failed to update subscriber, alias " . $c->qs($1) . " already exists"); - return; - } catch ($e where { /aliases \S+ already exist/ }) { - $e =~ /aliases (\S+) already exist/; - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Numbers '" . $1 . "' already exist.", "Numbers already exist.", - "failed to update subscriber, aliases " . $c->qs($1) . " already exist"); - return; - } catch ($e where { /more than \d+ provided aliases/ }) { - my $err_msg = "more than 10 provided aliases already exist"; - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, $err_msg); - return; - } - - my $billing_res = { - external_id => $resource->{external_id}, - status => $resource->{status}, - contact_id => $resource->{contact_id}, - }; - - if ($resource->{password} && $resource->{password} ne $prov_subscriber->password) { - NGCP::Panel::Utils::Subscriber::insert_password_journal( - $c, $prov_subscriber, $resource->{password} - ); - } - - if ($resource->{webpassword}) { - NGCP::Panel::Utils::Subscriber::insert_webpassword_journal( - $c, $prov_subscriber, $resource->{webpassword} - ); - } - - if (exists $resource->{webpassword} and $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS) { - $resource->{webpassword} = NGCP::Panel::Utils::Auth::generate_salted_hash($resource->{webpassword}); - } - - my $provisioning_res = { - webusername => $resource->{webusername}, - is_pbx_pilot => $resource->{is_pbx_pilot} // 0, - is_pbx_group => $resource->{is_pbx_group} // 0, - modify_timestamp => NGCP::Panel::Utils::DateTime::current_local, - profile_set_id => $profile_set ? $profile_set->id : undef, - profile_id => $profile ? $profile->id : undef, - pbx_extension => $resource->{pbx_extension}, - defined $resource->{administrative} ? (admin => $resource->{administrative}) : (), - }; - $provisioning_res->{password} = $resource->{password} if exists $resource->{password}; - $provisioning_res->{webpassword} = $resource->{webpassword} if exists $resource->{webpassword}; - if(is_true($resource->{is_pbx_group})) { - $provisioning_res->{pbx_hunt_policy} = $resource->{pbx_hunt_policy}; - $provisioning_res->{pbx_hunt_timeout} = $resource->{pbx_hunt_timeout}; - $provisioning_res->{pbx_hunt_cancel_mode} = $resource->{pbx_hunt_cancel_mode}; - NGCP::Panel::Utils::Subscriber::update_preferences( - c => $c, - prov_subscriber => $prov_subscriber, - 'preferences' => { - cloud_pbx_hunt_policy => $resource->{cloud_pbx_hunt_policy} // $resource->{pbx_hunt_policy}, - cloud_pbx_hunt_timeout => $resource->{cloud_pbx_hunt_timeout} // $resource->{pbx_hunt_timeout}, - cloud_pbx_hunt_cancel_mode => $resource->{cloud_pbx_hunt_cancel_mode} // $resource->{pbx_hunt_cancel_mode}, - } - ); - } - my $old_profile = $prov_subscriber->profile_id; - - $subscriber->update($billing_res); - $subscriber->discard_changes; - $prov_subscriber->update($provisioning_res); - $prov_subscriber->discard_changes; - - NGCP::Panel::Utils::Events::insert_profile_events( - c => $c, schema => $schema, subscriber_id => $subscriber->id, - old => $old_profile, new => $prov_subscriber->profile_id, - %$aliases_before, - ); - - NGCP::Panel::Utils::Subscriber::update_preferences( - c => $c, - prov_subscriber => $prov_subscriber, - preferences => $preferences, - ); - - NGCP::Panel::Utils::Subscriber::manage_pbx_groups( - c => $c, - schema => $schema, - groups => $groups, - groupmembers => $groupmembers, - customer => $customer, - subscriber => $subscriber, + item => $item, + full_resource => $full_resource, + resource => $resource, + err_code => sub { + my ($code, $msg, @errors) = @_; + $self->error($c, $code, $msg, @errors); + }, ); - - return $subscriber; } sub check_write_access { diff --git a/lib/NGCP/Panel/Utils/ProvisioningTemplates.pm b/lib/NGCP/Panel/Utils/ProvisioningTemplates.pm index d810d386a7..e116e017c4 100644 --- a/lib/NGCP/Panel/Utils/ProvisioningTemplates.pm +++ b/lib/NGCP/Panel/Utils/ProvisioningTemplates.pm @@ -29,6 +29,7 @@ use NGCP::Panel::Utils::Subscriber qw(); use NGCP::Panel::Utils::Preferences qw(); use NGCP::Panel::Utils::Kamailio qw(); use NGCP::Panel::Utils::CallForwards qw(); +use NGCP::Panel::Utils::Events qw(); BEGIN { # Temporarily override the warning handler @@ -603,7 +604,7 @@ sub provision_commit_row { $schema, $c->stash->{provisioning_templates}->{$template}, ); - _create_subscriber( + _create_update_subscriber( $c, $context, $schema, @@ -619,7 +620,7 @@ sub provision_commit_row { if ($purge && $subscriber) { $c->log->debug("provisioning template - terminating subscriber id " . $subscriber->id); NGCP::Panel::Utils::Subscriber::terminate(c => $c, subscriber => $subscriber); - _create_subscriber( + _create_update_subscriber( $c, $context, $schema, @@ -644,7 +645,7 @@ sub provision_commit_row { $schema, $c->stash->{provisioning_templates}->{$template}, ); - _create_subscriber( + _create_update_subscriber( $c, $context, $schema, @@ -652,7 +653,7 @@ sub provision_commit_row { } else { die $msg; } - } catch($e where { /alias '([^']+)' already exists/ }) { + } catch($e where { /alias '([^']+)' already exists/ or /more than \d+ provided aliases already exist/ }) { my ($msg, $subscriber) = _get_duplicate_subs( $c, $context, @@ -671,7 +672,7 @@ sub provision_commit_row { $schema, $c->stash->{provisioning_templates}->{$template}, ); - _create_subscriber( + _create_update_subscriber( $c, $context, $schema, @@ -865,15 +866,17 @@ sub _init_contract_context { } $context->{contract_contact} = \%contract_contact; if (scalar @identifiers) { - delete $contract_contact{id}; + my $found; foreach my $e ($schema->resultset('contacts')->search_rs({ map { $_ => $contract_contact{$_}; } @identifiers })->all) { if ('terminated' ne $e->status) { $contract_contact{id} = $e->id; + $found = 1; last; } } + delete $contract_contact{id} unless $found; } else { delete $contract_contact{id}; } @@ -951,7 +954,7 @@ sub _init_contract_context { $context->{contract} = \%contract; if (scalar @identifiers) { - delete $contract{id}; + my $found; foreach my $e ($schema->resultset('contracts')->search_rs({ map { $_ => $contract{$_}; } @identifiers })->all) { @@ -960,9 +963,11 @@ sub _init_contract_context { $old_resource = { $e->get_inflated_columns }; + $found = 1; last; } } + delete $contract{id} unless $found; } else { delete $contract{id}; } @@ -1025,23 +1030,36 @@ sub _init_subscriber_context { my $item; if (scalar @identifiers) { - delete $subscriber{id}; + my $found; foreach my $e ($schema->resultset('voip_subscribers')->search_rs({ map { $_ => $subscriber{$_}; } @identifiers },{ #join => 'domain', })->all) { if ('terminated' ne $e->status) { + my $from_item = NGCP::Panel::Utils::Subscriber::resource_from_item( + c => $c, + item => $e, + ); + if ($from_item) { + foreach my $k (keys %$from_item) { + next if $k eq 'id'; + $subscriber{$k} = $from_item->{$k} unless exists $subscriber{$k}; + } + } $subscriber{id} = $e->id; $item = $e; + $found = 1; last; } } + delete $subscriber{id} unless $found; } else { delete $subscriber{id}; } $context->{subscriber}->{customer_id} //= $context->{contract}->{id}; + $context->{_subscriber_item} = $item; $context->{_cs} = NGCP::Panel::Utils::Subscriber::prepare_resource( c => $c, @@ -1064,7 +1082,7 @@ sub _init_subscriber_context { return $contract; }, item => $item, - ) unless $context->{subscriber}->{id}; + ); $c->log->debug("provisioning template - subscriber: " . Dumper($context->{subscriber})); } @@ -1404,57 +1422,73 @@ sub _set_fraud_preferences { } -sub _create_subscriber { +sub _create_update_subscriber { my ($c, $context, $schema) = @_; - if (exists $context->{subscriber} and not $context->{subscriber}->{id}) { - my $error_info = { extended => {} }; - - my @events_to_create = (); - my $event_context = { events_to_create => \@events_to_create }; - my $subscriber = NGCP::Panel::Utils::Subscriber::create_subscriber( - c => $c, - schema => $schema, - contract => $context->{_cs}->{customer}, - params => $context->{_cs}->{resource}, - preferences => $context->{_cs}->{preferences}, - admin_default => 0, - event_context => $event_context, - error => $error_info, - ); - $context->{subscriber}->{id} = $subscriber->id; - if($context->{_cs}->{resource}->{status} eq 'locked') { - NGCP::Panel::Utils::Subscriber::lock_provisoning_voip_subscriber( + if (exists $context->{subscriber}) { + if ($context->{subscriber}->{id}) { + my $subscriber = NGCP::Panel::Utils::Subscriber::update_subscriber( c => $c, - prov_subscriber => $subscriber->provisioning_voip_subscriber, - level => $context->{_cs}->{resource}->{lock} || 4, + schema => $schema, + item => $context->{_subscriber_item}, + full_resource => $context->{_cs}, + resource => $context->{_cs}->{resource}, + err_code => sub { + my ($code, $msg) = @_; + die($msg); + }, ); + die("failed to update subscriber") unless $subscriber; + $c->log->debug("provisioning template - subscriber id $context->{subscriber}->{id} updated"); } else { - NGCP::Panel::Utils::ProfilePackages::underrun_lock_subscriber(c => $c, subscriber => $subscriber); + my $error_info = { extended => {} }; + + my @events_to_create = (); + my $event_context = { events_to_create => \@events_to_create }; + my $subscriber = NGCP::Panel::Utils::Subscriber::create_subscriber( + c => $c, + schema => $schema, + contract => $context->{_cs}->{customer}, + params => $context->{_cs}->{resource}, + preferences => $context->{_cs}->{preferences}, + admin_default => 0, + event_context => $event_context, + error => $error_info, + ); + $context->{subscriber}->{id} = $subscriber->id; + if($context->{_cs}->{resource}->{status} eq 'locked') { + NGCP::Panel::Utils::Subscriber::lock_provisoning_voip_subscriber( + c => $c, + prov_subscriber => $subscriber->provisioning_voip_subscriber, + level => $context->{_cs}->{resource}->{lock} || 4, + ); + } else { + NGCP::Panel::Utils::ProfilePackages::underrun_lock_subscriber(c => $c, subscriber => $subscriber); + } + NGCP::Panel::Utils::Subscriber::update_subscriber_numbers( + c => $c, + schema => $schema, + alias_numbers => $context->{_cs}->{alias_numbers}, + reseller_id => $context->{_cs}->{customer}->contact->reseller_id, + subscriber_id => $subscriber->id, + ); + $subscriber->discard_changes; # reload row because of new number + NGCP::Panel::Utils::Subscriber::manage_pbx_groups( + c => $c, + schema => $schema, + groups => $context->{_cs}->{groups}, + groupmembers => $context->{_cs}->{groupmembers}, + customer => $context->{_cs}->{customer}, + subscriber => $subscriber, + ); + NGCP::Panel::Utils::Events::insert_deferred( + c => $c, schema => $schema, + events_to_create => \@events_to_create, + ); + + $c->log->debug("provisioning template - subscriber id $context->{subscriber}->{id} created"); } - NGCP::Panel::Utils::Subscriber::update_subscriber_numbers( - c => $c, - schema => $schema, - alias_numbers => $context->{_cs}->{alias_numbers}, - reseller_id => $context->{_cs}->{customer}->contact->reseller_id, - subscriber_id => $subscriber->id, - ); - $subscriber->discard_changes; # reload row because of new number - NGCP::Panel::Utils::Subscriber::manage_pbx_groups( - c => $c, - schema => $schema, - groups => $context->{_cs}->{groups}, - groupmembers => $context->{_cs}->{groupmembers}, - customer => $context->{_cs}->{customer}, - subscriber => $subscriber, - ); - NGCP::Panel::Utils::Events::insert_deferred( - c => $c, schema => $schema, - events_to_create => \@events_to_create, - ); - - $c->log->debug("provisioning template - subscriber id $context->{subscriber}->{id} created"); } } @@ -1820,6 +1854,36 @@ sub _get_duplicate_subs { } $subscriber = [ values %subs ]; } + } elsif ($e =~ /more than \d+ provided aliases already exist/) { + $msg = $e; + if ($get) { + $c->log->debug("provisioning template - $msg"); + my %subs = (); + my @usernames = (); + foreach my $alias (@{ $context->{_cs}->{alias_numbers} // [] }) { + my $e164 = $alias->{e164} // $alias; + push @usernames, ($e164->{cc} // '') . ($e164->{ac} // '') . ($e164->{sn} // ''); + } + if (my $e164 = $context->{_cs}->{resource}->{e164}) { + push @usernames, ($e164->{cc} // '') . ($e164->{ac} // '') . ($e164->{sn} // ''); + } + @usernames = grep { length $_ } @usernames; + if (scalar @usernames) { + my $own_prov_id = $context->{_subscriber_item} + ? $context->{_subscriber_item}->provisioning_voip_subscriber->id + : undef; + foreach my $alias ($c->model('DB')->resultset('voip_dbaliases')->search_rs({ + username => { -in => \@usernames }, + (defined $own_prov_id ? (subscriber_id => { '!=' => $own_prov_id }) : ()), + }, undef)->all) { + next unless $alias->subscriber; + my $sub = $alias->subscriber->voip_subscriber; + next unless $sub; + $subs{$sub->id} = $sub unless exists $subs{$sub->id}; + } + } + $subscriber = [ values %subs ]; + } } elsif ($idx and 'number_idx' eq $idx) { $msg = "number already exists: $e"; if ($get) { diff --git a/lib/NGCP/Panel/Utils/Subscriber.pm b/lib/NGCP/Panel/Utils/Subscriber.pm index 96b4c1051c..15e4d15949 100644 --- a/lib/NGCP/Panel/Utils/Subscriber.pm +++ b/lib/NGCP/Panel/Utils/Subscriber.pm @@ -25,6 +25,7 @@ use HTTP::Status qw(:constants); use IPC::System::Simple qw/capturex/; use File::Slurp qw/read_file/; use NGCP::Panel::Utils::Encryption qw(); +use boolean qw(true); my %LOCK = ( 0, 'none', @@ -187,6 +188,205 @@ sub get_lock_string { return $LOCK{$level}; } +sub resource_from_item { + my %params = @_; + + my ($c, $item, $patch_mode, $validate_code, $get_customer_code) = @params{qw/ + c + item + patch_mode + validate_code + get_customer_code + /}; + + my $pref; + + my $bill_resource = { $item->get_inflated_columns }; + my $prov_resource = { $item->provisioning_voip_subscriber->get_inflated_columns }; + my $customer = $get_customer_code + ? $get_customer_code->($item->contract_id) + : $item->contract; + return unless $customer; + + delete $prov_resource->{domain_id}; + delete $prov_resource->{account_id}; + my %resource = %{ merge($bill_resource, $prov_resource) }; + $resource{administrative} = delete $resource{admin}; + + unless ($customer->product->class eq 'pbxaccount') { + delete $resource{is_pbx_group}; + delete $resource{is_pbx_pilot}; + delete $resource{pbx_extension}; + } + unless (is_true($resource{is_pbx_group})) { + delete $resource{pbx_hunt_policy}; + delete $resource{cloud_pbx_hunt_policy}; + delete $resource{pbx_hunt_timeout}; + delete $resource{cloud_pbx_hunt_timeout}; + delete $resource{pbx_hunt_cancel_mode}; + delete $resource{cloud_pbx_hunt_cancel_mode}; + } + delete $resource{contact_id}; + if ($item->contact) { + $resource{email} = $item->contact->email; + $resource{timezone} = $item->contact->timezone; + } else { + $resource{email} = undef; + $resource{timezone} = undef; + } + my $sippassword = $resource{password}; + my $webpassword = $resource{webpassword}; + + if ($validate_code) { + # form validation during PATCH causes fields to be removed from the + # %resource and then apply_patch() removes the fields that were not a + # part the PATCH ops from the database, therefore a copy of the resource + # is validated instead, preserving the original one when $patch_mode is + # enabled + my %validate_resource = %resource; + return unless $validate_code->($patch_mode ? \%validate_resource : \%resource); + } + + $resource{_password} = $sippassword; + $resource{_webpassword} = $webpassword; + + if ($customer->product->class eq 'pbxaccount') { + if ($resource{administrative} == 1) { + $resource{ext_range_min} = $customer->voip_contract_preferences->search( + { + 'attribute.attribute' => 'ext_range_min' + }, + { + join => 'attribute', + } + )->get_column('value')->first; + + $resource{ext_range_max} = $customer->voip_contract_preferences->search( + { + 'attribute.attribute' => 'ext_range_max' + }, + { + join => 'attribute', + } + )->get_column('value')->first; + } + $resource{pbx_group_ids} = []; + foreach my $group ($item->provisioning_voip_subscriber->voip_pbx_groups->search_rs(undef, { 'order_by' => 'me.id' })->all) { + push @{ $resource{pbx_group_ids} }, int($group->group->voip_subscriber->id); + } + if ($item->provisioning_voip_subscriber->is_pbx_group) { + $resource{pbx_groupmember_ids} = []; + foreach my $member ($item->provisioning_voip_subscriber->voip_pbx_group_members->search_rs(undef, { 'order_by' => 'me.id' })->all) { + push @{ $resource{pbx_groupmember_ids} }, int($member->subscriber->voip_subscriber->id); + } + } + } + + if ($item->primary_number) { + $resource{primary_number}->{cc} = $item->primary_number->cc; + $resource{primary_number}->{ac} = $item->primary_number->ac; + $resource{primary_number}->{sn} = $item->primary_number->sn; + $resource{primary_number}->{number_id} = int($item->primary_number->id); + } else { + $resource{primary_number} = undef; + } + + my @aliases = (); + if ($item->voip_numbers->first) { + foreach my $n ($item->voip_numbers->search(undef, { + prefetch => 'voip_dbalias', + })->all) { + my $alias = { + cc => $n->cc, + ac => $n->ac, + sn => $n->sn, + number_id => int($n->id), + }; + next if ($resource{primary_number} && $resource{primary_number}->{number_id} == $alias->{number_id}); + $alias->{is_devid} = 0; + if (defined $n->voip_dbalias) { + $alias->{is_devid} = $n->voip_dbalias->is_devid; + } + $alias->{is_devid} = bool $alias->{is_devid}; + push(@aliases, $alias); + } + } + $resource{alias_numbers} = \@aliases; + + $pref = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( + c => $c, attribute => 'display_name', + prov_subscriber => $item->provisioning_voip_subscriber); + if ($pref && $pref->first && $pref->first->value) { + $resource{display_name} = $pref->first->value; + } else { + $resource{display_name} = undef; + } + + $resource{id} = int($item->id); + $resource{domain} = $item->domain->domain; + + # don't leak internal info to subscribers via API for those fields + # not filtered via forms + my $contract_id = int(delete $resource{contract_id}); + + # ngcp-provisioning-template Catalyst stub has no user(); presume admin. + my $user = ($c->can('user') ? $c->user : undef); + my $roles = $user ? $user->roles : 'admin'; + my $show_passwords = ($user && $user->can('show_passwords')) ? $user->show_passwords : 0; + + if ($roles eq "admin" || $roles eq "reseller" || + $roles eq "ccareadmin" || $roles eq "ccare") { + $resource{customer_id} = $contract_id; + $resource{uuid} = $item->uuid; + + my $lock_pref = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( + c => $c, attribute => 'lock', + prov_subscriber => $item->provisioning_voip_subscriber); + $resource{lock} = 0; + if ($lock_pref && $lock_pref->first && length($lock_pref->first->value) > 0) { + #cast to Numeric accordingly to the form field type and customer note in the ticket #10313 + $resource{lock} = $lock_pref->first->value; + } else { + $resource{lock} = undef; + } + if ($show_passwords) { + foreach my $k (qw/password webpassword/) { + eval { + if (not NGCP::Panel::Utils::Auth::is_salted_hash($resource{$k})) { + $resource{'_' . $k} = NGCP::Panel::Utils::Encryption::encrypt_rsa($c, $resource{$k}); + } else { + delete $resource{'_' . $k}; + } + }; + if ($@) { + $c->log->error("Failed to encrypt $k: " . $@) if $c->can('log'); + delete $resource{'_' . $k}; + } + } + } else { + foreach my $k (qw/password webpassword/) { + delete $resource{'_' . $k}; + } + } + } else { + delete $resource{'password'} if $roles eq 'subscriber'; + + if ($roles eq "subscriberadmin") { + $resource{customer_id} = $contract_id; + if ($user && $item->id != $user->voip_subscriber->id) { + if (!$c->config->{security}->{password}->{sip_expose_subadmin}) { + delete $resource{_password}; + } + if (!$c->config->{security}->{password}->{web_expose_subadmin}) { + delete $resource{_webpassword}; + } + } + } + } + + return \%resource; +} + sub prepare_resource { my %params = @_; @@ -901,6 +1101,229 @@ sub create_subscriber { }); } +sub update_subscriber { + my %params = @_; + + my ($c, $schema, $item, $full_resource, $resource, $err_code) = @params{qw/ + c + schema + item + full_resource + resource + err_code + /}; + + if (!defined $err_code || ref $err_code ne 'CODE') { + $err_code = sub { }; + } + + $schema //= $c->model('DB'); + $resource //= $full_resource->{resource}; + + my $subscriber = $item; + my $customer = $full_resource->{customer}; + my $alias_numbers = $full_resource->{alias_numbers}; + my $preferences = $full_resource->{preferences}; + my $groups = $full_resource->{groups}; + my $groupmembers = $full_resource->{groupmembers}; + my $prov_subscriber = $subscriber->provisioning_voip_subscriber; + + $resource->{timezone} = $resource->{timezone}->{name} if 'HASH' eq ref $resource->{timezone}; + $resource->{timezone} = NGCP::Panel::Utils::DateTime::get_timezone_link($c, $resource->{timezone}); + + if ($subscriber->provisioning_voip_subscriber->is_pbx_pilot && !is_true($resource->{is_pbx_pilot})) { + &$err_code(HTTP_UNPROCESSABLE_ENTITY, "Cannot revoke is_pbx_pilot status from a subscriber."); + return; + } + + if ($resource->{customer_id} && ($resource->{customer_id} != $subscriber->contract->id)) { + &$err_code(HTTP_UNPROCESSABLE_ENTITY, "customer_id can't be changed."); + return; + } + + if ($resource->{timezone} && !NGCP::Panel::Utils::DateTime::is_valid_timezone_name($resource->{timezone})) { + &$err_code(HTTP_UNPROCESSABLE_ENTITY, "invalid timezone name."); + return; + } + + if ($subscriber->status ne $resource->{status}) { + if ($resource->{status} eq 'locked') { + $resource->{lock} = 4; + } elsif ($subscriber->status eq 'locked' && $resource->{status} eq 'active') { + $resource->{lock} ||= 0; + } elsif ($resource->{status} eq 'terminated') { + try { + terminate(c => $c, subscriber => $subscriber); + return $subscriber; + } catch ($e) { + &$err_code(HTTP_INTERNAL_SERVER_ERROR, "Failed to terminate subscriber", + "failed to terminate subscriber id ".$subscriber->id, $e); + return; + } + } + } + try { + lock_provisoning_voip_subscriber( + c => $c, + prov_subscriber => $prov_subscriber, + level => $resource->{lock} || 0, + ); + } catch ($e) { + &$err_code(HTTP_INTERNAL_SERVER_ERROR, "Failed to update subscriber lock", + "failed to lock subscriber id ".$subscriber->id." with level ".$resource->{lock}, $e); + return; + } + + my ($error, $profile_set, $profile) = check_profile_set_and_profile($c, $resource, $subscriber); + if ($error) { + &$err_code($error->{response_code}, $error->{description}, $error->{error}); + return; + } + + if ($resource->{email} || $resource->{timezone}) { + my $contact = $subscriber->contact; + unless ($contact) { + $contact = $schema->resultset('contacts')->create({ + reseller_id => $subscriber->contract->contact->reseller_id, + }); + } + if (not $contact->email or ($contact->email ne $resource->{email})) { + $contact->update({ + email => $resource->{email}, + }); + } + if (not $contact->timezone or ($contact->timezone ne $resource->{timezone})) { + $contact->update({ + timezone => $resource->{timezone}, + }); + } + $resource->{contact_id} = $contact->id; + } elsif ($subscriber->contact) { + try { + $c->log->debug("delete contact id ".$subscriber->contact->id); + $subscriber->contact->delete; + } catch ($e) { + $c->log->debug("contact still in use: ".$e); + } + $resource->{contact_id} = undef; # mark for clearance + } + delete $resource->{email}; + delete $resource->{timezone}; + + my $aliases_before = NGCP::Panel::Utils::Events::get_aliases_snapshot( + c => $c, + schema => $schema, + subscriber => $subscriber, + ); + + try { + update_subscriber_numbers( + c => $c, + schema => $schema, + primary_number => $resource->{e164}, + alias_numbers => $alias_numbers, + reseller_id => $customer->contact->reseller_id, + subscriber_id => $subscriber->id, + ); + } catch (DBIx::Class::Exception $e where { /Duplicate entry '([^']+)' for key 'number_idx'/ }) { + $e =~ /Duplicate entry '([^']+)' for key 'number_idx'/; + &$err_code(HTTP_UNPROCESSABLE_ENTITY, "Number '" . $1 . "' already exists.", "Number already exists.", + "failed to update subscriber, number " . $1 . " already exists"); + return; + } catch ($e where { /alias \d+ already exists/ }) { + $e =~ /alias (\d+) already exists/; + &$err_code(HTTP_UNPROCESSABLE_ENTITY, "Number '" . $1 . "' already exists.", "Number already exists.", + "failed to update subscriber, alias " . $1 . " already exists"); + return; + } catch ($e where { /aliases \S+ already exist/ }) { + $e =~ /aliases (\S+) already exist/; + &$err_code(HTTP_UNPROCESSABLE_ENTITY, "Numbers '" . $1 . "' already exist.", "Numbers already exist.", + "failed to update subscriber, aliases " . $1 . " already exist"); + return; + } catch ($e where { /more than \d+ provided aliases/ }) { + &$err_code(HTTP_UNPROCESSABLE_ENTITY, "more than 10 provided aliases already exist"); + return; + } + + my $billing_res = { + external_id => $resource->{external_id}, + status => $resource->{status}, + contact_id => $resource->{contact_id}, + }; + + if ($resource->{password} && $resource->{password} ne $prov_subscriber->password) { + insert_password_journal( + $c, $prov_subscriber, $resource->{password} + ); + } + + if ($resource->{webpassword}) { + insert_webpassword_journal( + $c, $prov_subscriber, $resource->{webpassword} + ); + } + + if (exists $resource->{webpassword} and $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS) { + $resource->{webpassword} = NGCP::Panel::Utils::Auth::generate_salted_hash($resource->{webpassword}); + } + + my $provisioning_res = { + webusername => $resource->{webusername}, + is_pbx_pilot => $resource->{is_pbx_pilot} // 0, + is_pbx_group => $resource->{is_pbx_group} // 0, + modify_timestamp => NGCP::Panel::Utils::DateTime::current_local, + profile_set_id => $profile_set ? $profile_set->id : undef, + profile_id => $profile ? $profile->id : undef, + pbx_extension => $resource->{pbx_extension}, + defined $resource->{administrative} ? (admin => $resource->{administrative}) : (), + }; + $provisioning_res->{password} = $resource->{password} if exists $resource->{password}; + $provisioning_res->{webpassword} = $resource->{webpassword} if exists $resource->{webpassword}; + if (is_true($resource->{is_pbx_group})) { + $provisioning_res->{pbx_hunt_policy} = $resource->{pbx_hunt_policy}; + $provisioning_res->{pbx_hunt_timeout} = $resource->{pbx_hunt_timeout}; + $provisioning_res->{pbx_hunt_cancel_mode} = $resource->{pbx_hunt_cancel_mode}; + update_preferences( + c => $c, + prov_subscriber => $prov_subscriber, + 'preferences' => { + cloud_pbx_hunt_policy => $resource->{cloud_pbx_hunt_policy} // $resource->{pbx_hunt_policy}, + cloud_pbx_hunt_timeout => $resource->{cloud_pbx_hunt_timeout} // $resource->{pbx_hunt_timeout}, + cloud_pbx_hunt_cancel_mode => $resource->{cloud_pbx_hunt_cancel_mode} // $resource->{pbx_hunt_cancel_mode}, + } + ); + } + my $old_profile = $prov_subscriber->profile_id; + + $subscriber->update($billing_res); + $subscriber->discard_changes; + $prov_subscriber->update($provisioning_res); + $prov_subscriber->discard_changes; + + NGCP::Panel::Utils::Events::insert_profile_events( + c => $c, schema => $schema, subscriber_id => $subscriber->id, + old => $old_profile, new => $prov_subscriber->profile_id, + %$aliases_before, + ); + + update_preferences( + c => $c, + prov_subscriber => $prov_subscriber, + preferences => $preferences, + ); + + manage_pbx_groups( + c => $c, + schema => $schema, + groups => $groups, + groupmembers => $groupmembers, + customer => $customer, + subscriber => $subscriber, + ); + + return $subscriber; +} + sub check_profile_set_and_profile { my ($c, $resource, $subscriber) = @_;