internetee-registry/app/models/contact.rb
2017-03-06 07:27:11 +02:00

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