kiss 0.9.4 → 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION +1 -1
- data/lib/kiss.rb +545 -446
- data/lib/kiss/action.rb +34 -56
- data/lib/kiss/bench.rb +54 -0
- data/lib/kiss/controller_accessors.rb +42 -26
- data/lib/kiss/debug.rb +45 -0
- data/lib/kiss/exception_report.rb +49 -44
- data/lib/kiss/form.rb +105 -86
- data/lib/kiss/form/field.rb +212 -98
- data/lib/kiss/format.rb +175 -37
- data/lib/kiss/hacks.rb +133 -22
- data/lib/kiss/mailer.rb +4 -16
- data/lib/kiss/model.rb +60 -28
- data/lib/kiss/rack/bench.rb +2 -87
- data/lib/kiss/rack/log_exceptions.rb +2 -11
- data/lib/kiss/rack/show_debug.rb +3 -71
- data/lib/kiss/rack/show_exceptions.rb +2 -15
- data/lib/kiss/sequel_mysql.rb +4 -2
- data/lib/kiss/static_file.rb +31 -0
- data/lib/kiss/template_methods.rb +4 -11
- metadata +5 -3
- data/lib/kiss/rack/file.rb +0 -0
    
        data/VERSION
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            0 | 
| 1 | 
            +
            1.0
         | 
    
        data/lib/kiss.rb
    CHANGED
    
    | @@ -31,6 +31,32 @@ module Rack | |
| 31 31 | 
             
              autoload :ShowExceptions, 'kiss/rack/show_exceptions'
         | 
| 32 32 | 
             
            end
         | 
| 33 33 |  | 
| 34 | 
            +
            module Digest; end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            class Class
         | 
| 37 | 
            +
              # adapted from Rails, re-written for speed (only one class_eval call)
         | 
| 38 | 
            +
              def cattr_reader(*syms)
         | 
| 39 | 
            +
                class_eval(
         | 
| 40 | 
            +
                  syms.flatten.map do |sym|
         | 
| 41 | 
            +
                    sym.is_a?(Hash) ? '' : %Q(
         | 
| 42 | 
            +
                      unless defined? @@#{sym}
         | 
| 43 | 
            +
                        @@#{sym} = nil
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                      def self.#{sym}
         | 
| 47 | 
            +
                        @@#{sym}
         | 
| 48 | 
            +
                      end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                      def #{sym}
         | 
| 51 | 
            +
                        @@#{sym}
         | 
| 52 | 
            +
                      end
         | 
| 53 | 
            +
                    
         | 
| 54 | 
            +
                    )
         | 
| 55 | 
            +
                  end.join, __FILE__, __LINE__
         | 
| 56 | 
            +
                )
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
| 59 | 
            +
             | 
| 34 60 | 
             
            # Kiss - An MVC web application framework for Ruby, built on:
         | 
| 35 61 | 
             
            # * Erubis template engine
         | 
| 36 62 | 
             
            # * Sequel database ORM library
         | 
| @@ -40,523 +66,580 @@ class Kiss | |
| 40 66 | 
             
              autoload :Mailer, 'kiss/mailer'
         | 
| 41 67 | 
             
              autoload :Iterator, 'kiss/iterator'
         | 
| 42 68 | 
             
              autoload :Form, 'kiss/form'
         | 
| 69 | 
            +
              autoload :Format, 'kiss/format'
         | 
| 43 70 | 
             
              autoload :SequelSession, 'kiss/sequel_session'
         | 
| 71 | 
            +
              autoload :StaticFile, 'kiss/static_file'
         | 
| 72 | 
            +
              autoload :Bench, 'kiss/bench'
         | 
| 73 | 
            +
              autoload :Debug, 'kiss/debug'
         | 
| 74 | 
            +
              
         | 
| 75 | 
            +
              @@digest = {
         | 
| 76 | 
            +
                :MD5 => "digest/md5",
         | 
| 77 | 
            +
                :RMD160 => "digest/rmd160",
         | 
| 78 | 
            +
                :SHA1 => "digest/sha1",
         | 
| 79 | 
            +
                :SHA256 => "digest/sha2",
         | 
| 80 | 
            +
                :SHA384 => "digest/sha2",
         | 
| 81 | 
            +
                :SHA512 => "digest/sha2"
         | 
| 82 | 
            +
              }
         | 
| 83 | 
            +
              @@digest.each_pair do |type,path|
         | 
| 84 | 
            +
                Digest.autoload type, path
         | 
| 85 | 
            +
              end
         | 
| 44 86 |  | 
| 45 | 
            -
               | 
| 46 | 
            -
             | 
| 47 | 
            -
                : | 
| 87 | 
            +
              # attributes below are application-wide
         | 
| 88 | 
            +
              cattr_reader :action_dir, :template_dir, :email_template_dir, :model_dir, :upload_dir,
         | 
| 89 | 
            +
                :evolution_dir, :asset_dir, :public_dir, :db, :environment, :options, :layout, :rack_file
         | 
| 48 90 |  | 
| 49 | 
            -
               | 
| 91 | 
            +
              # attributes below are request-specific
         | 
| 92 | 
            +
              attr_reader :params, :args, :action, :action_subdir, :action_path, :extension, :host, :request,
         | 
| 93 | 
            +
                :session, :login
         | 
| 50 94 |  | 
| 51 | 
            -
               | 
| 95 | 
            +
              attr_accessor :last_sql
         | 
| 96 | 
            +
              
         | 
| 97 | 
            +
              @@default_action = 'index'
         | 
| 98 | 
            +
              @@default_cookie_name = 'Kiss'
         | 
| 52 99 |  | 
| 53 100 | 
             
              # these supplement the mime types from Rack::File
         | 
| 54 101 | 
             
              @@mime_types = {
         | 
| 55 102 | 
             
                'rhtml' => 'text/html'
         | 
| 56 103 | 
             
              }
         | 
| 57 104 |  | 
| 58 | 
            -
              # Purposely empty class.
         | 
| 59 | 
            -
              # ActionDone exception is raised when render complete to abort execution.
         | 
| 60 | 
            -
              # Could probably use throw...catch instead.
         | 
| 61 | 
            -
              class ActionDone < Exception; end
         | 
| 62 | 
            -
              
         | 
| 63 105 | 
             
              ### Class Methods
         | 
| 64 106 |  | 
| 65 | 
            -
               | 
| 66 | 
            -
             | 
| 67 | 
            -
                 | 
| 68 | 
            -
             | 
| 69 | 
            -
              
         | 
| 70 | 
            -
              # Converts date strings from m/d/y to y/m/d. Useful for MySQL.
         | 
| 71 | 
            -
              def self.mdy_to_ymd(date)
         | 
| 72 | 
            -
                return '0000-00-00' unless date && date =~ /\S/
         | 
| 73 | 
            -
                date.sub!(/\A\D+/,'')
         | 
| 74 | 
            -
                date.sub!(/\D+\Z/,'')
         | 
| 75 | 
            -
                
         | 
| 76 | 
            -
                month, day, year = date.split(/\D+/,3).each {|s| s.to_i}
         | 
| 77 | 
            -
                
         | 
| 78 | 
            -
                return 0 unless month && day
         | 
| 79 | 
            -
                
         | 
| 80 | 
            -
                current_year = Time.now.year
         | 
| 81 | 
            -
                if !year || year.length == 0
         | 
| 82 | 
            -
                  # use current year if year is missing.
         | 
| 83 | 
            -
                  year = current_year
         | 
| 84 | 
            -
                else
         | 
| 85 | 
            -
                  # convert two-digit years to four-digit years
         | 
| 86 | 
            -
                  year = year.to_i
         | 
| 87 | 
            -
                  if year < 100
         | 
| 88 | 
            -
                    year += 1900
         | 
| 89 | 
            -
                    year += 100 if year < current_year - 95
         | 
| 90 | 
            -
                  end
         | 
| 107 | 
            +
              class << self
         | 
| 108 | 
            +
                # Creates new controller instance to handle Rack request.
         | 
| 109 | 
            +
                def call(env)
         | 
| 110 | 
            +
                  new.call(env)
         | 
| 91 111 | 
             
                end
         | 
| 92 112 |  | 
| 93 | 
            -
                 | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 101 | 
            -
             | 
| 113 | 
            +
                # Runs Kiss application found at project_dir (default: '..'), with options 
         | 
| 114 | 
            +
                # read from config files plus additional options if passed in.
         | 
| 115 | 
            +
                def run(options = nil)
         | 
| 116 | 
            +
                  begin
         | 
| 117 | 
            +
                    if @@options
         | 
| 118 | 
            +
                      merge_options(options) if options
         | 
| 119 | 
            +
                    else
         | 
| 120 | 
            +
                      load(options)
         | 
| 121 | 
            +
                    end
         | 
| 102 122 |  | 
| 103 | 
            -
             | 
| 104 | 
            -
             | 
| 105 | 
            -
                   | 
| 106 | 
            -
                     | 
| 123 | 
            +
                    # TODO: rewrite evolution file exists check for speed
         | 
| 124 | 
            +
                    check_evolution_number if @@db
         | 
| 125 | 
            +
                  
         | 
| 126 | 
            +
                    app = self
         | 
| 127 | 
            +
                    builder_options = @@options[:rack_builder] || []
         | 
