diff --git a/Gemfile b/Gemfile index b7c03de..51a5dfe 100644 --- a/Gemfile +++ b/Gemfile @@ -18,17 +18,20 @@ gem 'cancan', '~> 1.6.5' #gem "meta_where", "~> 1.0" # squeel ? gem 'sentient_user', '~> 0.3.2' gem 'active_scaffold', '~> 3.1.0', :git => 'https://github.com/activescaffold/active_scaffold.git' -gem 'web-app-theme', :git => "git://github.com/tscolari/web-app-theme.git", :branch => "v3.1.0" -gem "pjax_rails", "~> 0.1.10" -gem "rails_config", "~> 0.2.4" -# gem "rails-settings-cached", :require => "rails-settings" +gem 'web-app-theme', :git => 'git://github.com/tscolari/web-app-theme.git', :branch => 'v3.1.0' +gem 'pjax_rails', '~> 0.1.10' +gem 'validates_hostname', '~> 1.0.0', :git => 'https://github.com/KimNorgaard/validates_hostname.git' +gem 'nilify_blanks', '~> 1.0.0' +gem 'rails_config', '~> 0.2.4' +# gem 'rails-settings-cached', :require => 'rails-settings' +gem 'capistrano', '~> 2.9.0' # Gems used only for assets and not required # in production environments by default. group :assets do - gem 'sass-rails', " ~> 3.1.0" - gem "compass", "~> 0.12.alpha.0" - gem 'coffee-rails', "~> 3.1.0" + gem 'sass-rails', '~> 3.1.0' + gem 'compass', '~> 0.12.alpha.0' + gem 'coffee-rails', '~> 3.1.0' gem 'uglifier' gem 'therubyracer' end @@ -36,12 +39,9 @@ end gem 'jquery-rails' # gem 'foreigner' ? -group :test, :development do - gem "rspec-rails", "~> 2.6.1" - # gem 'capybara', '~> 1.1.1' -end - +gem 'rspec-rails', '~> 2.6.1', :group => [:test, :development] group :test do - gem 'factory_girl_rails','~> 1.2' + gem 'factory_girl_rails', '~> 1.2' + gem 'capybara', '~> 1.1.1' gem 'spork', '~> 0.9.0.rc' end diff --git a/Gemfile.lock b/Gemfile.lock index da7c3e1..97754fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,9 +6,15 @@ GIT web-app-theme (3.1.0) thor (~> 0.14) +GIT + remote: https://github.com/KimNorgaard/validates_hostname.git + revision: 6421e9bd8261a2fe010c627191a0dbe30352469f + specs: + validates_hostname (1.0.0) + GIT remote: https://github.com/activescaffold/active_scaffold.git - revision: 5c4ae3b25238eaed5f3313ab2ac0dea395b4c8cb + revision: 5391f1f4ab8a71ca4c010107e970266bff5a09b1 specs: active_scaffold (3.1.2) rails (~> 3.1.0) @@ -49,7 +55,22 @@ GEM bcrypt-ruby (3.0.1) builder (3.0.0) cancan (1.6.5) - chunky_png (1.2.4) + capistrano (2.9.0) + highline + net-scp (>= 1.0.0) + net-sftp (>= 2.0.0) + net-ssh (>= 2.0.14) + net-ssh-gateway (>= 1.1.0) + capybara (1.1.1) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + selenium-webdriver (~> 2.0) + xpath (~> 0.1.4) + childprocess (0.2.2) + ffi (~> 1.0.6) + chunky_png (1.2.5) coffee-rails (3.1.1) coffee-script (>= 2.2.0) railties (~> 3.1.0) @@ -61,24 +82,28 @@ GEM chunky_png (~> 1.2) fssm (>= 0.2.7) sass (~> 3.1) - devise (1.4.6) + devise (1.4.7) bcrypt-ruby (~> 3.0) orm_adapter (~> 0.0.3) warden (~> 1.0.3) diff-lcs (1.1.3) erubis (2.7.0) - execjs (1.2.8) + execjs (1.2.9) multi_json (~> 1.0) - factory_girl (2.1.0) + factory_girl (2.1.2) + activesupport factory_girl_rails (1.2.0) factory_girl (~> 2.1.0) railties (>= 3.0.0) + ffi (1.0.9) fssm (0.2.7) + highline (1.6.2) hike (1.2.1) i18n (0.6.0) jquery-rails (1.0.14) railties (~> 3.0) thor (~> 0.14) + json_pure (1.6.1) libv8 (3.3.10.2) mail (2.3.0) i18n (>= 0.4.0) @@ -86,6 +111,17 @@ GEM treetop (~> 1.4.8) mime-types (1.16) multi_json (1.0.3) + net-scp (1.0.4) + net-ssh (>= 1.99.1) + net-sftp (2.0.5) + net-ssh (>= 2.0.9) + net-ssh (2.2.1) + net-ssh-gateway (1.1.0) + net-ssh (>= 1.99.1) + nilify_blanks (1.0.0) + activerecord (>= 3.0.0) + activesupport (>= 3.0.0) + nokogiri (1.5.0) orm_adapter (0.0.5) pg (0.11.0) pjax_rails (0.1.10) @@ -132,6 +168,7 @@ GEM activesupport (~> 3.0) railties (~> 3.0) rspec (~> 2.6.0) + rubyzip (0.9.4) sass (3.1.7) sass-rails (3.1.2) actionpack (~> 3.1.0) @@ -139,6 +176,11 @@ GEM sass (>= 3.1.4) sprockets (~> 2.0.0) tilt (~> 1.3.2) + selenium-webdriver (2.7.0) + childprocess (>= 0.2.1) + ffi (>= 1.0.7) + json_pure + rubyzip sentient_user (0.3.2) spork (0.9.0.rc9) sprockets (2.0.0) @@ -159,6 +201,8 @@ GEM multi_json (>= 1.0.2) warden (1.0.5) rack (>= 1.0) + xpath (0.1.4) + nokogiri (~> 1.3) PLATFORMS ruby @@ -166,11 +210,14 @@ PLATFORMS DEPENDENCIES active_scaffold (~> 3.1.0)! cancan (~> 1.6.5) + capistrano (~> 2.9.0) + capybara (~> 1.1.1) coffee-rails (~> 3.1.0) compass (~> 0.12.alpha.0) devise (~> 1.4.5) factory_girl_rails (~> 1.2) jquery-rails + nilify_blanks (~> 1.0.0) pg pjax_rails (~> 0.1.10) rails (= 3.1.0) @@ -182,4 +229,5 @@ DEPENDENCIES sqlite3 therubyracer uglifier + validates_hostname (~> 1.0.0)! web-app-theme! diff --git a/app/assets/images/marketing/arrow_left.png b/app/assets/images/marketing/arrow_left.png new file mode 100644 index 0000000..e420c19 Binary files /dev/null and b/app/assets/images/marketing/arrow_left.png differ diff --git a/app/assets/stylesheets/marketing.css.scss b/app/assets/stylesheets/marketing.css.scss index d42b2e4..40da202 100644 --- a/app/assets/stylesheets/marketing.css.scss +++ b/app/assets/stylesheets/marketing.css.scss @@ -1,32 +1,4 @@ -@import "compass/css3/gradient"; -@import "compass/css3/border-radius"; -@import "compass/css3/box-shadow"; - -@mixin slick-black { - @include background( - image-url("marketing/action/slick-black.png"), - linear-gradient(top, rgba(50, 50, 50, 0.9) 0%, rgba(30, 30, 30, 0.9) 50%, rgba(20, 20, 20, 0.9) 50%, rgba(0, 0, 0, 0.9) 100%)); - border: 0; - @include border-radius(4px); - @include box-shadow(inset 1px 1px 1px 0px rgba(135, 135, 135, 0.1), inset -1px -1px 1px 0px rgba(135, 135, 135, 0.1)); - color: #fff; - line-height: 1; - padding: 8px 0; - text-shadow: 0px -1px 1px rgba(0, 0, 0, .8), 0 1px 1px rgba(255, 255, 255, 0.3); - - &:hover { - @include background( - image-url("marketing/action/slick-black-hover.png"), - linear-gradient(top, rgba(70, 70, 70, 0.9) 0%, rgba(50, 50, 50, 0.9) 50%, rgba(40, 40, 40, 0.9) 50%, rgba(20, 20, 20, 0.9) 100%)); - cursor: pointer; - } - - &:active { - @include background( - image-url("marketing/action/slick-black-hover.png"), - linear-gradient(top, rgba(30, 30, 30, 0.9) 0%, rgba(20, 20, 20, 0.9) 50%, rgba(10, 10, 10, 0.9) 50%, rgba(0, 0, 0, 0.9) 100%)); - } -} +@import "ui/buttons"; .marketing .header-inner { width: 77%; @@ -73,12 +45,18 @@ .action { padding: 0 20px 20px 20px; + a { + display: block; + width: 350px; + margin: auto; + } button { - @include slick-black; + // @include slick-black; + @include cupid-green; + width: 300px; height: 60px; margin: auto; - display: block; font-family: "MuseoSans500", helvetica, arial, sans-serif; strong { font-size: 18px; diff --git a/app/assets/stylesheets/normalize.css b/app/assets/stylesheets/normalize.css index a7a472a..02e3d11 100644 --- a/app/assets/stylesheets/normalize.css +++ b/app/assets/stylesheets/normalize.css @@ -296,7 +296,7 @@ button, input, select, textarea { - -webkit-appearance: none; + /* -webkit-appearance: none; wtf this hides content */ border-radius: 0; vertical-align: baseline; *vertical-align: middle; diff --git a/app/assets/stylesheets/ui/buttons.css.scss b/app/assets/stylesheets/ui/buttons.css.scss new file mode 100644 index 0000000..ed53e2f --- /dev/null +++ b/app/assets/stylesheets/ui/buttons.css.scss @@ -0,0 +1,36 @@ +@import "compass/css3/gradient"; +@import "compass/css3/border-radius"; +@import "compass/css3/box-shadow"; + +@mixin cupid-green { + @include background( + image-url("marketing/action/slick-black.png"), + linear-gradient(top, #7fbf4d, #63a62f) + ); + border: 1px solid #63a62f; + border-bottom: 1px solid #5b992b; + @include border-radius(3px); + @include box-shadow(inset 0 1px 0 0 #96ca6d); + color: #fff; + font: bold 11px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; + line-height: 1; + padding: 7px 0 8px 0; + text-align: center; + text-shadow: 0 -1px 0 #4c9021; + width: 150px; + + &:hover { + @include background( + image-url("marketing/action/slick-black.png"), + linear-gradient(top, #76b347, #5e9e2e) + ); + @include box-shadow(inset 0 1px 0 0 #8dbf67); + cursor: pointer; + } + + &:active { + border: 1px solid #5b992b; + border-bottom: 1px solid #538c27; + @include box-shadow(inset 0 0 8px 4px #548c29, 0 1px 0 0 #eee); + } +} diff --git a/app/controllers/domains_controller.rb b/app/controllers/domains_controller.rb index b0a8b12..0576ba2 100644 --- a/app/controllers/domains_controller.rb +++ b/app/controllers/domains_controller.rb @@ -1,22 +1,38 @@ class DomainsController < ApplicationController - begin active_scaffold :domain do |conf| - conf.columns = [:name, :records] - conf.create.columns = [:name] - conf.update.columns = [:name] + conf.columns = [:name, :soa_record, :ns_records, :records] + conf.list.columns = [:name, :soa_record, :ns_records, :records] + conf.create.columns = [:name, :soa_record, :ns_records] + conf.update.columns = [:name, :soa_record, :ns_records] conf.actions.exclude :show conf.list.sorting = { :name => :asc } - end - rescue => e - puts e.backtrace - raise e + + conf.columns[:records].label = 'All Records' end protected - + def before_create_save(record) record.type = 'NATIVE' end + # TODO: move to core + def do_edit_associated + @parent_record = params[:id].nil? ? new_model : find_if_allowed(params[:id], :update) + @column = active_scaffold_config.columns[params[:association]] + + # NOTE: we don't check whether the user is allowed to update this record, because if not, we'll still let them associate the record. we'll just refuse to do more than associate, is all. + @record = @column.association.klass.find(params[:associated_id]) if params[:associated_id] + @record ||= @column.singular_association? ? @parent_record.send(:"build_#{@column.name}") : @parent_record.send(@column.name).build + reflection = @parent_record.class.reflect_on_association(@column.name) + if reflection && reflection.reverse + reverse_macro = @record.class.reflect_on_association(reflection.reverse).macro + @record.send(:"#{reflection.reverse}=", @parent_record) if [:has_one, :belongs_to].include?(reverse_macro) + end + + @scope = "[#{@column.name}]" + @scope += (@record.new_record?) ? "[#{(Time.now.to_f*1000).to_i.to_s}]" : "[#{@record.id}]" if @column.plural_association? + end + end diff --git a/app/controllers/ns_controller.rb b/app/controllers/ns_controller.rb new file mode 100644 index 0000000..69ccb52 --- /dev/null +++ b/app/controllers/ns_controller.rb @@ -0,0 +1,12 @@ +class NsController < ApplicationController + active_scaffold :ns do |conf| + conf.columns = [:name, :content, :ttl] + conf.columns[:content].label = 'Hostname' + end + + protected + + def beginning_of_chain + super.readonly(false) + end +end diff --git a/app/controllers/records_controller.rb b/app/controllers/records_controller.rb index 8aa611a..c15e829 100644 --- a/app/controllers/records_controller.rb +++ b/app/controllers/records_controller.rb @@ -1,5 +1,12 @@ class RecordsController < ApplicationController active_scaffold :record do |conf| + conf.sti_children = [:SOA, :NS] conf.columns = [:name, :type, :content, :ttl, :prio, :change_date] end + + protected + + def beginning_of_chain + super.readonly(false) + end end diff --git a/app/controllers/soas_controller.rb b/app/controllers/soas_controller.rb new file mode 100644 index 0000000..87f24b0 --- /dev/null +++ b/app/controllers/soas_controller.rb @@ -0,0 +1,11 @@ +class SoasController < ApplicationController + active_scaffold :soa do |conf| + conf.columns = [:name, :primary_ns, :contact, :ttl] + end + + protected + + def beginning_of_chain + super.readonly(false) + end +end diff --git a/app/helpers/ns_helper.rb b/app/helpers/ns_helper.rb new file mode 100644 index 0000000..091f627 --- /dev/null +++ b/app/helpers/ns_helper.rb @@ -0,0 +1,2 @@ +module NsHelper +end \ No newline at end of file diff --git a/app/helpers/soas_helper.rb b/app/helpers/soas_helper.rb new file mode 100644 index 0000000..cc71c24 --- /dev/null +++ b/app/helpers/soas_helper.rb @@ -0,0 +1,2 @@ +module SoasHelper +end \ No newline at end of file diff --git a/app/models/a.rb b/app/models/a.rb new file mode 100644 index 0000000..3863116 --- /dev/null +++ b/app/models/a.rb @@ -0,0 +1,17 @@ +# See #A + +# = IPv4 Address Record (A) +# +# Defined in RFC 1035. Forward maps a host name to IPv4 address. The only +# parameter is an IP address in dotted decimal format. The IP address in not +# terminated with a '.' (dot). Valid host name format (a.k.a 'label' in DNS +# jargon). If host name is BLANK (or space) then the last valid name (or label) +# is substituted. +# +# Obtained from http://www.zytrax.com/books/dns/ch8/a.html +# +class A < Record + # Only accept valid IPv4 addresses + validates :content, :presence => true, :ip => true + +end diff --git a/app/models/cname.rb b/app/models/cname.rb new file mode 100644 index 0000000..4fa4f98 --- /dev/null +++ b/app/models/cname.rb @@ -0,0 +1,13 @@ +# See #CNAME + +# = Canonical Name Record (CNAME) +# +# A CNAME record maps an alias or nickname to the real or Canonical name which +# may lie outside the current zone. Canonical means expected or real name. +# +# Obtained from http://www.zytrax.com/books/dns/ch8/cname.html +# +class CNAME < Record + validates :content, :presence => true, :hostname => true + +end diff --git a/app/models/domain.rb b/app/models/domain.rb index 03033c8..a21c3da 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -1,4 +1,33 @@ class Domain < ActiveRecord::Base - set_inheritance_column { 'sti_type' } - has_many :records, :dependent => :destroy # :delete_all ? + set_inheritance_column "sti_disabled" + nilify_blanks + + belongs_to :user + has_many :records, :dependent => :destroy + + cattr_reader :types + @@types = ['NATIVE', 'MASTER', 'SLAVE', 'SUPERSLAVE'] + + has_one :soa_record, :class_name => 'SOA', :conditions => {:type => 'SOA'} + + # Handle relations and validations for all resource records on this zone + validates_associated :records + for type in Record.types + has_many :"#{type.downcase}_records", :class_name => type, :conditions => {:type => type} + validates_associated :"#{type.downcase}_records" + end + + validates :name, :presence => true, :uniqueness => true, :domainname => {:require_valid_tld => false} + validates :master, :presence => true, :if => :slave? + validates :master, :ip => true, :if => :slave?, :allow_nil => true + validates :type, :inclusion => { :in => @@types, :message => "Unknown domain type" } + validates :soa_record, :presence => {:unless => :slave?} + validates_associated :soa_record, :allow_nil => true + validates :ns_records, :presence => true, :length => {:minimum => 2, :message => "must have be at least 2"} + MASTER_FORMAT = /\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\z/ + validate :master, :presence => {:if => :slave?}, + :format => {:with => MASTER_FORMAT, :allow_blank => true, :if => :slave?} + + # Are we a slave domain + def slave?; self.type == 'SLAVE' end end diff --git a/app/models/mx.rb b/app/models/mx.rb new file mode 100644 index 0000000..611f562 --- /dev/null +++ b/app/models/mx.rb @@ -0,0 +1,15 @@ +# See #MX + +# = Mail Exchange Record (MX) +# Defined in RFC 1035. Specifies the name and relative preference of mail +# servers (mail exchangers in the DNS jargon) for the zone. +# +# Obtained from http://www.zytrax.com/books/dns/ch8/mx.html +# +class MX < Record + validates :prio, + :numericality => {:greater_than_or_equal_to => 0, :less_than_or_equal_to => 65535, :only_integer => true} + validates :content, :presence => true, :hostname => true + + def supports_prio?; true end +end diff --git a/app/models/ns.rb b/app/models/ns.rb new file mode 100644 index 0000000..6cc684c --- /dev/null +++ b/app/models/ns.rb @@ -0,0 +1,27 @@ +# See #NS + +# = Name Server Record (NS) +# +# Defined in RFC 1035. NS RRs appear in two places. Within the zone file, in +# which case they are authoritative records for the zone's name servers. At the +# point of delegation for either a subdomain of the zone or in the zone's +# parent. Thus the zone example.com's parent zone (.com) will contain +# non-authoritative NS RRs for the zone example.com at its point of delegation +# and subdomain.example.com will have non-authoritative NS RSS in the zone +# example.com at its point of delegation. NS RRs at the point of delegation are +# never authoritative only NS RRs for the zone are regarded as authoritative. +# While this may look a fairly trivial point, is has important implications for +# DNSSEC. +# +# NS RRs are required because DNS queries respond with an authority section +# listing all the authoritative name servers, for sub-domains or queries to the +# zones parent where they are required to allow referral to take place. +# +# Obtained from http://www.zytrax.com/books/dns/ch8/ns.html +# +class NS < Record + validates :content, :presence => true, :hostname => true + +end + +Ns = NS diff --git a/app/models/record.rb b/app/models/record.rb index a8cbc98..97071cb 100644 --- a/app/models/record.rb +++ b/app/models/record.rb @@ -1,4 +1,41 @@ class Record < ActiveRecord::Base - set_inheritance_column { 'sti_type' } belongs_to :domain + + cattr_reader :types + @@types = ['SOA', 'NS', 'A', 'MX', 'TXT', 'CNAME'] + + validates :domain_id, :name, :presence => true + validates :type, :inclusion => {:in => @@types, :message => "Unknown record type"} + # RFC 2181, 8 + validates :ttl, :numericality => { :greater_than_or_equal_to => 0, :less_than => 2**31 }, :allow_blank => true + + before_validation :prepare_name! + before_save :update_change_date + after_save :update_soa_serial + + # By default records don't support priorities. + # Those who do can overwrite this in their own classes. + def supports_priority?; false end + + def shortname; name.gsub(/\.?#{self.domain.name}$/, '') end + def shortname=(value); self.name = value end + + protected + + def prepare_name! + return if domain.nil? or domain.name.blank? + self.name = domain.name if name.blank? or name == '@' + self.name << ".#{domain.name}" unless name.index(domain.name) + end + + # Update the change date for automatic serial number generation + def update_change_date; self.change_date = Time.now.to_i end + + def update_soa_serial #:nodoc: + unless type == 'SOA' || @serial_updated || domain.slave? + domain.soa_record.update_serial! + @serial_updated = true + end + end + end diff --git a/app/models/soa.rb b/app/models/soa.rb new file mode 100644 index 0000000..d597862 --- /dev/null +++ b/app/models/soa.rb @@ -0,0 +1,72 @@ +# See #SOA + +# = Start of Authority Record +# Defined in RFC 1035. The SOA defines global parameters for the zone (domain). +# There is only one SOA record allowed in a zone file. +# +# Obtained from http://www.zytrax.com/books/dns/ch8/soa.html +# +class SOA < Record + validates :domain_id, :uniqueness => true # one SOA per domain + validates :name, :presence => true, :hostname => true + validates :content, :presence => true + validates :primary_ns, :presence => true + CONTACT_FORMAT = /\A[a-zA-Z0-9\-\.]+@[a-zA-Z0-9-]+\.[a-zA-Z.]{2,6}\z/ + validates :contact, :format => {:with => CONTACT_FORMAT} + validates :serial, :presence => true, :numericality => {:allow_blank => true, :greater_than_or_equal_to => 0} + + before_validation :assemble_content + before_update :update_serial + after_initialize :disassemble_content + + # This allows us to have these convenience attributes act like any other + # column in terms of validations + attr_accessor :primary_ns, :contact, :serial + + # Treat contact specially + # replacing the first period with an @ if no @'s are present + def contact=(email) + email.sub!('.', '@') if email.present? && email.index('@').nil? + @contact = email + end + + # Hook into #reload + def reload_with_content + reload_without_content + disassemble_content + end + alias_method_chain :reload, :content + + # Updates the serial number to the next logical one. Format of the generated + # serial is YYYYMMDDNN, where NN is the number of the change for the day. + # 01 for the first change, 02 the seconds, etc... + # + # If the serial number is 0, we opt for PowerDNS's automatic serial number + # generation, that gets triggered by updating the change_date + def update_serial + return if self.content_changed? + date_serial = Time.now.strftime( "%Y%m%d00" ).to_i + @serial = (@serial.nil? || date_serial > @serial) ? date_serial : @serial + 1 + end + + # Same as #update_serial and saves the record + def update_serial! + update_serial + save + end + + private + + def assemble_content + self.content = "#{@primary_ns} #{@contact} #{@serial}".strip + end + + # Update our convenience accessors when the object has changed + def disassemble_content + @primary_ns, @contact, @serial = content.split(/\s+/) unless content.blank? + @serial = @serial.to_i unless @serial.nil? + update_serial if @serial.nil? || @serial.zero? + end +end + +Soa = SOA diff --git a/app/models/txt.rb b/app/models/txt.rb new file mode 100644 index 0000000..64af818 --- /dev/null +++ b/app/models/txt.rb @@ -0,0 +1,14 @@ +# See #TXT + +# = Text Record (TXT) +# Provides the ability to associate some text with a host or other name. The TXT +# record is used to define the Sender Policy Framework (SPF) information record +# which may be used to validate legitimate email sources from a domain. The SPF +# record while being increasing deployed is not (July 2004) a formal IETF RFC +# standard. +# +# Obtained from http://www.zytrax.com/books/dns/ch8/txt.html +class TXT < Record + validate :content, :presence => true + +end diff --git a/app/models/user.rb b/app/models/user.rb index a6e1f89..7ad789b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,4 +13,6 @@ class User < ActiveRecord::Base # Setup accessible (or protected) attributes for your model attr_accessible :email, :password, :password_confirmation, :remember_me + + has_many :records end diff --git a/app/validators/ip_validator.rb b/app/validators/ip_validator.rb new file mode 100644 index 0000000..0adf150 --- /dev/null +++ b/app/validators/ip_validator.rb @@ -0,0 +1,62 @@ +# Validates IP addresses +# +# @author Kim Nørgaard +class IpValidator < ActiveModel::EachValidator + + # @param [Hash] options Options for validation + # @option options [Symbol] :ip_type (:any) The IP address type (:any, :v4 or :v6) + # @see ActiveModel::EachValidator#new + def initialize(options) + options[:ip_type] ||= :any + super + end + + def validate_each(record, attribute, value) + case options[:ip_type] + when :v4 + record.errors.add(attribute, options[:message] || :ipv4) unless ipv4?(value) + when :v6 + record.errors.add(attribute, options[:message] || :ipv6) unless ipv6?(value) + else + record.errors.add(attribute, options[:message] || :ip) unless ip?(value) + end + end +private + # Validates IPv4 address + # @param [String] address the ipv4 address + # @return [Boolean] the validation result + def ipv4?(address) + address =~ /^ + (?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} + (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) + $/x + end + + # Validates IPv6 address + # @param [String] address the ipv6 address + # @return [Boolean] the validation result + def ipv6?(address) + address =~ /^ + ( + (([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}) + |(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4}) + |(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}) + |(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}) + |(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}) + |(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}) + |(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) + |(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) + |(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) + |([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) + |(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) + |(([0-9A-Fa-f]{1,4}:){1,7}:) + )$/x + end + + # Validates IP (v4 or v6) address + # @param [String] address the ip address + # @return [Boolean] the validation result + def ip?(address) + ipv4?(address) || ipv6?(address) + end +end diff --git a/app/validators/zone_validator.rb b/app/validators/zone_validator.rb new file mode 100644 index 0000000..83ddd7c --- /dev/null +++ b/app/validators/zone_validator.rb @@ -0,0 +1,46 @@ +# Validates Zone objects +# +# @author Kim Nørgaard +class ZoneValidator < ActiveModel::Validator + def validate(record) + record.errors.add(:mname, options[:message] || :mname) if mname_is_zone_name?(record) + record.errors.add(:base, options[:message] || :missing_ns_record) unless has_two_ns_rr?(record) + record.errors.add(:mname, options[:message] || :not_a_defined_nameserver) unless mname_is_a_defined_nameserver?(record) + record.errors.add(:base, options[:message] || :duplicate_nameservers_found) unless nameservers_are_unique?(record) + end + +private + # Check if the zone name equals the primary nameserver + # @param [Zone] record The Zone to check + # @return [Boolean] True if zone name and primary nameserver are identical + def mname_is_zone_name?(zone) + zone.name == zone.mname + end + + # Check if the zone has at least two associated NS resource records + # @param [Zone] zone The Zone to check + # @return [Boolean] True if the zone has at least two associated NS resource records + def has_two_ns_rr?(zone) + zone.ns_resource_records.length >= 2 + end + + # Check if the primary nameserver of the zone is defined as one of the + # associated NS resource records + # @param [Zone] zone The Zone to check + # @return [Boolean] True if the primary nameserver is defined as one of the + # associated NS resource records + def mname_is_a_defined_nameserver?(zone) + unless zone.mname.blank? + nameservers = zone.ns_resource_records.map(&:rdata) + nameservers.select {|value| value.downcase == zone.mname.downcase}.size > 0 + end + end + + # Check if the NS resource records of the zone are unique + # @param [Zone] zone The Zone to check + # @return [Boolean] True if the Zone has unique NS resource records associated + def nameservers_are_unique?(zone) + nameserver_data_fields = zone.ns_resource_records.map(&:rdata) + nameserver_data_fields.inject({}) {|h,v| h[v]=h[v].to_i+1; h}.reject{|k,v| v==1}.keys.empty? + end +end \ No newline at end of file diff --git a/app/views/active_scaffold_overrides/_form_association.html.erb b/app/views/active_scaffold_overrides/_form_association.html.erb new file mode 100644 index 0000000..d52353c --- /dev/null +++ b/app/views/active_scaffold_overrides/_form_association.html.erb @@ -0,0 +1,21 @@ +<% +parent_record = @record +associated = column.singular_association? ? [parent_record.send(column.name)].compact : parent_record.send(column.name).to_a +associated = associated.sort_by {|r| r.new_record? ? 99999999999 : r.id} unless column.association.options.has_key?(:order) +if show_blank_record = column.show_blank_record?(associated) + child = column.singular_association? ? parent_record.send(:"build_#{column.name}") : parent_record.send(column.name).build + reflection = parent_record.class.reflect_on_association(column.name) + if reflection && reflection.reverse + reverse_macro = child.class.reflect_on_association(reflection.reverse).macro + child.send(:"#{reflection.reverse}=", parent_record) if [:has_one, :belongs_to].include?(reverse_macro) + end + associated << child +end +subform_div_id = "#{sub_form_id({:association => column.name, :id => parent_record.id || 99999999999})}-div" +-%> +
<%= column.label -%>
+
> + <%= render :partial => subform_partial_for_column(column), :locals => {:column => column, :parent_record => parent_record, :associated => associated, :show_blank_record => show_blank_record} %> +
+<%= link_to_visibility_toggle(subform_div_id, {:default_visible => !column.collapsed}) -%> +<% @record = parent_record -%> diff --git a/app/views/domains/_form_association_footer.html.erb b/app/views/domains/_form_association_footer.html.erb new file mode 100644 index 0000000..c07da7a --- /dev/null +++ b/app/views/domains/_form_association_footer.html.erb @@ -0,0 +1,49 @@ +<% +# hide "Replace with new" for SOA record + +begin + remote_controller = active_scaffold_controller_for(column.association.klass) +rescue ActiveScaffold::ControllerNotFound + remote_controller = nil +end +@record = parent_record + +show_add_existing = column_show_add_existing(column) +show_add_new = column_show_add_new(column, associated, @record) + +return unless show_add_new or show_add_existing + +edit_associated_url = url_for(:action => 'edit_associated', :id => parent_record.id, :association => column.name, :associated_id => '--ID--', :escape => false, :eid => params[:eid], :parent_controller => params[:parent_controller], :parent_id => params[:parent_id]) if show_add_existing +add_new_url = url_for(:action => 'edit_associated', :id => parent_record.id, :association => column.name, :escape => false, :eid => params[:eid], :parent_controller => params[:parent_controller], :parent_id => params[:parent_id]) if show_add_new + +-%> + diff --git a/app/views/domains/_list_record_columns.html.erb b/app/views/domains/_list_record_columns.html.erb index 672f49a..f19874f 100644 --- a/app/views/domains/_list_record_columns.html.erb +++ b/app/views/domains/_list_record_columns.html.erb @@ -4,7 +4,12 @@ <% if column.name == :records %> - <% column_value = 'Manage Records (0)' if column_value == '-' %> + <% column_value = 'Manage All Records (0)' if column_value == '-' %> + <%= authorized ? render_list_column(column_value, column, record) : column_value %> + + <% elsif column.name == :ns_records %> + + <% column_value = 'Manage NS Records (0)' if column_value == '-' %> <%= authorized ? render_list_column(column_value, column, record) : column_value %> <% else %> diff --git a/app/views/fragments/_bottom.html.erb b/app/views/fragments/_bottom.html.erb index 87ae651..ee15e73 100644 --- a/app/views/fragments/_bottom.html.erb +++ b/app/views/fragments/_bottom.html.erb @@ -1,5 +1,10 @@ \ No newline at end of file diff --git a/app/views/home/_headlines.html.erb b/app/views/home/_headlines.html.erb index 7e9dae2..18b37d7 100644 --- a/app/views/home/_headlines.html.erb +++ b/app/views/home/_headlines.html.erb @@ -24,6 +24,7 @@
<%= link_to new_user_registration_path, :style => 'text-decoration: none' do %> + <%= image_tag 'marketing/arrow_left.png' %>