mirror of
https://github.com/internetee/registry.git
synced 2025-05-17 09:57:23 +02:00
597 lines
18 KiB
Ruby
597 lines
18 KiB
Ruby
class Contact < ActiveRecord::Base
|
|
include Versions # version/contact_version.rb
|
|
include EppErrors
|
|
include UserEvents
|
|
|
|
belongs_to :registrar
|
|
has_many :domain_contacts
|
|
has_many :domains, through: :domain_contacts
|
|
has_many :legal_documents, as: :documentable
|
|
has_many :registrant_domains, class_name: 'Domain', foreign_key: 'registrant_id' # when contant is registrant
|
|
|
|
# TODO: remove later
|
|
has_many :depricated_statuses, class_name: 'DepricatedContactStatus', dependent: :destroy
|
|
|
|
has_paper_trail class_name: "ContactVersion", meta: { children: :children_log }
|
|
|
|
attr_accessor :legal_document_id
|
|
alias_attribute :kind, :ident_type
|
|
|
|
accepts_nested_attributes_for :legal_documents
|
|
|
|
validates :name, :phone, :email, :ident, :ident_type, :registrar, presence: true
|
|
validates :street, :city, :zip, :country_code, presence: true, if: 'self.class.address_processing?'
|
|
|
|
validates :phone, format: /\+[0-9]{1,3}\.[0-9]{1,14}?/, phone: true
|
|
|
|
validates :email, format: /@/
|
|
validates :email, email_format: { message: :invalid }, if: proc { |c| c.email_changed? }
|
|
validates :ident,
|
|
format: { with: /\d{4}-\d{2}-\d{2}/, message: :invalid_birthday_format },
|
|
if: proc { |c| c.ident_type == 'birthday' }
|
|
validates :ident_country_code, presence: true, if: proc { |c| %w(org priv).include? c.ident_type }, on: :create
|
|
validates :code,
|
|
uniqueness: { message: :epp_id_taken },
|
|
format: { with: /\A[\w\-\:\.\_]*\z/i, message: :invalid },
|
|
length: { maximum: 100, message: :too_long_contact_code }
|
|
|
|
validate :val_ident_type
|
|
validate :val_ident_valid_format?
|
|
validate :validate_html
|
|
validate :validate_country_code
|
|
validate :validate_ident_country_code
|
|
|
|
after_initialize do
|
|
self.status_notes = {} if status_notes.nil?
|
|
self.ident_updated_at = Time.zone.now if new_record? && ident_updated_at.blank?
|
|
end
|
|
|
|
before_validation :to_upcase_country_code
|
|
before_validation :strip_email
|
|
before_create :generate_auth_info
|
|
|
|
before_update :manage_emails
|
|
def manage_emails
|
|
return nil unless email_changed?
|
|
return nil unless deliver_emails == true
|
|
emails = []
|
|
emails << [email, email_was]
|
|
# emails << domains.map(&:registrant_email) if domains.present?
|
|
emails = emails.flatten.uniq
|
|
emails.each do |e|
|
|
ContactMailer.email_updated(email_was, e, id, deliver_emails).deliver
|
|
end
|
|
end
|
|
|
|
|
|
after_save :update_related_whois_records
|
|
|
|
# for overwrite when doing children loop
|
|
attr_writer :domains_present
|
|
|
|
scope :current_registrars, ->(id) { where(registrar_id: id) }
|
|
|
|
ORG = 'org'
|
|
PRIV = 'priv'
|
|
BIRTHDAY = 'birthday'.freeze
|
|
PASSPORT = 'passport'
|
|
|
|
IDENT_TYPES = [
|
|
ORG, # Company registry code (or similar)
|
|
PRIV, # National idendtification number
|
|
BIRTHDAY # Birthday date
|
|
]
|
|
|
|
attr_accessor :deliver_emails
|
|
attr_accessor :domain_transfer # hack but solves problem faster
|
|
|
|
#
|
|
# STATUSES
|
|
#
|
|
# Requests to delete the object MUST be rejected.
|
|
CLIENT_DELETE_PROHIBITED = 'clientDeleteProhibited'
|
|
SERVER_DELETE_PROHIBITED = 'serverDeleteProhibited'
|
|
|
|
# Requests to transfer the object MUST be rejected.
|
|
CLIENT_TRANSFER_PROHIBITED = 'clientTransferProhibited'
|
|
SERVER_TRANSFER_PROHIBITED = 'serverTransferProhibited'
|
|
|
|
# The contact object has at least one active association with
|
|
# another object, such as a domain object. Servers SHOULD provide
|
|
# services to determine existing object associations.
|
|
# "linked" status MAY be combined with any status.
|
|
LINKED = 'linked'
|
|
|
|
# This is the normal status value for an object that has no pending
|
|
# operations or prohibitions. This value is set and removed by the
|
|
# server as other status values are added or removed.
|
|
# "ok" status MAY only be combined with "linked" status.
|
|
OK = 'ok'
|
|
|
|
# Requests to update the object (other than to remove this status) MUST be rejected.
|
|
CLIENT_UPDATE_PROHIBITED = 'clientUpdateProhibited'
|
|
SERVER_UPDATE_PROHIBITED = 'serverUpdateProhibited'
|
|
|
|
# A transform command has been processed for the object, but the
|
|
# action has not been completed by the server. Server operators can
|
|
# delay action completion for a variety of reasons, such as to allow
|
|
# for human review or third-party action. A transform command that
|
|
# is processed, but whose requested action is pending, is noted with
|
|
# response code 1001.
|
|
# When the requested action has been completed, the pendingCreate,
|
|
# pendingDelete, pendingTransfer, or pendingUpdate status value MUST be
|
|
# removed. All clients involved in the transaction MUST be notified
|
|
# using a service message that the action has been completed and that
|
|
# the status of the object has changed.
|
|
# The pendingCreate, pendingDelete, pendingTransfer, and pendingUpdate
|
|
# status values MUST NOT be combined with each other.
|
|
PENDING_CREATE = 'pendingCreate'
|
|
# "pendingTransfer" status MUST NOT be combined with either
|
|
# "clientTransferProhibited" or "serverTransferProhibited" status.
|
|
PENDING_TRANSFER = 'pendingTransfer'
|
|
# "pendingUpdate" status MUST NOT be combined with either
|
|
# "clientUpdateProhibited" or "serverUpdateProhibited" status.
|
|
PENDING_UPDATE = 'pendingUpdate'
|
|
# "pendingDelete" MUST NOT be combined with either
|
|
# "clientDeleteProhibited" or "serverDeleteProhibited" status.
|
|
PENDING_DELETE = 'pendingDelete'
|
|
|
|
STATUSES = [
|
|
CLIENT_DELETE_PROHIBITED, SERVER_DELETE_PROHIBITED,
|
|
CLIENT_TRANSFER_PROHIBITED,
|
|
SERVER_TRANSFER_PROHIBITED, CLIENT_UPDATE_PROHIBITED, SERVER_UPDATE_PROHIBITED,
|
|
OK, PENDING_CREATE, PENDING_DELETE, PENDING_TRANSFER,
|
|
PENDING_UPDATE, LINKED
|
|
]
|
|
|
|
CLIENT_STATUSES = [
|
|
CLIENT_DELETE_PROHIBITED, CLIENT_TRANSFER_PROHIBITED,
|
|
CLIENT_UPDATE_PROHIBITED
|
|
]
|
|
|
|
SERVER_STATUSES = [
|
|
SERVER_UPDATE_PROHIBITED,
|
|
SERVER_DELETE_PROHIBITED,
|
|
SERVER_TRANSFER_PROHIBITED
|
|
]
|
|
#
|
|
# END OF STATUSES
|
|
#
|
|
|
|
class << self
|
|
def search_by_query(query)
|
|
res = search(code_cont: query).result
|
|
res.reduce([]) { |o, v| o << { id: v[:id], display_key: "#{v.name} (#{v.code})" } }
|
|
end
|
|
|
|
def find_orphans
|
|
where('
|
|
NOT EXISTS(
|
|
select 1 from domains d where d.registrant_id = contacts.id
|
|
) AND NOT EXISTS(
|
|
select 1 from domain_contacts dc where dc.contact_id = contacts.id
|
|
)
|
|
')
|
|
end
|
|
|
|
def find_linked
|
|
where('
|
|
EXISTS(
|
|
select 1 from domains d where d.registrant_id = contacts.id
|
|
) OR EXISTS(
|
|
select 1 from domain_contacts dc where dc.contact_id = contacts.id
|
|
)
|
|
')
|
|
end
|
|
|
|
def filter_by_states in_states
|
|
states = Array(in_states).dup
|
|
scope = all
|
|
|
|
# all contacts has state ok, so no need to filter by it
|
|
scope = scope.where("NOT contacts.statuses && ?::varchar[]", "{#{(STATUSES - [OK, LINKED]).join(',')}}") if states.delete(OK)
|
|
scope = scope.find_linked if states.delete(LINKED)
|
|
scope = scope.where("contacts.statuses @> ?::varchar[]", "{#{states.join(',')}}") if states.any?
|
|
scope
|
|
end
|
|
|
|
# To leave only new ones we need to check
|
|
# if contact was at any time used in domain.
|
|
# This can be checked by domain history.
|
|
# This can be checked by saved relations in children attribute
|
|
def destroy_orphans
|
|
STDOUT << "#{Time.zone.now.utc} - Destroying orphaned contacts\n" unless Rails.env.test?
|
|
|
|
counter = Counter.new
|
|
find_orphans.find_each do |contact|
|
|
ver_scope = []
|
|
%w(admin_contacts tech_contacts registrant).each do |type|
|
|
ver_scope << "(children->'#{type}')::jsonb <@ json_build_array(#{contact.id})::jsonb"
|
|
end
|
|
next if DomainVersion.where("created_at > ?", Time.now - Setting.orphans_contacts_in_months.to_i.months).where(ver_scope.join(" OR ")).any?
|
|
next if contact.domains_present?
|
|
|
|
contact.destroy
|
|
counter.next
|
|
STDOUT << "#{Time.zone.now.utc} Contact.destroy_orphans: ##{contact.id} (#{contact.name})\n" unless Rails.env.test?
|
|
end
|
|
|
|
STDOUT << "#{Time.zone.now.utc} - Successfully destroyed #{counter} orphaned contacts\n" unless Rails.env.test?
|
|
end
|
|
|
|
def privs
|
|
where("ident_type = '#{PRIV}'")
|
|
end
|
|
|
|
def admin_statuses
|
|
[
|
|
SERVER_UPDATE_PROHIBITED,
|
|
SERVER_DELETE_PROHIBITED
|
|
]
|
|
end
|
|
|
|
def admin_statuses_map
|
|
[
|
|
['UpdateProhibited', SERVER_UPDATE_PROHIBITED],
|
|
['DeleteProhibited', SERVER_DELETE_PROHIBITED]
|
|
]
|
|
end
|
|
|
|
def to_csv
|
|
CSV.generate do |csv|
|
|
csv << column_names
|
|
all.each do |contact|
|
|
csv << contact.attributes.values_at(*column_names)
|
|
end
|
|
end
|
|
end
|
|
|
|
def pdf(html)
|
|
kit = PDFKit.new(html)
|
|
kit.to_pdf
|
|
end
|
|
|
|
def names
|
|
pluck(:name)
|
|
end
|
|
|
|
def emails
|
|
pluck(:email)
|
|
end
|
|
|
|
def address_processing?
|
|
Setting.address_processing
|
|
end
|
|
|
|
def address_attribute_names
|
|
%w(
|
|
city
|
|
street
|
|
zip
|
|
country_code
|
|
state
|
|
)
|
|
end
|
|
end
|
|
|
|
def roid
|
|
"EIS-#{id}"
|
|
end
|
|
|
|
# kind of decorator in order to always return statuses
|
|
# if we use separate decorator, then we should add it
|
|
# to too many places
|
|
def statuses
|
|
calculated = Array(read_attribute(:statuses))
|
|
calculated.delete(Contact::OK)
|
|
calculated.delete(Contact::LINKED)
|
|
calculated << Contact::OK if calculated.empty?# && valid?
|
|
calculated << Contact::LINKED if domains_present?
|
|
|
|
calculated.uniq
|
|
end
|
|
|
|
def statuses= arr
|
|
write_attribute(:statuses, Array(arr).uniq)
|
|
end
|
|
|
|
def to_s
|
|
name || '[no name]'
|
|
end
|
|
|
|
def val_ident_type
|
|
errors.add(:ident_type, :epp_ident_type_invalid, code: code) if !%w(org priv birthday).include?(ident_type)
|
|
end
|
|
|
|
def val_ident_valid_format?
|
|
case ident_country_code
|
|
when 'EE'.freeze
|
|
err_msg = "invalid_EE_identity_format#{"_update" if id}".to_sym
|
|
case ident_type
|
|
when 'priv'.freeze
|
|
errors.add(:ident, err_msg) unless Isikukood.new(ident).valid?
|
|
when 'org'.freeze
|
|
# !%w(1 7 8 9).freeze.include?(ident.first) ||
|
|
if ident.size != 8 || !(ident =~/\A[0-9]{8}\z/)
|
|
errors.add(:ident, err_msg)
|
|
end
|
|
when BIRTHDAY
|
|
errors.add(:ident, err_msg) if id.blank? # only for create action right now. Later for all of them
|
|
end
|
|
end
|
|
end
|
|
|
|
def validate_html
|
|
self.class.columns.each do |column|
|
|
next unless column.type == :string
|
|
|
|
c_name = column.name
|
|
val = read_attribute(c_name)
|
|
if val && (val.include?('<') || val.include?('>') || val.include?('%3C') || val.include?('%3E'))
|
|
errors.add(c_name, :invalid)
|
|
return # want to run code faster
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
def org?
|
|
ident_type == ORG
|
|
end
|
|
|
|
# it might mean priv or birthday type
|
|
def priv?
|
|
!org?
|
|
end
|
|
|
|
def birthday?
|
|
ident_type == BIRTHDAY
|
|
end
|
|
|
|
def generate_auth_info
|
|
return if @generate_auth_info_disabled
|
|
return if auth_info.present?
|
|
self.auth_info = SecureRandom.hex(11)
|
|
end
|
|
|
|
def disable_generate_auth_info! # needed for testing
|
|
@generate_auth_info_disabled = true
|
|
end
|
|
|
|
# def auth_info=(pw)
|
|
# self[:auth_info] = pw if new_record?
|
|
# end
|
|
|
|
def code=(code)
|
|
self[:code] = code if new_record? # cannot change code later
|
|
end
|
|
|
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
def generate_code
|
|
return nil unless new_record?
|
|
return nil if registrar.blank?
|
|
code = self[:code]
|
|
|
|
# custom code from client
|
|
# add prefix when needed
|
|
if code.present?
|
|
prefix, *custom_code = code.split(':')
|
|
code = custom_code.join(':') if prefix == registrar.code
|
|
end
|
|
|
|
code = SecureRandom.hex(4) if code.blank? || code == registrar.code
|
|
|
|
self[:code] = "#{registrar.code}:#{code}".upcase
|
|
end
|
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
|
|
def country
|
|
Country.new(country_code)
|
|
end
|
|
|
|
# TODO: refactor, it should not allow to destroy with normal destroy,
|
|
# no need separate method
|
|
# should use only in transaction
|
|
def destroy_and_clean frame
|
|
if domains_present?
|
|
errors.add(:domains, :exist)
|
|
return false
|
|
end
|
|
|
|
legal_document_data = Epp::Domain.parse_legal_document_from_frame(frame)
|
|
|
|
if legal_document_data
|
|
|
|
doc = LegalDocument.create(
|
|
documentable_type: Contact,
|
|
document_type: legal_document_data[:type],
|
|
body: legal_document_data[:body]
|
|
)
|
|
self.legal_documents = [doc]
|
|
self.legal_document_id = doc.id
|
|
self.save
|
|
end
|
|
destroy
|
|
end
|
|
|
|
def to_upcase_country_code
|
|
self.ident_country_code = ident_country_code.upcase if ident_country_code
|
|
self.country_code = country_code.upcase if country_code
|
|
end
|
|
|
|
def validate_country_code
|
|
return unless country_code
|
|
errors.add(:country_code, :invalid) unless Country.new(country_code)
|
|
end
|
|
|
|
def validate_ident_country_code
|
|
errors.add(:ident, :invalid_country_code) unless Country.new(ident_country_code)
|
|
end
|
|
|
|
def related_domain_descriptions
|
|
@desc = {}
|
|
|
|
registrant_domains.each do |dom|
|
|
@desc[dom.name] ||= []
|
|
@desc[dom.name] << :registrant
|
|
end
|
|
|
|
domain_contacts.each do |dc|
|
|
@desc[dc.domain.name] ||= []
|
|
@desc[dc.domain.name] << dc.name.downcase.to_sym
|
|
@desc[dc.domain.name] = @desc[dc.domain.name].compact
|
|
end
|
|
|
|
@desc
|
|
end
|
|
|
|
def status_notes_array=(notes)
|
|
self.status_notes = {}
|
|
notes ||= []
|
|
statuses.each_with_index do |status, i|
|
|
status_notes[status] = notes[i]
|
|
end
|
|
end
|
|
|
|
# optimization under children loop,
|
|
# otherwise bullet will not be happy
|
|
def domains_present?
|
|
return @domains_present if @domains_present
|
|
domain_contacts.present? || registrant_domains.present?
|
|
end
|
|
|
|
|
|
def search_name
|
|
"#{code} #{name}"
|
|
end
|
|
|
|
def strip_email
|
|
self.email = email.to_s.strip
|
|
end
|
|
|
|
|
|
# what we can do load firstly by registrant
|
|
# if total is smaller than needed, the load more
|
|
# we also need to sort by valid_to
|
|
# todo: extract to drapper. Then we can remove Domain#roles
|
|
def all_domains(page: nil, per: nil, params: {})
|
|
# compose filter sql
|
|
filter_sql = case params[:domain_filter]
|
|
when "Registrant".freeze
|
|
%Q{select id from domains where registrant_id=#{id}}
|
|
when AdminDomainContact.to_s, TechDomainContact.to_s
|
|
%Q{select domain_id from domain_contacts where contact_id=#{id} AND type='#{params[:domain_filter]}'}
|
|
else
|
|
%Q{select domain_id from domain_contacts where contact_id=#{id} UNION select id from domains where registrant_id=#{id}}
|
|
end
|
|
|
|
# get sorting rules
|
|
sorts = params.fetch(:sort, {}).first || []
|
|
sort = Domain.column_names.include?(sorts.first) ? sorts.first : "valid_to"
|
|
order = {"asc"=>"desc", "desc"=>"asc"}[sorts.second] || "desc"
|
|
|
|
|
|
# fetch domains
|
|
domains = Domain.where("domains.id IN (#{filter_sql})")
|
|
domains = domains.where("domains.id" => params[:leave_domains]) if params[:leave_domains]
|
|
domains = domains.includes(:registrar).page(page).per(per)
|
|
|
|
if sorts.first == "registrar_name".freeze
|
|
# using small rails hack to generate outer join
|
|
domains = domains.includes(:registrar).where.not(registrars: {id: nil}).order("registrars.name #{order} NULLS LAST")
|
|
else
|
|
domains = domains.order("#{sort} #{order} NULLS LAST")
|
|
end
|
|
|
|
|
|
|
|
# adding roles. Need here to make faster sqls
|
|
domain_c = Hash.new([])
|
|
registrant_domains.where(id: domains.map(&:id)).each{|d| domain_c[d.id] |= ["Registrant".freeze] }
|
|
DomainContact.where(contact_id: id, domain_id: domains.map(&:id)).each{|d| domain_c[d.domain_id] |= [d.type] }
|
|
domains.each{|d| d.roles = domain_c[d.id].uniq}
|
|
|
|
domains
|
|
end
|
|
|
|
def all_registrant_domains(page: nil, per: nil, params: {}, registrant: nil)
|
|
|
|
if registrant
|
|
sorts = params.fetch(:sort, {}).first || []
|
|
sort = Domain.column_names.include?(sorts.first) ? sorts.first : "valid_to"
|
|
order = {"asc"=>"desc", "desc"=>"asc"}[sorts.second] || "desc"
|
|
|
|
domain_ids = DomainContact.distinct.where(contact_id: registrant.id).pluck(:domain_id)
|
|
|
|
domains = Domain.where(id: domain_ids).includes(:registrar).page(page).per(per)
|
|
if sorts.first == "registrar_name".freeze
|
|
domains = domains.includes(:registrar).where.not(registrars: {id: nil}).order("registrars.name #{order} NULLS LAST")
|
|
else
|
|
domains = domains.order("#{sort} #{order} NULLS LAST")
|
|
end
|
|
|
|
domain_c = Hash.new([])
|
|
registrant_domains.where(id: domains.map(&:id)).each{|d| domain_c[d.id] |= ["Registrant".freeze] }
|
|
DomainContact.where(contact_id: id, domain_id: domains.map(&:id)).each{|d| domain_c[d.domain_id] |= [d.type] }
|
|
domains.each{|d| d.roles = domain_c[d.id].uniq}
|
|
domains
|
|
end
|
|
end
|
|
|
|
|
|
def update_prohibited?
|
|
(statuses & [
|
|
CLIENT_UPDATE_PROHIBITED,
|
|
SERVER_UPDATE_PROHIBITED,
|
|
CLIENT_TRANSFER_PROHIBITED,
|
|
SERVER_TRANSFER_PROHIBITED,
|
|
PENDING_CREATE,
|
|
PENDING_TRANSFER,
|
|
PENDING_UPDATE,
|
|
PENDING_DELETE
|
|
]).present?
|
|
end
|
|
|
|
def delete_prohibited?
|
|
(statuses & [
|
|
CLIENT_DELETE_PROHIBITED,
|
|
SERVER_DELETE_PROHIBITED,
|
|
CLIENT_TRANSFER_PROHIBITED,
|
|
SERVER_TRANSFER_PROHIBITED,
|
|
PENDING_CREATE,
|
|
PENDING_TRANSFER,
|
|
PENDING_UPDATE,
|
|
PENDING_DELETE
|
|
]).present?
|
|
end
|
|
|
|
def update_related_whois_records
|
|
# not doing anything if no real changes
|
|
return if changes.slice(*(self.class.column_names - ["updated_at", "created_at", "statuses", "status_notes"])).empty?
|
|
|
|
names = related_domain_descriptions.keys
|
|
UpdateWhoisRecordJob.enqueue(names, :domain) if names.present?
|
|
end
|
|
|
|
def children_log
|
|
log = HashWithIndifferentAccess.new
|
|
log[:legal_documents]= [legal_document_id]
|
|
log
|
|
end
|
|
|
|
def remove_address
|
|
self.class.address_attribute_names.each do |attr_name|
|
|
self[attr_name.to_sym] = nil
|
|
end
|
|
end
|
|
|
|
def reg_no
|
|
return if priv?
|
|
ident
|
|
end
|
|
|
|
def id_code
|
|
return unless priv?
|
|
ident
|
|
end
|
|
end
|