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
|