kiss 0.9.4 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.4
1
+ 1.0
@@ -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
- attr_reader :action, :action_subdir, :action_path, :extension, :host, :template_dir,
46
- :email_template_dir, :upload_dir, :evolution_dir, :asset_dir, :public_dir, :params, :args, :db,
47
- :login, :request, :environment
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
- @@default_action = 'index'
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
- @@cookie_name = 'Kiss'
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
- # Creates a new Kiss instance and runs it.
66
- def self.run(options = {})
67
- new(options).run
68
- end
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
- return sprintf("%04d-%02d-%02d",year,month.to_i,day.to_i)
94
- end
95
-
96
- # Returns regexp and error message for validating specified format.
97
- # Will likely be rewritten in a future version of Kiss.
98
- def self.format_details(format)
99
- if format.is_a?(Regexp)
100
- return format, 'invalid value'
101
- end
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
- case format
104
- when :integer
105
- return /\A\-?\d+\Z/,
106
- 'must be an integer'
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
- when :integer_positive, :positive_integer, :id
109
- return /\A\d*[1-9]\d*\Z/,
110
- 'must be a positive integer'
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
- when :decimal
121
- return /\A\-?(\d+(\.\d*)?|\.\d+)\Z/,
122
- 'must be a decimal number'
141
+ use(builder_option,*builder_args)
142
+ end
123
143
 
124
- when :alphanum
125
- return /\A[a-z0-9]+\Z/,
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
- end
149
- end
150
-
151
- # Validates value against specified format.
152
- # If required is true, value must contain a non-whitespace character.
153
- # If required is false, value need not match format if and only if value contains only whitespace.
154
- def self.validate_value(value,format,required = false)
155
- if required && (value !~ /\S/)
156
- # value required
157
- return 'required'
158
- elsif format && (value =~ /\S/)
159
- regexp,error = format_details(format)
160
- return error unless value =~ regexp
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
- return nil
164
- end
165
-
166
- # Generates string of random text of the specified length.
167
- def self.random_text(length)
168
- chars = ('A'..'Z').to_a + ('0'..'9').to_a # array
169
-
170
- text = ''
171
- size = chars.size
172
- length.times { text += chars[rand(size)] }
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
- text
175
- end
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
- @files_cached_this_request[path] = true
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
- if !File.file?(path)
247
- raise fnf_exception_class, "#{fnf_file_type} file missing: '#{path}'" if fnf_file_type
248
-
249
- @file_cache[path] = nil
250
- contents = nil
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
- if gem_dirs = config_options.delete(:gem_dirs)
279
- @gem_dirs.push( gem_dirs )
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
- if require_libs = config_options.delete(:require)
282
- @require.push( require_libs )
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
- @options.merge!( config_options )
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
- # directories
306
- script_dir = $0.sub(/[^\/]+\Z/,'')
307
- script_dir = '' if script_dir == './'
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
- @config_dir = loader_options[:config_dir] || 'config'
218
+ @@asset_dir = @@public_dir = @@options[:asset_dir] || @@options[:public_dir] || 'public_html'
312
219
 
