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 +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
|