| 128 | 
            +
                    rack = Rack::Builder.new do
         | 
| 129 | 
            +
                      builder_options.each do |builder_option|
         | 
| 130 | 
            +
                        if builder_option.is_a?(Array)
         | 
| 131 | 
            +
                          builder_args = builder_option
         | 
| 132 | 
            +
                          builder_option = builder_args.shift
         | 
| 133 | 
            +
                        else
         | 
| 134 | 
            +
                          builder_args = []
         | 
| 135 | 
            +
                        end
         | 
| 107 136 |  | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
                when :integer_unsigned, :unsigned_integer, :id_or_zero, :id_zero
         | 
| 113 | 
            -
                  return /\A\d+\Z/,
         | 
| 114 | 
            -
                    'must be a positive integer or zero'
         | 
| 115 | 
            -
             | 
| 116 | 
            -
                when :integer_negative, :negative_integer
         | 
| 117 | 
            -
                  return /\A\-\d*[1-9]\d*\Z/,
         | 
| 118 | 
            -
                    'must be a negative integer'
         | 
| 137 | 
            +
                        unless builder_option.is_a?(Class)
         | 
| 138 | 
            +
                          builder_option = Rack.const_get(builder_option.to_s)
         | 
| 139 | 
            +
                        end
         | 
| 119 140 |  | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
                    'must be a decimal number'
         | 
| 141 | 
            +
                        use(builder_option,*builder_args)
         | 
| 142 | 
            +
                      end
         | 
| 123 143 |  | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
                    'only letters and numbers'
         | 
| 127 | 
            -
             | 
| 128 | 
            -
                when :word
         | 
| 129 | 
            -
                  return /\A\w+\Z/,
         | 
| 130 | 
            -
                    'only letters, numbers, and _'
         | 
| 131 | 
            -
             | 
| 132 | 
            -
                when :email_address
         | 
| 133 | 
            -
                  return /\A[A-Z0-9._%+-]+\@([A-Z0-9-]+\.)+[A-Z]{2,4}\Z/i,
         | 
| 134 | 
            -
                    'must be a valid email address'
         | 
| 135 | 
            -
             | 
| 136 | 
            -
                when :date
         | 
| 137 | 
            -
                  return /\A\d+\D\d+(\D\d+)?\Z/,
         | 
| 138 | 
            -
                    'must be a valid date'
         | 
| 139 | 
            -
             | 
| 140 | 
            -
                when :time
         | 
| 141 | 
            -
                  return /\A\d+\:\d+\s*[ap]m\Z/i,
         | 
| 142 | 
            -
                    'must be a valid time'
         | 
| 143 | 
            -
                    
         | 
| 144 | 
            -
                when :datetime
         | 
| 145 | 
            -
                  return /\A\d+\D\d+(\D\d+)?\s+\d{1,2}\:\d{2}\s*[ap]m\Z/i,
         | 
| 146 | 
            -
                    'must be a valid date and time'
         | 
| 144 | 
            +
                      run app
         | 
| 145 | 
            +
                    end
         | 
| 147 146 |  | 
| 148 | 
            -
             | 
| 149 | 
            -
             | 
| 150 | 
            -
             | 
| 151 | 
            -
             | 
| 152 | 
            -
             | 
| 153 | 
            -
             | 
| 154 | 
            -
             | 
| 155 | 
            -
             | 
| 156 | 
            -
             | 
| 157 | 
            -
             | 
| 158 | 
            -
             | 
| 159 | 
            -
             | 
| 160 | 
            -
             | 
| 147 | 
            +
                    handler = @@options[:rack_handler] || Rack::Handler::WEBrick
         | 
| 148 | 
            +
                    if !handler.is_a?(Class)
         | 
| 149 | 
            +
                      handler = Rack::Handler.const_get(handler.to_s)
         | 
| 150 | 
            +
                    end
         | 
| 151 | 
            +
                    handler.run(rack,@@options[:rack_handler_options] || {:Port => 4000})
         | 
| 152 | 
            +
                  rescue StandardError, LoadError, SyntaxError => e
         | 
| 153 | 
            +
                    if @@options[:rack_handler] == :CGI
         | 
| 154 | 
            +
                      print "Content-type: text/html\n\n"
         | 
| 155 | 
            +
                      print Kiss::ExceptionReport.generate(e)
         | 
| 156 | 
            +
                    else
         | 
| 157 | 
            +
                      print "Content-type: text/plain\n\n"
         | 
| 158 | 
            +
                      puts "exception:\n" + e.message
         | 
| 159 | 
            +
                      puts "\ntraceback:\n" + e.backtrace.join("\n")
         | 
| 160 | 
            +
                    end
         | 
| 161 | 
            +
                  end
         | 
| 161 162 | 
             
                end
         | 
| 162 163 |  | 
| 163 | 
            -
                 | 
| 164 | 
            -
             | 
| 165 | 
            -
             | 
| 166 | 
            -
             | 
| 167 | 
            -
             | 
| 168 | 
            -
             | 
| 169 | 
            -
             | 
| 170 | 
            -
             | 
| 171 | 
            -
             | 
| 172 | 
            -
             | 
| 164 | 
            +
                # Load and set up Kiss application from config file options and
         | 
| 165 | 
            +
                # any passed-in options.
         | 
| 166 | 
            +
                def load(loader_options = nil)
         | 
| 167 | 
            +
                  # store cached files
         | 
| 168 | 
            +
                  @@file_cache = {}
         | 
| 169 | 
            +
                  @@directory_cache = {}
         | 
| 170 | 
            +
                  @@file_cache_time = {}
         | 
| 171 | 
            +
                  
         | 
| 172 | 
            +
                  loader_options ||= {}
         | 
| 173 | 
            +
                  # if loader_options is string, then it specifies environment
         | 
| 174 | 
            +
                  # else it should be a hash of config options
         | 
| 175 | 
            +
                  if loader_options.is_a?(String)
         | 
| 176 | 
            +
                    loader_options = { :environment => loader_options }
         | 
| 177 | 
            +
                  end
         | 
| 173 178 |  | 
| 174 | 
            -
             | 
| 175 | 
            -
             | 
| 176 | 
            -
              
         | 
| 177 | 
            -
              # Returns exception cache, for use in Kiss::ExceptionReport.
         | 
| 178 | 
            -
              def self.exception_cache
         | 
| 179 | 
            -
                @@exception_cache
         | 
| 180 | 
            -
              end
         | 
| 181 | 
            -
              
         | 
| 182 | 
            -
              ### Instance Methods
         | 
| 183 | 
            -
              
         | 
| 184 | 
            -
              # Adds specified data to exception cache, to be included in reports generated
         | 
| 185 | 
            -
              # by Kiss::ExceptionReport in case of an exception.
         | 
| 186 | 
            -
              def set_exception_cache(data)
         | 
| 187 | 
            -
                @@exception_cache.merge!(data)
         | 
| 188 | 
            -
              end
         | 
| 189 | 
            -
              
         | 
| 190 | 
            -
              # Clears exception cache.
         | 
| 191 | 
            -
              def clear_exception_cache
         | 
| 192 | 
            -
                @@exception_cache = {}
         | 
| 193 | 
            -
              end
         | 
| 194 | 
            -
              
         | 
| 195 | 
            -
              # Generates string of random text of the specified length.
         | 
| 196 | 
            -
              def random_text(*args)
         | 
| 197 | 
            -
                self.class.random_text(*args)
         | 
| 198 | 
            -
              end
         | 
| 199 | 
            -
              
         | 
| 200 | 
            -
              # Returns URL/URI of app root (corresponding to top level of action_dir).
         | 
| 201 | 
            -
              def app(suffix = nil)
         | 
| 202 | 
            -
                suffix ? @app_url + suffix : @app_url
         | 
| 203 | 
            -
              end
         | 
| 204 | 
            -
              
         | 
| 205 | 
            -
              # Returns path of current action, under action_dir.
         | 
| 206 | 
            -
              def action
         | 
| 207 | 
            -
                @action
         | 
| 208 | 
            -
              end
         | 
| 209 | 
            -
              
         | 
| 210 | 
            -
              # Kiss Model cache, used to invoke and store Kiss database models.
         | 
| 211 | 
            -
              #
         | 
| 212 | 
            -
              # Example:
         | 
| 213 | 
            -
              # models[:users] : database model for `users' table
         | 
| 214 | 
            -
              def models
         | 
| 215 | 
            -
                @dbm
         | 
| 216 | 
            -
              end
         | 
| 217 | 
            -
              alias_method :dbm, :models
         | 
| 218 | 
            -
              
         | 
| 219 | 
            -
              # Returns URL/URI of app's static assets (asset_host or public_uri).
         | 
| 220 | 
            -
              def assets(suffix = nil)
         | 
| 221 | 
            -
                @pub ||= @options[:asset_host]
         | 
| 222 | 
            -
                suffix ? @pub + '/' + suffix : @pub
         | 
| 223 | 
            -
              end
         | 
| 224 | 
            -
              alias_method :pub, :assets
         | 
| 225 | 
            -
              
         | 
| 226 | 
            -
              # Returns true if specified path is a directory.
         | 
| 227 | 
            -
              # Cache result if file_cache_no_reload option is set; otherwise, always check filesystem.
         | 