313
- # get environment name from config/environment
314
- if (@environment.nil?) && File.file?(env_file = @config_dir+'/environment')
315
- @environment = File.read(env_file).sub(/\s+\Z/,'')
316
- end
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
- # init options
319
- @options = {}
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
- # common (shared) config
325
- if (File.file?(config_file = @config_dir+'/common.yml'))
326
- merge_options( YAML::load(File.read(config_file)) )
327
- end
328
- # environment config
329
- if (File.file?(config_file = "#{@config_dir}/environments/#{@environment}.yml"))
330
- merge_options( YAML::load(File.read(config_file)) )
331
- end
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
- merge_options( loader_options )
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
- # set class vars from options
336
- @action_dir = @options[:action_dir] || 'actions'
337
- @template_dir = @options[:template_dir] ? @options[:template_dir] : @action_dir
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
- @asset_dir = @public_dir = @options[:asset_dir] || @options[:public_dir] || 'public_html'
247
+ # include lib dirs
248
+ $LOAD_PATH.unshift(*( @@lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
340
249
 
341
- @model_dir = @options[:model_dir] || 'models'
342
- @evolution_dir = @options[:evolution_dir] || 'evolutions'
250
+ # add gem dir to rubygems search path
251
+ Gem.path.unshift(*( @@gem_dirs.flatten.select {|dir| File.directory?(dir) } ))
343
252
 
344
- @email_template_dir = @options[:email_template_dir] || 'email_templates'
345
- @upload_dir = @options[:upload_dir] || 'uploads'
253
+ # require libs
254
+ @@require.flatten.each {|lib| require lib }
346
255
 
347
- @cookie_name = @options[:cookie_name] || 'Kiss'
348
- @default_action = @options[:default_action] || @@default_action
349
- @action_root_class = @options[:action_class] || Class.new(Kiss::Action)
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
- # public_uri: uri of requests to serve from public_dir
352
- @asset_uri = @public_uri = @options[:asset_uri] || @options[:public_uri]
353
- @rack_file = Rack::File.new(@asset_dir) if @asset_uri
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
- # include lib dirs
356
- $LOAD_PATH.unshift(*( @lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
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
- # add gem dir to rubygems search path
359
- Gem.path.unshift(*( @gem_dirs.flatten.select {|dir| File.directory?(dir) } ))
281
+ # setup session storage, if session class specified in config
282
+ @@session_class.setup_storage(self) if @@session_class
360
283
 
361
- # require libs
362
- @require.flatten.each {|lib| require lib }
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
- # session class
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
- # load extensions to action class
370
- action_extension_path = @action_dir + '/_action.rb'
371
- if File.file?(action_extension_path)
372
- @action_root_class.class_eval(File.read(action_extension_path),action_extension_path) rescue nil
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
- alias_method :execute_old, :execute
392
- def execute(sql, *args, &block)
393
- @@query = sql
394
- execute_old(sql, *args, &block)
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
- # setup session storage, if session class specified in config
409
- @session_class.setup_storage(self) if @session_class
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
- # prepare authenticate_exclude
412
- if @options[:authenticate_all]
413
- if @options[:authenticate_exclude].is_a?(Array)
414
- @options[:authenticate_exclude] = @options[:authenticate_exclude].map do |action|
415
- action = '/'+action unless action =~ /\A\//
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+)/) {'&nbsp;' * $1.length}
418
346
  else
419
- @options[:authenticate_exclude] = []
347
+ Rack::Utils.escape_html(obj.inspect)
420
348
  end
421
349
  end
422
350
 
423
- self
424
- end
425
-
426
- # Sets up Rack builder options and begins Rack execution.
427
- def run(options = {})
428
- merge_options(options)
429
-
430
- app = self
431
- builder_options = @options[:rack_builder] || []
432
- rack = Rack::Builder.new do
433
- builder_options.each do |builder_option|
434
- if builder_option.is_a?(Array)
435
- builder_args = builder_option
436
- builder_option = builder_args.shift
437
- else
438
- builder_args = []
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
- unless builder_option.is_a?(Class)
442
- builder_option = Rack.const_get(builder_option.to_s)
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
- handler = @options[:rack_handler] || Rack::Handler::WEBrick
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
- # Processes and responds to requests received via Rack.
459
- # Returns Rack::Response object.
460
- def call(env)
461
- clear_exception_cache
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
- if (env["PATH_INFO"] == '/favicon.ico') ||
464
- (@asset_uri && env["PATH_INFO"].sub!(Regexp.new("\\A#{@asset_uri}"),''))
465
- return @rack_file.call(env)
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
- check_evolution_number if @db
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
- get_request(env)
473
-
474
- @response = Rack::Response.new
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
- begin
477
- parse_action_path(@path)
523
+ # catch and report exceptions in this block
478
524
 
479
- setup_session
480
- if login_session_valid?
481
- load_from_login_session
482
- elsif @options[:authenticate_all]
483
- if (!@options[:authenticate_exclude].is_a?(Array) ||
484
- @options[:authenticate_exclude].select {|action| action == @action}.size == 0)
485
- authenticate
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
- process.render
489
- rescue Kiss::ActionDone
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
- @response.finish
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 = @options[:app_host] ? ('http://' + @options[:app_host]) : @request.server
501
- @app_uri = @options[:app_uri] || @request.script_name || ''
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 Sequel dataset to evolution_number table, which specifies app's current evolution number.
512
- # Creates evolution_number table if it does not exist.
513
- def evolution_number_table
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 = @session_class ? begin
547
- session = @session_class.persist(@request.cookies[@cookie_name])
548
- @_fingerprint = Marshal.dump(session.data).hash
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 => @options[:cookie_path] || @app_uri,
553
- :domain => @options[:cookie_domain] || @request.host
634
+ :path => @@options[:cookie_path] || @app_uri,
635
+ :domain => @@options[:cookie_domain] || @request.host
554
636
  }
555
- cookie_vars[:expires] = Time.now + @options[:cookie_lifespan] if @options[:cookie_lifespan]
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 @cookie_name, cookie_vars
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 @_fingerprint != Marshal.dump(@session.data).hash
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
- @action_dir + '/login.rb'
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 = @action_dir + '/' + action.to_s + '.rb'
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(@action_root_class)
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(app + '/') if path == ''
742
+ redirect_url(app_url + '/') if path == ''
668
743
 
669
- path += @default_action if path =~ /\/\Z/
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 = @action_dir + @action_subdir + '/' + part
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(app + '/' + path + '/') unless @action
770
+ redirect_url(app_url + '/' + path + '/') unless @action
696
771
 
697
- @action_path = @action_dir + '/' + @action + '.rb'
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
- ##### RACK OUTPUT METHODS #####
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 be returned to Rack.
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] || @content_type ||
720
- (extension ? Rack::File::MIME_TYPES[extension] || @@mime_types[extension] : nil)
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
- raise Kiss::ActionDone
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(*options)
742
- Kiss::Mailer.new(*options)
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