app/models/domain.rb
class Domain < ApplicationRecord
include UserEvents
include Roids
include Versions # version/domain_version.rb
include Domain::Expirable
include Domain::Activatable
include Domain::ForceDelete
include Domain::Discardable
include Domain::Deletable
include Domain::Transferable
include Domain::RegistryLockable
include Domain::Releasable
include Domain::Disputable
include Domain::BulkUpdatable
PERIODS = [
['3 months', '3m'],
['6 months', '6m'],
['9 months', '9m'],
['1 year', '1y'],
['2 years', '2y'],
['3 years', '3y'],
['4 years', '4y'],
['5 years', '5y'],
['6 years', '6y'],
['7 years', '7y'],
['8 years', '8y'],
['9 years', '9y'],
['10 years', '10y'],
].freeze
attr_accessor :roles,
:legal_document_id,
:is_admin,
:registrant_typeahead,
:update_me,
:epp_pending_update,
:epp_pending_delete,
:reserved_pw
alias_attribute :on_hold_time, :outzone_at
alias_attribute :outzone_time, :outzone_at
alias_attribute :auth_info, :transfer_code # Old attribute name; for PaperTrail
alias_attribute :registered_at, :created_at
store_accessor :json_statuses_history,
:force_delete_domain_statuses_history,
:admin_store_statuses_history
# TODO: whois requests ip whitelist for full info for own domains and partial info for other domains
# TODO: most inputs should be trimmed before validation, probably some global logic?
belongs_to :registrar, required: true
belongs_to :registrant, required: true
# TODO: should we user validates_associated :registrant here?
has_many :admin_domain_contacts
accepts_nested_attributes_for :admin_domain_contacts,
allow_destroy: true, reject_if: :admin_change_prohibited?
has_many :tech_domain_contacts
accepts_nested_attributes_for :tech_domain_contacts,
allow_destroy: true, reject_if: :tech_change_prohibited?
def registrant_change_prohibited?
statuses.include? DomainStatus::SERVER_REGISTRANT_CHANGE_PROHIBITED
end
# NB! contacts, admin_contacts, tech_contacts are empty for a new record
has_many :domain_contacts, dependent: :destroy
has_many :contacts, through: :domain_contacts, source: :contact
has_many :admin_contacts, through: :admin_domain_contacts, source: :contact
has_many :tech_contacts, through: :tech_domain_contacts, source: :contact
has_many :nameservers, dependent: :destroy, inverse_of: :domain
accepts_nested_attributes_for :nameservers, allow_destroy: true,
reject_if: proc { |attrs| attrs[:hostname].blank? }
has_many :transfers, class_name: 'DomainTransfer', dependent: :destroy
has_many :dnskeys, dependent: :destroy
has_one :whois_record # destroyment will be done in after_commit
accepts_nested_attributes_for :dnskeys, allow_destroy: true
has_many :legal_documents, as: :documentable
has_many :registrant_verifications, dependent: :destroy
has_one :csync_record, dependent: :destroy
after_initialize do
self.pending_json = {} if pending_json.blank?
self.statuses = [] if statuses.nil?
self.status_notes = {} if status_notes.nil?
end
before_save :manage_automatic_statuses
before_save :touch_always_version
def touch_always_version
self.updated_at = Time.zone.now
end
before_update :manage_statuses
def manage_statuses
return unless will_save_change_to_registrant_id? # rollback has not yet happened
pending_update! if registrant_verification_asked?
true
end
after_commit :update_whois_record
after_create :update_reserved_domains
def update_reserved_domains
ReservedDomain.new_password_for(name) if in_reserved_list?
end
validates :name_dirty, domain_name: true, uniqueness: true
validates :puny_label, length: { maximum: 63 }
validates :period, presence: true, numericality: { only_integer: true }
validates :transfer_code, presence: true
validate :validate_reservation
def validate_reservation
return if persisted? || !in_reserved_list?
if reserved_pw.blank?
errors.add(:base, :required_parameter_missing_reserved)
return false
end
return if ReservedDomain.pw_for(name) == reserved_pw
errors.add(:base, :invalid_auth_information_reserved)
end
validate :status_is_consistant
def status_is_consistant
has_error = (hold_status? && statuses.include?(DomainStatus::SERVER_MANUAL_INZONE))
if !has_error && statuses.include?(DomainStatus::PENDING_DELETE)
has_error = statuses.include? DomainStatus::SERVER_DELETE_PROHIBITED
end
errors.add(:domains, I18n.t(:object_status_prohibits_operation)) if has_error
end
# Removed to comply new ForceDelete procedure
# at https://github.com/internetee/registry/issues/1428#issuecomment-570561967
#
# validate :check_permissions, :unless => :is_admin
# def check_permissions
# return unless force_delete_scheduled?
# errors.add(:base, I18n.t(:object_status_prohibits_operation))
# false
# end
validates :nameservers, domain_nameserver: {
min: -> { Setting.ns_min_count },
max: -> { Setting.ns_max_count },
}
validates :dnskeys, object_count: {
min: -> { Setting.dnskeys_min_count },
max: -> { Setting.dnskeys_max_count },
}
validates :admin_domain_contacts, object_count: {
min: -> { Setting.admin_contacts_min_count },
max: -> { Setting.admin_contacts_max_count },
}
validates :tech_domain_contacts, object_count: {
min: -> { Setting.tech_contacts_min_count },
max: -> { Setting.tech_contacts_max_count },
}
validates :nameservers, uniqueness_multi: {
attribute: 'hostname',
}
validates :dnskeys, uniqueness_multi: {
attribute: 'public_key',
}
validate :validate_nameserver_ips
validate :statuses_uniqueness
def security_level_resolver
resolver = Dnsruby::Resolver.new(nameserver: Dnskey::RESOLVERS)
resolver.do_validation = true
resolver.do_caching = false
resolver.dnssec = true
resolver
end
def dnssec_security_level(stubber: nil)
Dnsruby::Dnssec.reset
resolver = security_level_resolver
Dnsruby::Recursor.clear_caches(resolver)
if Rails.env.staging?
clear_dnssec_trusted_anchors_and_keys
elsif stubber
Dnsruby::Dnssec.add_trust_anchor(stubber.ds_rr)
end
recursor = Dnsruby::Recursor.new(resolver)
recursor.dnssec = true
recursor.query(name, 'A', 'IN').security_level
end
def clear_dnssec_trusted_anchors_and_keys
Dnsruby::Dnssec.clear_trust_anchors
Dnsruby::Dnssec.clear_trusted_keys
Dnsruby::Dnssec.add_trust_anchor(Dnsruby::RR.create(ENV['trusted_dnskey']))
end
def statuses_uniqueness
return if statuses.uniq == statuses
errors.add(:statuses, :taken)
end
self.ignored_columns = %w[legacy_id legacy_registrar_id legacy_registrant_id]
def subordinate_nameservers
nameservers.select { |x| x.hostname.end_with?(name) }
end
def delegated_nameservers
nameservers.reject { |x| x.hostname.end_with?(name) }
end
def extension_update_prohibited?
statuses.include? DomainStatus::SERVER_EXTENSION_UPDATE_PROHIBITED
end
def dnskey_update_enabled?
statuses.include? DomainStatus::SERVER_OBJ_UPDATE_PROHIBITED
end
def admin_change_prohibited?
statuses.include? DomainStatus::SERVER_ADMIN_CHANGE_PROHIBITED
end
def tech_change_prohibited?
statuses.include? DomainStatus::SERVER_TECH_CHANGE_PROHIBITED
end
class << self
def ransackable_associations(*)
authorizable_ransackable_associations
end
def ransackable_attributes(*)
authorizable_ransackable_attributes
end
def nameserver_required?
Setting.nameserver_required
end
def registrant_user_admin_registrant_domains(registrant_user)
companies = Contact.registrant_user_company_contacts(registrant_user)
from(
"(#{registrant_user_administered_domains(registrant_user).to_sql} UNION " \
"#{registrant_user_company_registrant(companies).to_sql} UNION " \
"#{registrant_user_domains_company(companies, except_tech: true).to_sql}) AS domains"
)
end
def registrant_user_direct_admin_registrant_domains(registrant_user)
from(
"(#{registrant_user_direct_domains_by_registrant(registrant_user).to_sql} UNION " \
"#{registrant_user_direct_domains_by_contact(registrant_user,
except_tech: true).to_sql}) AS domains"
)
end
def registrant_user_domains(registrant_user)
from(
"(#{registrant_user_domains_by_registrant(registrant_user).to_sql} UNION " \
"#{registrant_user_indirect_domains(registrant_user).to_sql} UNION " \
"#{registrant_user_domains_by_contact(registrant_user).to_sql}) AS domains"
)
end
def registrant_user_direct_domains(registrant_user)
from(
"(#{registrant_user_direct_domains_by_registrant(registrant_user).to_sql} UNION " \
"#{registrant_user_direct_domains_by_contact(registrant_user).to_sql}) AS domains"
)
end
def registrant_user_administered_domains(registrant_user)
from(
"(#{registrant_user_domains_by_registrant(registrant_user).to_sql} UNION " \
"#{registrant_user_domains_by_admin_contact(registrant_user).to_sql}) AS domains"
)
end
def registrant_user_indirect_domains(registrant_user)
companies = Contact.registrant_user_company_contacts(registrant_user)
from(
"(#{registrant_user_company_registrant(companies).to_sql} UNION "\
"#{registrant_user_domains_company(companies).to_sql}) AS domains"
)
end
private
def registrant_user_domains_by_registrant(registrant_user)
where(registrant: registrant_user.contacts)
end
def registrant_user_domains_by_contact(registrant_user)
joins(:domain_contacts).where(domain_contacts: { contact_id: registrant_user.contacts })
end
def registrant_user_domains_by_admin_contact(registrant_user)
joins(:domain_contacts).where(domain_contacts: { contact_id: registrant_user.contacts,
type: [AdminDomainContact.name] })
end
def registrant_user_direct_domains_by_registrant(registrant_user)
where(registrant: registrant_user.direct_contacts)
end
def registrant_user_direct_domains_by_contact(registrant_user, except_tech: false)
request = { contact_id: registrant_user.direct_contacts }
request[:type] = [AdminDomainContact.name] if except_tech
joins(:domain_contacts).where(domain_contacts: request)
end
def registrant_user_company_registrant(companies)
where(registrant: companies)
end
def registrant_user_domains_company(companies, except_tech: false)
request = { contact: companies }
request[:type] = [AdminDomainContact.name] if except_tech
joins(:domain_contacts).where(domain_contacts: request)
end
end
def name=(value)
value&.strip!
value&.downcase!
self[:name] = SimpleIDN.to_unicode(value)
self[:name_puny] = SimpleIDN.to_ascii(value)
self[:name_dirty] = value
end
# find by internationalized domain name
# internet domain name => ascii or puny, but db::domains.name is unicode
def self.find_by_idn(name)
domain = find_by(name: name)
if domain.blank? && name.include?('-')
unicode = SimpleIDN.to_unicode name # we have no index on domains.name_puny
domain = find_by(name: unicode)
end
domain
end
def puny_label
name_puny.to_s.split('.').first
end
def registrant_typeahead
@registrant_typeahead || registrant.try(:name) || nil
end
def in_reserved_list?
@in_reserved_list ||= ReservedDomain.by_domain(name).any?
end
def pending_transfer
transfers.find_by(status: DomainTransfer::PENDING)
end
def server_holdable?
return false if statuses.include?(DomainStatus::SERVER_HOLD)
return false if statuses.include?(DomainStatus::SERVER_MANUAL_INZONE)
true
end
def renewable?
return false unless renew_blocking_statuses.empty?
return true unless Setting.days_to_renew_domain_before_expire != 0
# if you can renew domain at days_to_renew before domain expiration
return false if (expire_time.to_date - Time.zone.today) + 1 > Setting.days_to_renew_domain_before_expire
true
end
def renew_blocking_statuses
disallowed = [DomainStatus::DELETE_CANDIDATE, DomainStatus::PENDING_RENEW,
DomainStatus::PENDING_TRANSFER, DomainStatus::CLIENT_RENEW_PROHIBITED,
DomainStatus::PENDING_UPDATE, DomainStatus::SERVER_RENEW_PROHIBITED,
DomainStatus::PENDING_DELETE_CONFIRMATION]
(statuses & disallowed)
end
def notify_registrar(message_key)
# TODO: To be deleted with DomainDeleteConfirm refactoring
registrar.notifications.create!(
text: "#{I18n.t(message_key)}: #{name}",
attached_obj_id: id,
attached_obj_type: self.class.to_s
)
end
def preclean_pendings
# TODO: To be deleted with refactoring
self.registrant_verification_token = nil
self.registrant_verification_asked_at = nil
end
def clean_pendings!
# TODO: To be deleted with refactoring
preclean_pendings
self.pending_json = {}
statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION)
statuses.delete(DomainStatus::PENDING_UPDATE)
statuses.delete(DomainStatus::PENDING_DELETE)
status_notes[DomainStatus::PENDING_UPDATE] = ''
status_notes[DomainStatus::PENDING_DELETE] = ''
save
end
def pending_update!
return true if pending_update?
self.epp_pending_update = true # for epp
return true unless registrant_verification_asked?
pending_json_cache = pending_json
token = registrant_verification_token
asked_at = registrant_verification_asked_at
new_registrant_id = registrant.id
new_registrant_email = registrant.email
new_registrant_name = registrant.name
send_time = Time.zone.now + 1.minute
RegistrantChangeConfirmEmailJob.set(wait_until: send_time).perform_later(id, new_registrant_id)
RegistrantChangeNoticeEmailJob.set(wait_until: send_time).perform_later(id, new_registrant_id)
reload
self.pending_json = pending_json_cache
self.registrant_verification_token = token
self.registrant_verification_asked_at = asked_at
set_pending_update
touch_always_version
pending_json['new_registrant_id'] = new_registrant_id
pending_json['new_registrant_email'] = new_registrant_email
pending_json['new_registrant_name'] = new_registrant_name
# This pending_update! method is triggered by before_update
# Note, all before_save callbacks are executed before before_update,
# thus automatic statuses has already executed by this point
# and we need to trigger automatic statuses manually (second time).
manage_automatic_statuses
end
def registrant_update_confirmable?(token)
return false if statuses.include? DomainStatus::DELETE_CANDIDATE
return false unless pending_update?
return false unless registrant_verification_asked?
return false unless registrant_verification_token == token
true
end
def registrant_delete_confirmable?(token)
return false unless pending_delete?
return false unless registrant_verification_asked?
return false unless registrant_verification_token == token
true
end
def registrant_verification_asked?
registrant_verification_asked_at.present? && registrant_verification_token.present?
end
def registrant_verification_asked!(frame_str, current_user_id)
pending_json['frame'] = frame_str
pending_json['current_user_id'] = current_user_id
self.registrant_verification_asked_at = Time.zone.now
self.registrant_verification_token = SecureRandom.hex(42)
end
def pending_delete!
return true if pending_delete?
self.epp_pending_delete = true # for epp
# TODO: if this were to ever return true, that would be wrong. EPP would report sucess pending
return true unless registrant_verification_asked?
pending_delete_confirmation!
save(validate: false) # should check if this did succeed
Domains::DeleteConfirmEmail::SendRequest.run(domain: self)
end
def cancel_pending_delete
statuses.delete DomainStatus::PENDING_DELETE_CONFIRMATION
statuses.delete DomainStatus::PENDING_DELETE
self.delete_date = nil
end
def pricelist(operation_category, period_i = nil, unit = nil)
period_i ||= period
unit ||= period_unit
zone_name = name.split('.').drop(1).join('.')
zone = DNS::Zone.find_by(origin: zone_name)
duration = "P#{period_i}#{unit.upcase}"
Billing::Price.price_for(zone, operation_category, duration)
end
### VALIDATIONS ###
def validate_nameserver_ips
nameservers.to_a.reject(&:marked_for_destruction?).each do |ns|
next unless ns.hostname.end_with?(".#{name}")
next if ns.ipv4.present? || ns.ipv6.present?
errors.add(:nameservers, :invalid) if errors[:nameservers].blank?
ns.errors.add(:ipv4, :blank)
end
end
# used for highlighting form tabs
def parent_valid?
assoc_errors = errors.keys.select { |x| x.match(/\./) }
(errors.keys - assoc_errors).empty?
end
## SHARED
def name_in_wire_format
res = ''
parts = name_puny.split('.')
parts.each do |x|
res += format('%02X', x.length) # length of label in hex
res += x.each_byte.map { |b| format('%02X', b) }.join # label
end
res += '00'
res
end
def to_s
name
end
def pending_registrant
return '' if pending_json.blank?
return '' if pending_json['new_registrant_id'].blank?
Registrant.find_by(id: pending_json['new_registrant_id'])
end
def pending_update?
statuses.include?(DomainStatus::PENDING_UPDATE)
end
# depricated not used, not valid
def update_prohibited?
(statuses & DomainStatus::UPDATE_PROHIBIT_STATES).present?
end
# public api
def delete_prohibited?
statuses.include?(DomainStatus::FORCE_DELETE)
end
def update_unless_locked_by_registrant(update)
update(admin_store_statuses_history: update) unless locked_by_registrant?
end
def update_not_by_locked_statuses(update)
return unless locked_by_registrant?
result = update.reject { |status| RegistryLockable::LOCK_STATUSES.include? status }
update(admin_store_statuses_history: result)
end
# special handling for admin changing status
def admin_status_update(update)
return unless update
PaperTrail.request(enabled: false) do
update_unless_locked_by_registrant(update)
update_not_by_locked_statuses(update)
end
# check for deleted status
statuses.each do |s|
next if update.include? s
case s
when DomainStatus::PENDING_DELETE
self.delete_date = nil
when DomainStatus::SERVER_MANUAL_INZONE # removal causes server hold to set
self.outzone_at = Time.zone.now if force_delete_scheduled?
when DomainStatus::EXPIRED # removal causes server hold to set
self.outzone_at = expire_time + 15.day
when DomainStatus::SERVER_HOLD # removal causes server hold to set
self.outzone_at = nil
end
end
end
def pending_update_prohibited?
(statuses_was & DomainStatus::UPDATE_PROHIBIT_STATES).present?
end
def set_pending_update
if pending_update_prohibited?
logger.info "DOMAIN STATUS UPDATE ISSUE ##{id}: PENDING_UPDATE not allowed to set. [#{statuses}]"
return
end
statuses << DomainStatus::PENDING_UPDATE
end
def pending_delete?
(statuses & [DomainStatus::PENDING_DELETE_CONFIRMATION, DomainStatus::PENDING_DELETE]).any?
end
def pending_delete_confirmation?
statuses.include? DomainStatus::PENDING_DELETE_CONFIRMATION
end
def pending_delete_confirmation!
statuses << DomainStatus::PENDING_DELETE_CONFIRMATION unless pending_delete_prohibited?
end
def pending_delete_prohibited?
(statuses_was & DomainStatus::DELETE_PROHIBIT_STATES).present?
end
# let's use positive method names
def pending_deletable?
!pending_delete_prohibited?
end
def set_pending_delete
if pending_delete_prohibited?
logger.info "DOMAIN STATUS UPDATE ISSUE ##{id}: PENDING_DELETE not allowed to set. [#{statuses}]"
return
end
statuses << DomainStatus::PENDING_DELETE
end
def set_server_hold
statuses << DomainStatus::SERVER_HOLD
self.outzone_at = Time.current
end
def manage_automatic_statuses
unless self.class.nameserver_required?
deactivate if nameservers.reject(&:marked_for_destruction?).empty?
activate if nameservers.reject(&:marked_for_destruction?).size >= Setting.ns_min_count
end
cancel_force_delete if force_delete_scheduled? && will_save_change_to_registrant_id?
if statuses.empty? && valid?
statuses << DomainStatus::OK
elsif (statuses.length > 1) || !valid?
statuses.delete(DomainStatus::OK)
end
p_d = statuses.include?(DomainStatus::PENDING_DELETE)
s_h = (statuses & [DomainStatus::SERVER_MANUAL_INZONE, DomainStatus::SERVER_HOLD]).empty?
statuses << DomainStatus::SERVER_HOLD if p_d && s_h
end
def children_log
log = HashWithIndifferentAccess.new
log[:admin_contacts] = admin_contact_ids
log[:tech_contacts] = tech_contact_ids
log[:nameservers] = nameserver_ids
log[:dnskeys] = dnskey_ids
log[:legal_documents] = [legal_document_id]
log[:registrant] = [registrant_id]
log
end
def update_whois_record
UpdateWhoisRecordJob.set(wait: 1.minute).perform_later name, 'domain'
end
def status_notes_array=(notes)
self.status_notes = {}
notes ||= []
statuses.each_with_index do |status, i|
status_notes[status] = notes[i]
end
end
def primary_contact_emails
(admin_contacts.emails + [registrant.email]).uniq
end
def expired_domain_contact_emails
(primary_contact_emails +
["info@#{name}", "#{prepared_domain_name}@#{name}"]).uniq
end
def all_related_emails
(admin_contacts.emails + tech_contacts.emails + [registrant.email]).uniq
end
def force_delete_contact_emails
(primary_contact_emails + tech_contacts.pluck(:email) +
["info@#{name}", "#{prepared_domain_name}@#{name}"]).uniq
end
def prepared_domain_name
name.split('.')&.first
end
def new_registrant_email
pending_json['new_registrant_email']
end
def new_registrant_id
pending_json['new_registrant_id']
end
def as_json(_options)
hash = super
hash['auth_info'] = hash.delete('transfer_code') # API v1 requirement
hash['valid_from'] = hash['created_at'] # API v1 requirement
hash
end
def domain_name
DNS::DomainName.new(name)
end
def contact_emails_verification_failed
contacts.select(&:email_verification_failed?)&.map(&:email)&.uniq
end
def as_csv_row
[
name,
registrant_info[0],
registrant_info[1],
registrant_info[2],
registrant_info[3],
valid_to.to_formatted_s(:db),
registrar,
created_at.to_formatted_s(:db),
statuses,
admin_contacts.map { |c| "#{c.name}, #{c.code}, #{ApplicationController.helpers.ident_for(c)}" },
tech_contacts.map { |c| "#{c.name}, #{c.code}, #{ApplicationController.helpers.ident_for(c)}" },
nameservers.pluck(:hostname),
force_delete_date,
force_delete_data,
]
end
def as_pdf
domain_html = ApplicationController.render(template: 'domain/pdf', assigns: { domain: self })
generator = PDFKit.new(domain_html, { enable_local_file_access: true })
generator.to_pdf
end
def registrant_info
if registrant
return [registrant.name, registrant.ident, registrant.ident_country_code,
registrant.ident_type]
end
ver = Version::ContactVersion.where(item_id: registrant_id).last
contact = Contact.all_versions_for([registrant_id], created_at).first
contact = ObjectVersionsParser.new(ver).parse if contact.nil? && ver
[contact.try(:name), contact.try(:ident), contact.try(:ident_country_code),
contact.try(:ident_type)] || ['Deleted']
end
def registrant_ident_info
return ApplicationController.helpers.ident_for(registrant) if registrant
end
def self.csv_header
[
'Domain', 'Registrant name', 'Registrant ident', 'Registrant ident country code',
'Registrant ident type', 'Valid to', 'Registrar', 'Created at',
'Statuses', 'Admin. contacts', 'Tech. contacts', 'Nameservers', 'Force delete date',
'Force delete data'
]
end
def self.pdf(html)
kit = PDFKit.new(html)
kit.to_pdf
end
def self.expire_warning_period
Setting.expire_warning_period.days
end
def self.redemption_grace_period
Setting.redemption_grace_period.days
end
def self.outzone_candidates
where("#{attribute_alias(:outzone_time)} < ?", Time.zone.now)
end
def self.uses_zone?(zone)
exists?(['name ILIKE ?', "%.#{zone.origin}"])
end
def self.swap_elements(array, indexes)
indexes.each do |index|
array[index[0]], array[index[1]] = array[index[1]], array[index[0]]
end
array
end
end