| 228 | 
            -
              def directory_exists?(dir)
         | 
| 229 | 
            -
                @options[:file_cache_no_reload] ? (
         | 
| 230 | 
            -
                  @directory_cache.has_key?(path) ?
         | 
| 231 | 
            -
                    @directory_cache[dir] :
         | 
| 232 | 
            -
                    @directory_cache[dir] = File.directory?(dir)
         | 
| 233 | 
            -
                  ) : File.directory?(dir)
         | 
| 234 | 
            -
              end
         | 
| 235 | 
            -
              
         | 
| 236 | 
            -
              # Caches the specified file and return its contents.
         | 
| 237 | 
            -
              # If block given, executes block on contents, then cache and return block result.
         | 
| 238 | 
            -
              # If fnf_file_type given, raises exception (of type fnf_exception_class) if file is not found.
         | 
| 239 | 
            -
              def file_cache(path, fnf_file_type = nil, fnf_exception_class = Kiss::FileNotFound)
         | 
| 240 | 
            -
                if (@options[:file_cache_no_reload] && @file_cache.has_key?(path)) || @files_cached_this_request[path]
         | 
| 241 | 
            -
                  return @file_cache[path]
         | 
| 242 | 
            -
                end
         | 
| 179 | 
            +
                  # environment
         | 
| 180 | 
            +
                  @@environment = loader_options[:environment]
         | 
| 243 181 |  | 
| 244 | 
            -
             | 
| 182 | 
            +
                  # directories
         | 
| 183 | 
            +
                  script_dir = $0.sub(/[^\/]+\Z/,'')
         | 
| 184 | 
            +
                  script_dir = '' if script_dir == './'
         | 
| 185 | 
            +
                  @@project_dir = script_dir + (loader_options[:project_dir] || loader_options[:root_dir] || '..')
         | 
| 186 | 
            +
                  Dir.chdir(@@project_dir)
         | 
| 245 187 |  | 
| 246 | 
            -
             | 
| 247 | 
            -
             | 
| 248 | 
            -
                  
         | 
| 249 | 
            -
                   | 
| 250 | 
            -
             | 
| 251 | 
            -
                else
         | 
| 252 | 
            -
                  # expire cache if file (or symlink) modified
         | 
| 253 | 
            -
                  # TODO: what about symlinks to symlinks?
         | 
| 254 | 
            -
                  if !@file_cache_time[path] ||
         | 
| 255 | 
            -
                    @file_cache_time[path] < File.mtime(path) ||
         | 
| 256 | 
            -
                    ( File.symlink?(path) && (@file_cache_time[path] < File.lstat(path).mtime) )
         | 
| 257 | 
            -
                  
         | 
| 258 | 
            -
                      @file_cache[path] = nil
         | 
| 259 | 
            -
                      @file_cache_time[path] = Time.now
         | 
| 260 | 
            -
                      contents = File.read(path)
         | 
| 261 | 
            -
                  end
         | 
| 262 | 
            -
                end
         | 
| 263 | 
            -
                  
         | 
| 264 | 
            -
                @file_cache[path] ||= begin
         | 
| 265 | 
            -
                  (block_given?) ? yield(contents) : contents
         | 
| 266 | 
            -
                end
         | 
| 267 | 
            -
              end
         | 
| 268 | 
            -
              
         | 
| 269 | 
            -
              # Merges specified options into previously defined/merged Kiss options.
         | 
| 270 | 
            -
              def merge_options(config_options)
         | 
| 271 | 
            -
                if config_options
         | 
| 272 | 
            -
                  if env_vars = config_options.delete(:ENV)
         | 
| 273 | 
            -
                    env_vars.each_pair {|k,v| ENV[k] = v }
         | 
| 274 | 
            -
                  end
         | 
| 275 | 
            -
                  if lib_dirs = config_options.delete(:lib_dirs)
         | 
| 276 | 
            -
                    @lib_dirs.push( lib_dirs )
         | 
| 188 | 
            +
                  @@config_dir = loader_options[:config_dir] || 'config'
         | 
| 189 | 
            +
                
         | 
| 190 | 
            +
                  # get environment name from config/environment
         | 
| 191 | 
            +
                  if (@@environment.nil?) && File.file?(env_file = @@config_dir+'/environment')
         | 
| 192 | 
            +
                    @@environment = File.read(env_file).sub(/\s+\Z/,'')
         | 
| 277 193 | 
             
                  end
         | 
| 278 | 
            -
             | 
| 279 | 
            -
             | 
| 194 | 
            +
                
         | 
| 195 | 
            +
                  # init options
         | 
| 196 | 
            +
                  @@options = {
         | 
| 197 | 
            +
                    :layout => '/_layout'
         | 
| 198 | 
            +
                  }
         | 
| 199 | 
            +
                  @@lib_dirs = ['lib']
         | 
| 200 | 
            +
                  @@gem_dirs = ['gems']
         | 
| 201 | 
            +
                  @@require = []
         | 
| 202 | 
            +
                
         | 
| 203 | 
            +
                  # common (shared) config
         | 
| 204 | 
            +
                  if (File.file?(config_file = @@config_dir+'/common.yml'))
         | 
| 205 | 
            +
                    merge_options( YAML::load(File.read(config_file)) )
         | 
| 280 206 | 
             
                  end
         | 
| 281 | 
            -
                   | 
| 282 | 
            -
             | 
| 207 | 
            +
                  # environment config
         | 
| 208 | 
            +
                  if (File.file?(config_file = "#{@@config_dir}/environments/#{@@environment}.yml"))
         | 
| 209 | 
            +
                    merge_options( YAML::load(File.read(config_file)) )
         | 
| 283 210 | 
             
                  end
         | 
| 284 211 |  | 
| 285 | 
            -
                   | 
| 286 | 
            -
                end
         | 
| 287 | 
            -
              end
         | 
| 288 | 
            -
              
         | 
| 289 | 
            -
              # Create a new Kiss application instance, based on specified options.
         | 
| 290 | 
            -
              def initialize(loader_options = {})
         | 
| 291 | 
            -
                # store cached files
         | 
| 292 | 
            -
                @file_cache = {}
         | 
| 293 | 
            -
                @directory_cache = {}
         | 
| 294 | 
            -
                @file_cache_time = {}
         | 
| 295 | 
            -
                
         | 
| 296 | 
            -
                # if loader_options is string, then it specifies environment
         | 
| 297 | 
            -
                # else it should be a hash of config options
         | 
| 298 | 
            -
                if loader_options.is_a?(String)
         | 
| 299 | 
            -
                  loader_options = { :environment => loader_options }
         | 
| 300 | 
            -
                end
         | 
| 301 | 
            -
                
         | 
| 302 | 
            -
                # environment
         | 
| 303 | 
            -
                @environment = loader_options[:environment]
         | 
| 212 | 
            +
                  merge_options( loader_options )
         | 
| 304 213 |  | 
| 305 | 
            -
             | 
| 306 | 
            -
             | 
| 307 | 
            -
             | 
| 308 | 
            -
                @project_dir = script_dir + (loader_options[:project_dir] || loader_options[:root_dir] || '..')
         | 
| 309 | 
            -
                Dir.chdir(@project_dir)
         | 
| 214 | 
            +
                  # set class vars from options
         | 
| 215 | 
            +
                  @@action_dir = @@options[:action_dir] || 'actions'
         | 
| 216 | 
            +
                  @@template_dir = @@options[:template_dir] ? @@options[:template_dir] : @@action_dir
         | 
| 310 217 |  | 
| 311 | 
            -
             | 
| 218 | 
            +
                  @@asset_dir = @@public_dir = @@options[:asset_dir] || @@options[:public_dir] || 'public_html'
         | 
| 312 219 |  | 
| 313 | 
            -
             | 
| 314 | 
            -
             | 
| 315 | 
            -
                   | 
| 316 | 
            -
             | 
| 220 | 
            +
                  @@model_dir = @@options[:model_dir] || 'models'
         | 
| 221 | 
            +
                  Kiss::ModelCache.model_dir = @@model_dir
         | 
| 222 | 
            +
                  
         | 
| 223 | 
            +
                  @@evolution_dir = @@options[:evolution_dir] || 'evolutions'
         | 
| 317 224 |  | 
| 318 | 
            -
             | 
| 319 | 
            -
             | 
| 320 | 
            -
                @lib_dirs = ['lib']
         | 
| 321 | 
            -
                @gem_dirs = ['gems']
         | 
| 322 | 
            -
                @require = []
         | 
| 225 | 
            +
                  @@email_template_dir = @@options[:email_template_dir] || 'email_templates'
         | 
| 226 | 
            +
                  @@upload_dir = @@options[:upload_dir] || 'uploads'
         | 
| 323 227 |  | 
| 324 | 
            -
             | 
| 325 | 
            -
             | 
| 326 | 
            -
                   | 
| 327 | 
            -
             | 
| 328 | 
            -
             | 
| 329 | 
            -
             | 
| 330 | 
            -
                   | 
| 331 | 
            -
             | 
| 228 | 
            +
                  @@cookie_name = @@options[:cookie_name] || @@default_cookie_name
         | 
| 229 | 
            +
                  @@default_action = @@options[:default_action] || @@default_action
         | 
| 230 | 
            +
                  @@action_root_class = @@options[:action_class] || Class.new(Kiss::Action)
         | 
| 231 | 
            +
                  
         | 
| 232 | 
            +
                  # exception log
         | 
| 233 | 
            +
                  @@exception_log_file = @@options[:exception_log] ? ::File.open(@@options[:exception_log],'a') : nil
         | 
| 234 | 
            +
                  
         | 
| 235 | 
            +
                  # default layout
         | 
| 236 | 
            +
                  @@layout = @@options[:layout]
         | 
| 332 237 |  | 
| 333 | 
            -
             | 
| 238 | 
            +
                  # public_uri: uri of requests to serve from public_dir
         | 
| 239 | 
            +
                  @@asset_uri = @@public_uri = @@options[:asset_uri] || @@options[:public_uri]
         | 
| 240 | 
            +
                  @@rack_file = Rack::File.new(@@asset_dir) if @@asset_uri
         | 
| 334 241 |  | 
| 335 | 
            -
             | 
| 336 | 
            -
             | 
| 337 | 
            -
             | 
| 242 | 
            +
                  # app_url: URL of the app actions root
         | 
| 243 | 
            +
                  @@app_host = @@options[:app_host] ? ('http://' + @@options[:app_host]) : ''
         | 
| 244 | 
            +
                  @@app_uri = @@options[:app_uri] || ''
         | 
| 245 | 
            +
                  @@app_url =  @@app_host + @@app_uri
         | 
| 338 246 |  | 
| 339 | 
            -
             | 
| 247 | 
            +
                  # include lib dirs
         | 
| 248 | 
            +
                  $LOAD_PATH.unshift(*( @@lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
         | 
| 340 249 |  | 
| 341 | 
            -
             | 
| 342 | 
            -
             | 
| 250 | 
            +
                  # add gem dir to rubygems search path
         | 
| 251 | 
            +
                  Gem.path.unshift(*( @@gem_dirs.flatten.select {|dir| File.directory?(dir) } ))
         | 
| 343 252 |  | 
| 344 | 
            -
             | 
| 345 | 
            -
             | 
| 253 | 
            +
                  # require libs
         | 
| 254 | 
            +
                  @@require.flatten.each {|lib| require lib }
         | 
| 346 255 |  | 
| 347 | 
            -
             | 
| 348 | 
            -
             | 
| 349 | 
            -
             | 
| 256 | 
            +
                  # session class
         | 
| 257 | 
            +
                  if (@@options[:session_class])
         | 
| 258 | 
            +
                    @@session_class = (@@options[:session_class].class == Class) ? @@options[:session_class] :
         | 
| 259 | 
            +
                      @@options[:session_class].to_const
         | 
| 260 | 
            +
                  end
         | 
| 350 261 |  | 
| 351 | 
            -
             | 
| 352 | 
            -
             | 
| 353 | 
            -
             | 
| 262 | 
            +
                  # load extensions to action class
         | 
| 263 | 
            +
                  action_extension_path = @@action_dir + '/_action.rb'
         | 
| 264 | 
            +
                  if File.file?(action_extension_path)
         | 
| 265 | 
            +
                    @@action_root_class.class_eval(File.read(action_extension_path),action_extension_path) rescue nil
         | 
| 266 | 
            +
                  end
         | 
| 354 267 |  | 
| 355 | 
            -
             | 
| 356 | 
            -
             | 
| 268 | 
            +
                  # database
         | 
| 269 | 
            +
                  if sequel = @@options[:database]
         | 
| 270 | 
            +
                    # open database connection (if not already open)
         | 
| 271 | 
            +
                    @@db = sequel.is_a?(String) ? (Sequel.open sequel) : sequel.is_a?(Hash) ? (Sequel.open sequel) : sequel
         | 
| 272 | 
            +
                  
         | 
| 273 | 
            +
                    if @@db.class.name == 'Sequel::MySQL::Database'
         | 
| 274 | 
            +
                      # add fetch_arrays, all_arrays methods
         | 
| 275 | 
            +
                      require 'kiss/sequel_mysql'
         | 
| 276 | 
            +
                      # turn off convert_tinyint_to_bool, unless options say otherwise
         | 
| 277 | 
            +
                      Sequel.convert_tinyint_to_bool = false unless @@options[:convert_tinyint_to_bool]
         | 
| 278 | 
            +
                    end
         | 
| 279 | 
            +
                  end
         | 
| 357 280 |  | 
| 358 | 
            -
             | 
| 359 | 
            -
             | 
| 281 | 
            +
                  # setup session storage, if session class specified in config
         | 
| 282 | 
            +
                  @@session_class.setup_storage(self) if @@session_class
         | 
| 360 283 |  | 
| 361 | 
            -
             | 
| 362 | 
            -
             | 
| 284 | 
            +
                  # prepare authenticate_exclude
         | 
| 285 | 
            +
                  if @@options[:authenticate_all]
         | 
| 286 | 
            +
                    if @@options[:authenticate_exclude].is_a?(Array)
         | 
| 287 | 
            +
                      @@options[:authenticate_exclude] = @@options[:authenticate_exclude].map do |action|
         | 
| 288 | 
            +
                        action = '/'+action unless action =~ /\A\//
         | 
| 289 | 
            +
                        action
         | 
| 290 | 
            +
                      end
         | 
| 291 | 
            +
                    else
         | 
| 292 | 
            +
                      @@options[:authenticate_exclude] = []
         | 
| 293 | 
            +
                    end
         | 
| 294 | 
            +
                  end
         | 
| 363 295 |  | 
| 364 | 
            -
             | 
| 365 | 
            -
                if (@options[:session_class])
         | 
| 366 | 
            -
                  @session_class = (@options[:session_class].class == Class) ? @options[:session_class] : @options[:session_class].to_const
         | 
| 296 | 
            +
                  self
         | 
| 367 297 | 
             
                end
         | 
| 368 | 
            -
             | 
| 369 | 
            -
                #  | 
| 370 | 
            -
                 | 
| 371 | 
            -
             | 
| 372 | 
            -
                   | 
| 298 | 
            +
              
         | 
| 299 | 
            +
                # Returns URL/URI of app's static assets (asset_host or public_uri).
         | 
| 300 | 
            +
                def assets(suffix = nil)
         | 
| 301 | 
            +
                  @@pub ||= @@options[:asset_host]
         | 
| 302 | 
            +
                  suffix ? @@pub + '/' + suffix : @@pub
         | 
| 303 | 
            +
                end
         | 
| 304 | 
            +
                alias_method :pub, :assets
         | 
| 305 | 
            +
              
         | 
| 306 | 
            +
                # Returns true if specified path is a directory.
         | 
| 307 | 
            +
                # Cache result if file_cache_no_reload option is set; otherwise, always check filesystem.
         | 
| 308 | 
            +
                def directory_exists?(dir)
         | 
| 309 | 
            +
                  @@options[:file_cache_no_reload] ? (
         | 
| 310 | 
            +
                    @@directory_cache.has_key?(path) ?
         | 
| 311 | 
            +
                      @@directory_cache[dir] :
         | 
| 312 | 
            +
                      @@directory_cache[dir] = File.directory?(dir)
         | 
| 313 | 
            +
                    ) : File.directory?(dir)
         | 
| 373 314 | 
             
                end
         | 
| 374 | 
            -
                
         | 
| 375 | 
            -
                # set controller access variables
         | 
| 376 | 
            -
                @action_root_class.set_controller(self)
         | 
| 377 | 
            -
                Kiss::Model.set_controller(self)
         | 
| 378 | 
            -
                Kiss::Mailer.set_controller(self)
         | 
| 379 | 
            -
                
         | 
| 380 | 
            -
                # database
         | 
| 381 | 
            -
                if sequel = @options[:database]
         | 
| 382 | 
            -
                  # open database connection (if not already open)
         | 
| 383 | 
            -
                  @db = sequel.is_a?(String) ? (Sequel.open sequel) : sequel.is_a?(Hash) ? (Sequel.open sequel) : sequel
         | 
| 384 | 
            -
                  # add query logging to database class
         | 
| 385 | 
            -
                  @db.class.class_eval do
         | 
| 386 | 
            -
                    @@query = nil
         | 
| 387 | 
            -
                    def self.last_query
         | 
| 388 | 
            -
                      @@query
         | 
| 389 | 
            -
                    end
         | 
| 390 315 |  | 
| 391 | 
            -
             | 
| 392 | 
            -
             | 
| 393 | 
            -
             | 
| 394 | 
            -
             | 
| 316 | 
            +
                # Merges specified options into previously defined/merged Kiss options.
         | 
| 317 | 
            +
                def merge_options(config_options)
         | 
| 318 | 
            +
                  if config_options
         | 
| 319 | 
            +
                    if env_vars = config_options.delete(:ENV)
         | 
| 320 | 
            +
                      env_vars.each_pair {|k,v| ENV[k] = v }
         | 
| 395 321 | 
             
                    end
         | 
| 322 | 
            +
                    if lib_dirs = config_options.delete(:lib_dirs)
         | 
| 323 | 
            +
                      @@lib_dirs.push( lib_dirs )
         | 
| 324 | 
            +
                    end
         | 
| 325 | 
            +
                    if gem_dirs = config_options.delete(:gem_dirs)
         | 
| 326 | 
            +
                      @@gem_dirs.push( gem_dirs )
         | 
| 327 | 
            +
                    end
         | 
| 328 | 
            +
                    if require_libs = config_options.delete(:require)
         | 
| 329 | 
            +
                      @@require.push( require_libs )
         | 
| 330 | 
            +
                    end
         | 
| 331 | 
            +
                
         | 
| 332 | 
            +
                    @@options.merge!( config_options )
         | 
| 396 333 | 
             
                  end
         | 
| 397 | 
            -
                  
         | 
| 398 | 
            -
                  if @db.class.name == 'Sequel::MySQL::Database'
         | 
| 399 | 
            -
                    # fix sequel mysql bugs; add all_rows
         | 
| 400 | 
            -
                    require 'kiss/sequel_mysql'
         | 
| 401 | 
            -
                    Sequel.convert_tinyint_to_bool = false unless @options[:convert_tinyint_to_bool]
         | 
| 402 | 
            -
                  end
         | 
| 403 | 
            -
                  
         | 
| 404 | 
            -
                  # create models cache
         | 
| 405 | 
            -
                  @dbm = Kiss::ModelCache.new(self,@model_dir)
         | 
| 406 334 | 
             
                end
         | 
| 407 335 |  | 
| 408 | 
            -
                #  | 
| 409 | 
            -
                 | 
| 336 | 
            +
                # Converts passed-in filename to absolute path if it does not start with '/'.
         | 
| 337 | 
            +
                def absolute_path(filename)
         | 
| 338 | 
            +
                  filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
         | 
| 339 | 
            +
                end
         | 
| 410 340 |  | 
| 411 | 
            -
                #  | 
| 412 | 
            -
                 | 
| 413 | 
            -
                   | 
| 414 | 
            -
             | 
| 415 | 
            -
             | 
| 416 | 
            -
                      action
         | 
| 417 | 
            -
                    end
         | 
| 341 | 
            +
                # Returns string representation of object with HTML entities escaped.
         | 
| 342 | 
            +
                def h(obj)
         | 
| 343 | 
            +
                  case obj
         | 
| 344 | 
            +
                  when String
         | 
| 345 | 
            +
                    Rack::Utils.escape_html(obj).gsub(/^(\s+)/) {' ' * $1.length}
         | 
| 418 346 | 
             
                  else
         | 
| 419 | 
            -
                     | 
| 347 | 
            +
                    Rack::Utils.escape_html(obj.inspect)
         | 
| 420 348 | 
             
                  end
         | 
| 421 349 | 
             
                end
         | 
| 422 350 |  | 
| 423 | 
            -
                 | 
| 424 | 
            -
             | 
| 425 | 
            -
             | 
| 426 | 
            -
             | 
| 427 | 
            -
             | 
| 428 | 
            -
                 | 
| 429 | 
            -
                
         | 
| 430 | 
            -
             | 
| 431 | 
            -
             | 
| 432 | 
            -
                 | 
| 433 | 
            -
             | 
| 434 | 
            -
             | 
| 435 | 
            -
             | 
| 436 | 
            -
             | 
| 437 | 
            -
             | 
| 438 | 
            -
             | 
| 351 | 
            +
                # Returns MIME type corresponding to passed-in extension.
         | 
| 352 | 
            +
                def mime_type(extension)
         | 
| 353 | 
            +
                  Rack::File::MIME_TYPES[extension] || @@mime_types[extension]
         | 
| 354 | 
            +
                end
         | 
| 355 | 
            +
                
         | 
| 356 | 
            +
                # Returns Digest class used to generate digest of specified type.
         | 
| 357 | 
            +
                def digest_class(type)
         | 
| 358 | 
            +
                  type = type.to_sym
         | 
| 359 | 
            +
                  @@digest[type] ? Digest.const_get(type) : nil
         | 
| 360 | 
            +
                end
         | 
| 361 | 
            +
                
         | 
| 362 | 
            +
                # Converts date strings from m/d/y to y/m/d. Useful for MySQL.
         | 
| 363 | 
            +
                def mdy_to_ymd(date)
         | 
| 364 | 
            +
                  return '0000-00-00' unless date && date =~ /\S/
         | 
| 365 | 
            +
                  date.sub!(/\A\D+/,'')
         | 
| 366 | 
            +
                  date.sub!(/\D+\Z/,'')
         | 
| 367 | 
            +
                
         | 
| 368 | 
            +
                  month, day, year = date.split(/\D+/,3).each {|s| s.to_i}
         | 
| 369 | 
            +
                
         | 
| 370 | 
            +
                  return 0 unless month && day
         | 
| 371 | 
            +
                  
         | 
| 372 | 
            +
                  current_year = Time.now.year
         | 
| 373 | 
            +
                  if !year || year.length == 0
         | 
| 374 | 
            +
                    # use current year if year is missing.
         | 
| 375 | 
            +
                    year = current_year
         | 
| 376 | 
            +
                  else
         | 
| 377 | 
            +
                    # convert two-digit years to four-digit years
         | 
| 378 | 
            +
                    year = year.to_i
         | 
| 379 | 
            +
                    if year < 100
         | 
| 380 | 
            +
                      year += 1900
         | 
| 381 | 
            +
                      year += 100 if year < current_year - 95
         | 
| 439 382 | 
             
                    end
         | 
| 440 | 
            -
             | 
| 441 | 
            -
             | 
| 442 | 
            -
             | 
| 383 | 
            +
                  end
         | 
| 384 | 
            +
                
         | 
| 385 | 
            +
                  return sprintf("%04d-%02d-%02d",year,month.to_i,day.to_i)
         | 
| 386 | 
            +
                end
         | 
| 387 | 
            +
              
         | 
| 388 | 
            +
                # Validates value against specified format.
         | 
| 389 | 
            +
                # If required is true, value must contain a non-whitespace character.
         | 
| 390 | 
            +
                # If required is false, value need not match format if and only if value contains only whitespace.
         | 
| 391 | 
            +
                def validate_value(value, format, required = false, label = nil)
         | 
| 392 | 
            +
                  if required && (value !~ /\S/)
         | 
| 393 | 
            +
                    # value required
         | 
| 394 | 
            +
                    raise "#{label || 'value'} required"
         | 
| 395 | 
            +
                  elsif format && (value =~ /\S/)
         | 
| 396 | 
            +
                    format = Kiss::Format.lookup(format)
         | 
| 397 | 
            +
                  
         | 
| 398 | 
            +
                    begin
         | 
| 399 | 
            +
                      format.validate(value)
         | 
| 400 | 
            +
                    rescue Kiss::Format::ValidateError => e
         | 
| 401 | 
            +
                      raise e.class, "#{label} validation error: #{e.message}"
         | 
| 443 402 | 
             
                    end
         | 
| 444 | 
            -
                    
         | 
| 445 | 
            -
                    use(builder_option,*builder_args)
         | 
| 446 403 | 
             
                  end
         | 
| 447 | 
            -
             | 
| 448 | 
            -
                  run app
         | 
| 449 404 | 
             
                end
         | 
| 405 | 
            +
              
         | 
| 406 | 
            +
                # Generates string of random text of the specified length.
         | 
| 407 | 
            +
                def random_text(length)
         | 
| 408 | 
            +
                  chars = ('A'..'Z').to_a + ('0'..'9').to_a  # array
         | 
| 409 | 
            +
              
         | 
| 410 | 
            +
                  text = ''
         | 
| 411 | 
            +
                  size = chars.size
         | 
| 412 | 
            +
                  length.times { text += chars[rand(size)] }
         | 
| 450 413 |  | 
| 451 | 
            -
             | 
| 452 | 
            -
                if !handler.is_a?(Class)
         | 
| 453 | 
            -
                  handler = Rack::Handler.const_get(handler.to_s)
         | 
| 414 | 
            +
                  text
         | 
| 454 415 | 
             
                end
         | 
| 455 | 
            -
                handler.run(rack,@options[:rack_handler_options] || {:Port => 4000})
         | 
| 456 | 
            -
              end
         | 
| 457 416 |  | 
| 458 | 
            -
             | 
| 459 | 
            -
             | 
| 460 | 
            -
             | 
| 461 | 
            -
                 | 
| 417 | 
            +
                # Returns exception cache, for use in Kiss::ExceptionReport.
         | 
| 418 | 
            +
                def exception_cache
         | 
| 419 | 
            +
                  @@exception_cache
         | 
| 420 | 
            +
                end
         | 
| 421 | 
            +
                
         | 
| 422 | 
            +
                # Given a file path, caches or returns the file's contents or the return value of
         | 
| 423 | 
            +
                # the passed block applied to the file's contents.
         | 
| 424 | 
            +
                # If file is not found, raises exception of type fnf_exception_class.
         | 
| 425 | 
            +
                def file_cache(path, fnf_file_type = nil, fnf_exception_class = Kiss::FileNotFound)
         | 
| 426 | 
            +
                  if (@@options[:file_cache_no_reload] && @@file_cache.has_key?(path))
         | 
| 427 | 
            +
                    return @@file_cache[path]
         | 
| 428 | 
            +
                  end
         | 
| 429 | 
            +
                  
         | 
| 430 | 
            +
                  if !File.file?(path)
         | 
| 431 | 
            +
                    raise fnf_exception_class, "#{fnf_file_type} file missing: '#{path}'" if fnf_file_type
         | 
| 462 432 |  | 
| 463 | 
            -
             | 
| 464 | 
            -
             | 
| 465 | 
            -
             | 
| 433 | 
            +
                    @@file_cache[path] = nil
         | 
| 434 | 
            +
                    contents = nil
         | 
| 435 | 
            +
                  else
         | 
| 436 | 
            +
                    # expire cache if file (or symlink) modified
         | 
| 437 | 
            +
                    # TODO: what about symlinks to symlinks?
         | 
| 438 | 
            +
                    if !@@file_cache_time[path] ||
         | 
| 439 | 
            +
                      @@file_cache_time[path] < File.mtime(path) ||
         | 
| 440 | 
            +
                      ( File.symlink?(path) && (@@file_cache_time[path] < File.lstat(path).mtime) )
         | 
| 441 | 
            +
                
         | 
| 442 | 
            +
                        @@file_cache[path] = nil
         | 
| 443 | 
            +
                        @@file_cache_time[path] = Time.now
         | 
| 444 | 
            +
                        contents = File.read(path)
         | 
| 445 | 
            +
                    end
         | 
| 446 | 
            +
                  end
         | 
| 447 | 
            +
                  
         | 
| 448 | 
            +
                  @@file_cache[path] ||= begin
         | 
| 449 | 
            +
                    (block_given?) ? yield(contents) : contents
         | 
| 450 | 
            +
                  end
         | 
| 466 451 | 
             
                end
         | 
| 467 452 |  | 
| 468 | 
            -
                 | 
| 453 | 
            +
                # Returns Sequel dataset to evolution_number table, which specifies app's current evolution number.
         | 
| 454 | 
            +
                # Creates evolution_number table if it does not exist.
         | 
| 455 | 
            +
                def evolution_number_table
         | 
| 456 | 
            +
                  unless db.table_exists?(:evolution_number)
         | 
| 457 | 
            +
                    db.create_table :evolution_number do
         | 
| 458 | 
            +
                      column :version, :integer, :null=> false
         | 
| 459 | 
            +
                    end
         | 
| 460 | 
            +
                    db[:evolution_number].insert(:version => 0)
         | 
| 461 | 
            +
                  end
         | 
| 462 | 
            +
                  db[:evolution_number]
         | 
| 463 | 
            +
                end
         | 
| 464 | 
            +
              
         | 
| 465 | 
            +
                # Returns app's current evolution number.
         | 
| 466 | 
            +
                def evolution_number
         | 
| 467 | 
            +
                  evolution_number_table.first.version
         | 
| 468 | 
            +
                end
         | 
| 469 | 
            +
              
         | 
| 470 | 
            +
                # Sets app's current evolution number.
         | 
| 471 | 
            +
                def evolution_number=(version)
         | 
| 472 | 
            +
                  load unless @@options
         | 
| 473 | 
            +
                  evolution_number_table.update(:version => version)
         | 
| 474 | 
            +
                end
         | 
| 475 | 
            +
              
         | 
| 476 | 
            +
                # Check whether there exists a file in evolution_dir whose number is greater than app's
         | 
| 477 | 
            +
                # current evolution number. If so, raise an error to indicate need to apply new evolutions.
         | 
| 478 | 
            +
                def check_evolution_number
         | 
| 479 | 
            +
                  version = evolution_number
         | 
| 480 | 
            +
                  if Kiss.directory_exists?(@@evolution_dir) &&
         | 
| 481 | 
            +
                      Dir.entries(@@evolution_dir).select { |f| f =~ /\A0*#{version+1}_/ }.size > 0
         | 
| 482 | 
            +
                    raise "current evolution #{version} is outdated; apply evolutions or update evolution number"
         | 
| 483 | 
            +
                  end
         | 
| 484 | 
            +
                end
         | 
| 485 | 
            +
              end # end class methods
         | 
| 486 | 
            +
              
         | 
| 487 | 
            +
              ### Instance Methods
         | 
| 488 | 
            +
              
         | 
| 489 | 
            +
              # Creates a new controller instance, and also configures the application with the
         | 
| 490 | 
            +
              # specified options.
         | 
| 491 | 
            +
              def initialize(options = nil)
         | 
| 492 | 
            +
                if @@options
         | 
| 493 | 
            +
                  self.class.merge_options(options) if options
         | 
| 494 | 
            +
                else
         | 
| 495 | 
            +
                  self.class.load(options)
         | 
| 496 | 
            +
                end
         | 
| 469 497 |  | 
| 498 | 
            +
                @exception_cache = {}
         | 
| 499 | 
            +
                @debug_messages = []
         | 
| 500 | 
            +
                @benchmarks = []
         | 
| 470 501 | 
             
                @files_cached_this_request = {}
         | 
| 502 | 
            +
              end
         | 
| 503 | 
            +
              
         | 
| 504 | 
            +
              # Caches the specified file and return its contents.  See Kiss.file_cache above.
         | 
| 505 | 
            +
              def file_cache(path, fnf_file_type = nil, fnf_exception_class = Kiss::FileNotFound, &block)
         | 
| 506 | 
            +
                return @@file_cache[path] if @files_cached_this_request[path]
         | 
| 507 | 
            +
                @files_cached_this_request[path] = true
         | 
| 471 508 |  | 
| 472 | 
            -
                 | 
| 473 | 
            -
             | 
| 474 | 
            -
             | 
| 509 | 
            +
                self.class.file_cache(path, fnf_file_type, fnf_exception_class, &block)
         | 
| 510 | 
            +
              end
         | 
| 511 | 
            +
              
         | 
| 512 | 
            +
              # Processes and responds to a request received via Rack.  Should only be called once
         | 
| 513 | 
            +
              # for each controller instance (i.e. each controller instance should handle at most 
         | 
| 514 | 
            +
              # one request, then be discarded).  Returns array of response code, headers, and body.
         | 
| 515 | 
            +
              def call(env)
         | 
| 516 | 
            +
                if @@rack_file && (
         | 
| 517 | 
            +
                  (env["PATH_INFO"] == '/favicon.ico') ||
         | 
| 518 | 
            +
                   (env["PATH_INFO"].sub!(Regexp.new("\\A#{@@asset_uri}"),''))
         | 
| 519 | 
            +
                )
         | 
| 520 | 
            +
                  return @@rack_file.call(env)
         | 
| 521 | 
            +
                end
         | 
| 475 522 |  | 
| 476 | 
            -
                 | 
| 477 | 
            -
                  parse_action_path(@path)
         | 
| 523 | 
            +
                # catch and report exceptions in this block
         | 
| 478 524 |  | 
| 479 | 
            -
             | 
| 480 | 
            -
                   | 
| 481 | 
            -
             | 
| 482 | 
            -
                   | 
| 483 | 
            -
             | 
| 484 | 
            -
             | 
| 485 | 
            -
             | 
| 525 | 
            +
                code, headers, body = begin
         | 
| 526 | 
            +
                  get_request(env)
         | 
| 527 | 
            +
                  @response = Rack::Response.new
         | 
| 528 | 
            +
                  
         | 
| 529 | 
            +
                  catch :kiss_action_done do
         | 
| 530 | 
            +
                    parse_action_path(@path)
         | 
| 531 | 
            +
                    env['kiss.parsed_action'] = @action
         | 
| 532 | 
            +
                    env['kiss.parsed_args'] = @args.inspect
         | 
| 533 | 
            +
                    
         | 
| 534 | 
            +
                    setup_session
         | 
| 535 | 
            +
                    if login_session_valid?
         | 
| 536 | 
            +
                      load_from_login_session
         | 
| 537 | 
            +
                    elsif @@options[:authenticate_all]
         | 
| 538 | 
            +
                      if (!@@options[:authenticate_exclude].is_a?(Array) ||
         | 
| 539 | 
            +
                          @@options[:authenticate_exclude].select {|action| action == @action}.size == 0)
         | 
| 540 | 
            +
                        authenticate
         | 
| 541 | 
            +
                      end
         | 
| 486 542 | 
             
                    end
         | 
| 543 | 
            +
                  
         | 
| 544 | 
            +
                    env['kiss.processed_action'] = @action
         | 
| 545 | 
            +
                    process.render
         | 
| 487 546 | 
             
                  end
         | 
| 488 | 
            -
                   | 
| 489 | 
            -
             | 
| 547 | 
            +
                  finalize_session if @session
         | 
| 548 | 
            +
                  
         | 
| 549 | 
            +
                  @response.finish
         | 
| 550 | 
            +
                rescue StandardError, LoadError, SyntaxError => e
         | 
| 551 | 
            +
                  body = Kiss::ExceptionReport.generate(e, env, @exception_cache, @last_sql)
         | 
| 552 | 
            +
                  if @@exception_log_file
         | 
| 553 | 
            +
                    @@exception_log_file.print(body + "\n--- End of exception report --- \n\n")
         | 
| 554 | 
            +
                  end
         | 
| 555 | 
            +
                  [500, {
         | 
| 556 | 
            +
                    "Content-Type" => "text/html",
         | 
| 557 | 
            +
                    "Content-Length" => body.length.to_s,
         | 
| 558 | 
            +
                    "X-Kiss-Error-Type" => e.class.name,
         | 
| 559 | 
            +
                    "X-Kiss-Error-Message" => e.message.sub(/\n.*/m,'')
         | 
| 560 | 
            +
                  }, body]
         | 
| 561 | 
            +
                end
         | 
| 562 | 
            +
                
         | 
| 563 | 
            +
                if @debug_messages.size > 0
         | 
| 564 | 
            +
                  extend Kiss::Debug
         | 
| 565 | 
            +
                  body = prepend_debug(body)
         | 
| 566 | 
            +
                  headers['Content-Length'] = body.length.to_s
         | 
| 490 567 | 
             
                end
         | 
| 491 | 
            -
                finalize_session if @session
         | 
| 492 568 |  | 
| 493 | 
            -
                @ | 
| 569 | 
            +
                if @benchmarks.size > 0
         | 
| 570 | 
            +
                  stop_benchmark
         | 
| 571 | 
            +
                  extend Kiss::Bench
         | 
| 572 | 
            +
                  body = prepend_benchmarks(body)
         | 
| 573 | 
            +
                  headers['Content-Length'] = body.length.to_s
         | 
| 574 | 
            +
                end
         | 
| 575 | 
            +
                
         | 
| 576 | 
            +
                [code,headers,body]
         | 
| 577 | 
            +
              end
         | 
| 578 | 
            +
              
         | 
| 579 | 
            +
              # Adds debug message to inspect object.  Debug messages will be shown at top of
         | 
| 580 | 
            +
              # application response body.
         | 
| 581 | 
            +
              def debug(object, context = Kernel.caller[0])
         | 
| 582 | 
            +
                @debug_messages.push( [object.inspect, context] )
         | 
| 583 | 
            +
                object
         | 
| 584 | 
            +
              end
         | 
| 585 | 
            +
              
         | 
| 586 | 
            +
              # Starts a new benchmark timer, with optional label.  Benchmark results will be shown 
         | 
| 587 | 
            +
              # at top of application response body.
         | 
| 588 | 
            +
              def bench(label = nil, context = Kernel.caller[0])
         | 
| 589 | 
            +
                stop_benchmark(context)
         | 
| 590 | 
            +
                @benchmarks.push(
         | 
| 591 | 
            +
                  :label => label,
         | 
| 592 | 
            +
                  :start_time => Time.now,
         | 
| 593 | 
            +
                  :start_context => context
         | 
| 594 | 
            +
                )
         | 
| 595 | 
            +
              end
         | 
| 596 | 
            +
              
         | 
| 597 | 
            +
              # Stops last benchmark timer, if still running.
         | 
| 598 | 
            +
              def stop_benchmark(end_context = nil)
         | 
| 599 | 
            +
                if @benchmarks[-1] && !@benchmarks[-1][:end_time]
         | 
| 600 | 
            +
                  @benchmarks[-1][:end_time] = Time.now
         | 
| 601 | 
            +
                  @benchmarks[-1][:end_context] = end_context
         | 
| 602 | 
            +
                end
         | 
| 494 603 | 
             
              end
         | 
| 495 604 |  | 
| 496 605 | 
             
              # Sets up request-specified variables based in request information received from Rack.
         | 
| 497 606 | 
             
              def get_request(env)
         | 
| 498 607 | 
             
                @request = Rack::Request.new(env)
         | 
| 499 608 |  | 
| 500 | 
            -
                @app_host =  | 
| 501 | 
            -
                @app_uri =  | 
| 609 | 
            +
                @app_host = @@options[:app_host] ? ('http://' + @@options[:app_host]) : @request.server rescue ''
         | 
| 610 | 
            +
                @app_uri = @@options[:app_uri] || @request.script_name || ''
         | 
| 502 611 | 
             
                @app_url =  @app_host + @app_uri
         | 
| 503 612 |  | 
| 504 613 | 
             
                @path = @request.path_info || '/'
         | 
| 505 614 | 
             
                @params = @request.params
         | 
| 506 615 |  | 
| 507 | 
            -
                @host ||= @request.host
         | 
| 616 | 
            +
                @host ||= @request.host rescue ''
         | 
| 508 617 | 
             
                @protocol = env['HTTPS'] == 'on' ? 'https' : 'http'
         | 
| 509 618 | 
             
              end
         | 
| 510 619 |  | 
| 511 | 
            -
              # Returns  | 
| 512 | 
            -
               | 
| 513 | 
            -
             | 
| 514 | 
            -
                unless db.table_exists?(:evolution_number)
         | 
| 515 | 
            -
                  db.create_table :evolution_number do
         | 
| 516 | 
            -
                    column :version, :integer, :null=> false
         | 
| 517 | 
            -
                  end
         | 
| 518 | 
            -
                  db[:evolution_number].insert(:version => 0)
         | 
| 519 | 
            -
                end
         | 
| 520 | 
            -
                db[:evolution_number]
         | 
| 521 | 
            -
              end
         | 
| 522 | 
            -
              
         | 
| 523 | 
            -
              # Returns app's current evolution number.
         | 
| 524 | 
            -
              def evolution_number
         | 
| 525 | 
            -
                evolution_number_table.first.version
         | 
| 526 | 
            -
              end
         | 
| 527 | 
            -
              
         | 
| 528 | 
            -
              # Sets app's current evolution number.
         | 
| 529 | 
            -
              def evolution_number=(version)
         | 
| 530 | 
            -
                evolution_number_table.update(:version => version)
         | 
| 531 | 
            -
              end
         | 
| 532 | 
            -
              
         | 
| 533 | 
            -
              # Check whether there exists a file in evolution_dir whose number is greater than app's
         | 
| 534 | 
            -
              # current evolution number. If so, raise an error to indicate need to apply new evolutions.
         | 
| 535 | 
            -
              def check_evolution_number
         | 
| 536 | 
            -
                version = evolution_number
         | 
| 537 | 
            -
                if directory_exists?(@evolution_dir) &&
         | 
| 538 | 
            -
                    Dir.entries(@evolution_dir).select { |f| f =~ /\A0*#{version+1}_/ }.size > 0
         | 
| 539 | 
            -
                  raise "current evolution #{version} is outdated; apply evolutions or update evolution number"
         | 
| 540 | 
            -
                end
         | 
| 620 | 
            +
              # Returns URL/URI of app root (corresponding to top level of action_dir).
         | 
| 621 | 
            +
              def app_url(suffix = nil)
         | 
| 622 | 
            +
                suffix ? @app_url + suffix : @app_url
         | 
| 541 623 | 
             
              end
         | 
| 542 624 |  | 
| 543 625 | 
             
              # Loads session from session store (specified by session_class).
         | 
| 544 626 | 
             
              def setup_session
         | 
| 545 627 | 
             
                @login = {}
         | 
| 546 | 
            -
                @session =  | 
| 547 | 
            -
                  session =  | 
| 548 | 
            -
                  @ | 
| 628 | 
            +
                @session = @@session_class ? begin
         | 
| 629 | 
            +
                  session = @@session_class.persist(@request.cookies[@@cookie_name])
         | 
| 630 | 
            +
                  @session_fingerprint = Marshal.dump(session.data).hash
         | 
| 549 631 |  | 
| 550 632 | 
             
                  cookie_vars = {
         | 
| 551 633 | 
             
                    :value => session.values[:session_id],
         | 
| 552 | 
            -
                    :path =>  | 
| 553 | 
            -
                    :domain =>  | 
| 634 | 
            +
                    :path => @@options[:cookie_path] || @app_uri,
         | 
| 635 | 
            +
                    :domain => @@options[:cookie_domain] || @request.host
         | 
| 554 636 | 
             
                  }
         | 
| 555 | 
            -
                  cookie_vars[:expires] = Time.now +  | 
| 637 | 
            +
                  cookie_vars[:expires] = Time.now + @@options[:cookie_lifespan] if @@options[:cookie_lifespan]
         | 
| 556 638 |  | 
| 557 639 | 
             
                  # set_cookie here or at render time
         | 
| 558 | 
            -
                  @response.set_cookie  | 
| 559 | 
            -
                  @login.merge!(session[:login]) if session[:login]
         | 
| 640 | 
            +
                  @response.set_cookie @@cookie_name, cookie_vars
         | 
| 641 | 
            +
                  @login.merge!(session[:login]) if session[:login] && session[:login][:expires_at] &&
         | 
| 642 | 
            +
                    session[:login][:expires_at] > Time.now
         | 
| 560 643 |  | 
| 561 644 | 
             
                  session
         | 
| 562 645 | 
             
                end : {}
         | 
| @@ -564,11 +647,7 @@ class Kiss | |
| 564 647 |  | 
| 565 648 | 
             
              # Saves session to session store, if session data has changed since load.
         | 
| 566 649 | 
             
              def finalize_session
         | 
| 567 | 
            -
                @session.save if @ | 
| 568 | 
            -
              end
         | 
| 569 | 
            -
              
         | 
| 570 | 
            -
              def session
         | 
| 571 | 
            -
                @session
         | 
| 650 | 
            +
                @session.save if @session_fingerprint != Marshal.dump(@session.data).hash
         | 
| 572 651 | 
             
              end
         | 
| 573 652 |  | 
| 574 653 | 
             
              ##### LOGIN SESSION #####
         | 
| @@ -609,16 +688,16 @@ class Kiss | |
| 609 688 |  | 
| 610 689 | 
             
              # Calls login action's load_from_session method to populate request login hash.
         | 
| 611 690 | 
             
              def load_from_login_session
         | 
| 612 | 
            -
                klass = action_class('login')
         | 
| 691 | 
            +
                klass = action_class('/login')
         | 
| 613 692 | 
             
                raise 'load_from_login_session called, but no login action found' unless klass
         | 
| 614 693 |  | 
| 615 | 
            -
                action_handler = klass.new
         | 
| 694 | 
            +
                action_handler = klass.new(self)
         | 
| 616 695 | 
             
                action_handler.load_from_session
         | 
| 617 696 | 
             
              end
         | 
| 618 697 |  | 
| 619 698 | 
             
              # Returns path to login action.
         | 
| 620 699 | 
             
              def login_path
         | 
| 621 | 
            -
                 | 
| 700 | 
            +
                @@action_dir + '/login.rb'
         | 
| 622 701 | 
             
              end
         | 
| 623 702 |  | 
| 624 703 | 
             
              # If valid login session exists, loads login action to populate request login hash data.
         | 
| @@ -627,7 +706,7 @@ class Kiss | |
| 627 706 | 
             
                if login_session_valid?
         | 
| 628 707 | 
             
                  load_from_login_session
         | 
| 629 708 | 
             
                else
         | 
| 630 | 
            -
                  klass = action_class('login')
         | 
| 709 | 
            +
                  klass = action_class('/login')
         | 
| 631 710 | 
             
                  raise 'authenticate called, but no login action found' unless klass
         | 
| 632 711 | 
             
                  old_extension = @extension
         | 
| 633 712 | 
             
                  @extension = 'rhtml'
         | 
| @@ -642,18 +721,14 @@ class Kiss | |
| 642 721 |  | 
| 643 722 | 
             
              ##### ACTION METHODS #####
         | 
| 644 723 |  | 
| 645 | 
            -
              def action_dir
         | 
| 646 | 
            -
                @action_dir
         | 
| 647 | 
            -
              end
         | 
| 648 | 
            -
              
         | 
| 649 724 | 
             
              # Creates and caches anonymous class with which to invoke specified (or current) action.
         | 
| 650 725 | 
             
              def action_class(action = @action)
         | 
| 651 | 
            -
                action_path =  | 
| 726 | 
            +
                action_path = @@action_dir + action.to_s + '.rb'
         | 
| 652 727 | 
             
                return nil unless action_path.is_a?(String) && action_path.length > 0
         | 
| 653 728 |  | 
| 654 729 | 
             
                file_cache(action_path) do |src|
         | 
| 655 730 | 
             
                  # create new action class, subclass of shared action parent class
         | 
| 656 | 
            -
                  klass = Class.new( | 
| 731 | 
            +
                  klass = Class.new(@@action_root_class)
         | 
| 657 732 | 
             
                  klass.class_eval(src,action_path) if src
         | 
| 658 733 | 
             
                  klass
         | 
| 659 734 | 
             
                end
         | 
| @@ -664,17 +739,17 @@ class Kiss | |
| 664 739 | 
             
                @action_subdir = ''
         | 
| 665 740 | 
             
                @action = nil
         | 
| 666 741 |  | 
| 667 | 
            -
                redirect_url( | 
| 742 | 
            +
                redirect_url(app_url + '/') if path == ''
         | 
| 668 743 |  | 
| 669 | 
            -
                path +=  | 
| 744 | 
            +
                path += @@default_action if path =~ /\/\Z/
         | 
| 670 745 |  | 
| 671 746 | 
             
                parts = path.sub(/^\/*/,'').split('/')
         | 
| 672 747 |  | 
| 673 748 | 
             
                while part = parts.shift
         | 
| 674 749 | 
             
                  raise 'bad action' if part !~ /\A[a-z0-9][\w\-\.]*\Z/i
         | 
| 675 750 |  | 
| 676 | 
            -
                  test_path =  | 
| 677 | 
            -
                  if directory_exists?(test_path)
         | 
| 751 | 
            +
                  test_path = @@action_dir + @action_subdir + '/' + part
         | 
| 752 | 
            +
                  if Kiss.directory_exists?(test_path)
         | 
| 678 753 | 
             
                    @action_subdir += '/' + part
         | 
| 679 754 | 
             
                    next
         | 
| 680 755 | 
             
                  end
         | 
| @@ -692,9 +767,9 @@ class Kiss | |
| 692 767 |  | 
| 693 768 | 
             
                # if no action, must have traversed all parts to a directory
         | 
| 694 769 | 
             
                # add a trailing slash and try again
         | 
| 695 | 
            -
                redirect_url( | 
| 770 | 
            +
                redirect_url(app_url + '/' + path + '/') unless @action
         | 
| 696 771 |  | 
| 697 | 
            -
                @action_path =  | 
| 772 | 
            +
                @action_path = @@action_dir + '/' + @action + '.rb'
         | 
| 698 773 |  | 
| 699 774 | 
             
                # keep rest of path_info in args
         | 
| 700 775 | 
             
                @args = parts
         | 
| @@ -703,28 +778,34 @@ class Kiss | |
| 703 778 | 
             
              # Processes specified (or current) action, by instantiating its anonymous
         | 
| 704 779 | 
             
              # action class and invoking `call' method on the instance.
         | 
| 705 780 | 
             
              def process(klass = action_class, action_path = @action_path)
         | 
| 706 | 
            -
                action_handler = klass.new
         | 
| 781 | 
            +
                action_handler = klass.new(self)
         | 
| 707 782 | 
             
                action_handler.call
         | 
| 708 783 |  | 
| 709 784 | 
             
                # return handler to follow with render
         | 
| 710 785 | 
             
                action_handler
         | 
| 711 786 | 
             
              end
         | 
| 712 787 |  | 
| 713 | 
            -
               | 
| 788 | 
            +
              # Outputs a Kiss::StaticFile object as response to Rack.
         | 
| 789 | 
            +
              # Used to return static files efficiently.
         | 
| 790 | 
            +
              def send_file(path, mime_type = nil)
         | 
| 791 | 
            +
                @response = Kiss::StaticFile.new(path,mime_type)
         | 
| 792 | 
            +
                
         | 
| 793 | 
            +
                throw :kiss_action_done
         | 
| 794 | 
            +
              end
         | 
| 714 795 |  | 
| 715 | 
            -
              # Prepares Rack::Response object to  | 
| 796 | 
            +
              # Prepares Rack::Response object to return application response to Rack.
         | 
| 716 797 | 
             
              # Raises Kiss::ActionDone exception to bypass caller stack and return directly 
         | 
| 717 798 | 
             
              # to Kiss#call.
         | 
| 718 799 | 
             
              def send_response(output = '',options = {})
         | 
| 719 | 
            -
                content_type = options[:content_type] ||  | 
| 720 | 
            -
                  (extension ?  | 
| 800 | 
            +
                content_type = options[:content_type] || 
         | 
| 801 | 
            +
                  (extension ? Kiss.mime_type(extension) : nil)
         | 
| 721 802 | 
             
                document_encoding ||= 'utf-8'
         | 
| 722 803 |  | 
| 723 804 | 
             
                @response['Content-Type'] = "#{content_type}; #{document_encoding}" if content_type
         | 
| 724 805 | 
             
                @response['Content-Length'] = output.length.to_s
         | 
| 725 806 | 
             
                @response.body = output
         | 
| 726 807 |  | 
| 727 | 
            -
                 | 
| 808 | 
            +
                throw :kiss_action_done
         | 
| 728 809 |  | 
| 729 810 | 
             
                # back to Kiss#call, which finalizes session and returns @response
         | 
| 730 811 | 
             
                # (throws exception if no @response set)
         | 
| @@ -738,7 +819,25 @@ class Kiss | |
| 738 819 | 
             
              end
         | 
| 739 820 |  | 
| 740 821 | 
             
              # Returns new Kiss::Mailer object using specified options.
         | 
| 741 | 
            -
              def new_email( | 
| 742 | 
            -
                Kiss::Mailer.new( | 
| 822 | 
            +
              def new_email(options = {})
         | 
| 823 | 
            +
                Kiss::Mailer.new(options)
         | 
| 824 | 
            +
              end
         | 
| 825 | 
            +
              
         | 
| 826 | 
            +
              # Kiss Model cache, used to invoke and store Kiss database models.
         | 
| 827 | 
            +
              #
         | 
| 828 | 
            +
              # Example:
         | 
| 829 | 
            +
              # models[:users] : database model for `users' table
         | 
| 830 | 
            +
              def dbm
         | 
| 831 | 
            +
                @dbm ||= Kiss::ModelCache.new(self)
         | 
| 832 | 
            +
              end
         | 
| 833 | 
            +
              alias_method :models, :dbm
         | 
| 834 | 
            +
              
         | 
| 835 | 
            +
              def h(*args)
         | 
| 836 | 
            +
                self.class.h(*args)
         | 
| 837 | 
            +
              end
         | 
| 838 | 
            +
              
         | 
| 839 | 
            +
              # Adds data to be displayed in "Cache" section of Kiss exception reports.
         | 
| 840 | 
            +
              def set_exception_cache(data)
         | 
| 841 | 
            +
                @exception_cache.merge!(data)
         | 
| 743 842 | 
             
              end
         | 
| 744 843 | 
             
            end
         |