kiss 1.0.4 → 1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +9 -6
- data/VERSION +1 -1
- data/bin/kiss +75 -0
- data/data/scaffold.tgz +0 -0
- data/lib/kiss.rb +328 -337
- data/lib/kiss/action.rb +133 -47
- data/lib/kiss/bench.rb +2 -3
- data/lib/kiss/controller_accessors.rb +24 -68
- data/lib/kiss/debug.rb +4 -5
- data/lib/kiss/exception_report.rb +10 -6
- data/lib/kiss/hacks.rb +7 -3
- data/lib/kiss/iterator.rb +6 -6
- data/lib/kiss/login.rb +45 -0
- data/lib/kiss/mailer.rb +20 -2
- data/lib/kiss/model.rb +27 -31
- data/lib/kiss/rack/bench.rb +5 -0
- data/lib/kiss/rack/errors_ok.rb +19 -0
- data/lib/kiss/rack/facebook.rb +1 -0
- data/lib/kiss/rack/recorder.rb +22 -0
- data/lib/kiss/rack/show_debug.rb +1 -1
- data/lib/kiss/rack/show_exceptions.rb +1 -1
- data/lib/kiss/template_methods.rb +71 -30
- metadata +16 -8
data/Rakefile
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
|
2
|
+
require 'rubygems/builder'
|
3
3
|
require 'rake/gempackagetask'
|
4
4
|
|
5
5
|
spec = Gem::Specification.new do |s|
|
@@ -11,16 +11,19 @@ spec = Gem::Specification.new do |s|
|
|
11
11
|
s.homepage = 'http://www.rubykiss.org'
|
12
12
|
s.rubyforge_project = 'kiss'
|
13
13
|
s.summary = 'An MVC web application framework using Rack, Sequel, and Erubis.'
|
14
|
-
s.files = %w( Rakefile VERSION LICENSE ) + Dir['{lib,tests}/**/*']
|
14
|
+
s.files = %w( Rakefile VERSION LICENSE ) + Dir['{bin,data,lib,tests}/**/*']
|
15
15
|
s.require_path = 'lib'
|
16
16
|
s.test_files = Dir.glob('tests/*.rb')
|
17
17
|
s.has_rdoc = true
|
18
|
+
s.executables = %w( kiss )
|
18
19
|
|
19
|
-
s.rdoc_options
|
20
|
-
|
20
|
+
s.rdoc_options.push(
|
21
|
+
'--main', 'Kiss',
|
22
|
+
'--title', 'Kiss: Ruby MVC Web Framework -- RDoc Reference Documentation'
|
23
|
+
)
|
21
24
|
|
22
|
-
s.add_dependency('rack')
|
23
|
-
s.add_dependency('sequel','>=2.
|
25
|
+
s.add_dependency('rack','=0.4.0')
|
26
|
+
s.add_dependency('sequel','>=2.5.0')
|
24
27
|
s.add_dependency('erubis')
|
25
28
|
end
|
26
29
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.1
|
data/bin/kiss
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
|
4
|
+
def create
|
5
|
+
project_name = ARGV[0]
|
6
|
+
display_usage('create') unless project_name =~ /\S/
|
7
|
+
|
8
|
+
path = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))),'data','scaffold.tgz')
|
9
|
+
|
10
|
+
`mkdir #{project_name}`
|
11
|
+
|
12
|
+
print "Expanding '#{path}' to '#{project_name}'..."
|
13
|
+
|
14
|
+
`cd '#{ARGV[0]}'; tar xvfz '#{path}'`
|
15
|
+
|
16
|
+
puts 'done.'
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
project_name = ARGV[0]
|
21
|
+
project_name = '.' unless project_name =~ /\S/
|
22
|
+
|
23
|
+
require 'rubygems'
|
24
|
+
require 'kiss'
|
25
|
+
|
26
|
+
Kiss.run( :project_dir => project_name )
|
27
|
+
end
|
28
|
+
|
29
|
+
def display_usage(command = nil)
|
30
|
+
case command
|
31
|
+
when 'create'
|
32
|
+
puts <<-EOT
|
33
|
+
usage: kiss create <app_directory>
|
34
|
+
Creates a skeleton for a new Kiss application.
|
35
|
+
EOT
|
36
|
+
when 'run'
|
37
|
+
puts <<-EOT
|
38
|
+
usage: kiss run [app_directory]
|
39
|
+
Runs a Kiss application located within app_directory.
|
40
|
+
|
41
|
+
The app_directory path may be omitted when running this command from
|
42
|
+
within the top-level directory of a Kiss application.
|
43
|
+
EOT
|
44
|
+
else
|
45
|
+
puts <<-EOT
|
46
|
+
usage: kiss <command> [arguments]
|
47
|
+
Utility to support and facilitate the Kiss web framework.
|
48
|
+
|
49
|
+
Commands:
|
50
|
+
create - Create a new Kiss application project.
|
51
|
+
run - Run a Kiss application.
|
52
|
+
help - Display usage info.
|
53
|
+
|
54
|
+
For help on any command above, type:
|
55
|
+
kiss help <command>
|
56
|
+
EOT
|
57
|
+
end
|
58
|
+
puts
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# main
|
64
|
+
|
65
|
+
case (command = ARGV.shift)
|
66
|
+
when 'create'
|
67
|
+
create
|
68
|
+
when 'run'
|
69
|
+
run
|
70
|
+
when 'help'
|
71
|
+
display_usage(ARGV[0])
|
72
|
+
else
|
73
|
+
display_usage
|
74
|
+
end
|
75
|
+
exit
|
data/data/scaffold.tgz
ADDED
Binary file
|
data/lib/kiss.rb
CHANGED
@@ -22,10 +22,12 @@ autoload :Sequel, 'sequel'
|
|
22
22
|
|
23
23
|
module Rack
|
24
24
|
autoload :Bench, 'kiss/rack/bench'
|
25
|
+
autoload :ErrorsOK, 'kiss/rack/errors_ok'
|
25
26
|
autoload :EmailErrors, 'kiss/rack/email_errors'
|
26
27
|
autoload :Facebook, 'kiss/rack/facebook'
|
27
28
|
autoload :FileNotFound, 'kiss/rack/file_not_found'
|
28
29
|
autoload :LogExceptions, 'kiss/rack/log_exceptions'
|
30
|
+
autoload :Recorder, 'kiss/rack/recorder'
|
29
31
|
autoload :ShowDebug, 'kiss/rack/show_debug'
|
30
32
|
autoload :ShowExceptions, 'kiss/rack/show_exceptions'
|
31
33
|
end
|
@@ -64,6 +66,7 @@ class Kiss
|
|
64
66
|
autoload :ExceptionReport, 'kiss/exception_report'
|
65
67
|
autoload :Mailer, 'kiss/mailer'
|
66
68
|
autoload :Iterator, 'kiss/iterator'
|
69
|
+
autoload :Login, 'kiss/login'
|
67
70
|
autoload :Form, 'kiss/form'
|
68
71
|
autoload :Format, 'kiss/format'
|
69
72
|
autoload :SequelSession, 'kiss/sequel_session'
|
@@ -88,10 +91,7 @@ class Kiss
|
|
88
91
|
:evolution_dir, :asset_dir, :public_dir, :environment, :options, :layout, :rack_file
|
89
92
|
|
90
93
|
# attributes below are request-specific
|
91
|
-
attr_reader :
|
92
|
-
:exception_cache
|
93
|
-
|
94
|
-
attr_accessor :last_sql
|
94
|
+
attr_reader :protocol, :host, :request, :exception_cache, :env
|
95
95
|
|
96
96
|
@@default_action = 'index'
|
97
97
|
@@default_cookie_name = 'Kiss'
|
@@ -133,12 +133,12 @@ class Kiss
|
|
133
133
|
unless builder_option.is_a?(Class)
|
134
134
|
builder_option = Rack.const_get(builder_option.to_s)
|
135
135
|
end
|
136
|
-
|
136
|
+
|
137
137
|
use(builder_option,*builder_args)
|
138
138
|
end
|
139
|
-
|
139
|
+
|
140
140
|
run app
|
141
|
-
end
|
141
|
+
end.to_app
|
142
142
|
|
143
143
|
handler = @@options[:rack_handler] || Rack::Handler::WEBrick
|
144
144
|
if !handler.is_a?(Class)
|
@@ -148,7 +148,7 @@ class Kiss
|
|
148
148
|
rescue StandardError, LoadError, SyntaxError => e
|
149
149
|
if @@options[:rack_handler] == :CGI
|
150
150
|
print "Content-type: text/html\n\n"
|
151
|
-
print Kiss::ExceptionReport.generate(e)
|
151
|
+
print $debug_messages.to_s + Kiss::ExceptionReport.generate(e)
|
152
152
|
else
|
153
153
|
print "Content-type: text/plain\n\n"
|
154
154
|
puts "exception:\n" + e.message
|
@@ -195,7 +195,7 @@ class Kiss
|
|
195
195
|
@@lib_dirs = ['lib']
|
196
196
|
@@gem_dirs = ['gems']
|
197
197
|
@@require = []
|
198
|
-
@@authenticate_exclude = ['/logout']
|
198
|
+
@@authenticate_exclude = ['/login','/logout']
|
199
199
|
|
200
200
|
# common (shared) config
|
201
201
|
if (File.file?(config_file = @@config_dir+'/common.yml'))
|
@@ -220,7 +220,7 @@ class Kiss
|
|
220
220
|
|
221
221
|
@@email_template_dir = @@options[:email_template_dir] || 'email_templates'
|
222
222
|
@@upload_dir = @@options[:upload_dir] || 'uploads'
|
223
|
-
|
223
|
+
|
224
224
|
@@cookie_name = @@options[:cookie_name] || @@default_cookie_name
|
225
225
|
@@default_action = @@options[:default_action] || @@default_action
|
226
226
|
@@action_root_class = @@options[:action_class] || Class.new(Kiss::Action)
|
@@ -231,14 +231,20 @@ class Kiss
|
|
231
231
|
# default layout
|
232
232
|
@@layout = @@options[:layout]
|
233
233
|
|
234
|
-
# public_uri: uri of requests to serve from public_dir
|
235
|
-
@@asset_uri = @@public_uri = @@options[:asset_uri] || @@options[:public_uri]
|
236
|
-
@@rack_file = Rack::File.new(@@asset_dir) if @@asset_uri
|
237
|
-
|
238
234
|
# app_url: URL of the app actions root
|
239
|
-
@@
|
235
|
+
@@protocol = ENV['HTTPS'] == 'on' ? 'https' : 'http'
|
236
|
+
@@app_host = @@options[:app_host] ? (@@protocol + '://' + @@options[:app_host]) : ''
|
240
237
|
@@app_uri = @@options[:app_uri] || ''
|
241
238
|
@@app_url = @@app_host + @@app_uri
|
239
|
+
|
240
|
+
# asset host: hostname of static assets
|
241
|
+
# (remove http:// prefix if present)
|
242
|
+
@@asset_host = @@options[:asset_host]
|
243
|
+
@@asset_host.sub!(/^http:\/\//,'') if @@asset_host
|
244
|
+
|
245
|
+
# public_uri: uri of requests to serve from public_dir
|
246
|
+
@@asset_uri = @@options[:asset_uri] || @@options[:public_uri] || ''
|
247
|
+
@@rack_file = Rack::File.new(@@asset_dir) if @@asset_uri
|
242
248
|
|
243
249
|
# include lib dirs
|
244
250
|
$LOAD_PATH.unshift(*( @@lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
|
@@ -250,16 +256,18 @@ class Kiss
|
|
250
256
|
@@require.flatten.each {|lib| require lib }
|
251
257
|
|
252
258
|
# session class
|
253
|
-
|
254
|
-
@@session_class = (@@options[:session_class].class == Class) ?
|
255
|
-
@@options[:session_class].to_const
|
256
|
-
|
259
|
+
@@session_class = @@options[:session_class] ?
|
260
|
+
(@@session_class = (@@options[:session_class].class == Class) ?
|
261
|
+
@@options[:session_class] : @@options[:session_class].to_const
|
262
|
+
) : nil
|
257
263
|
|
258
264
|
# load extensions to action class
|
259
265
|
action_extension_path = @@action_dir + '/_action.rb'
|
260
266
|
if File.file?(action_extension_path)
|
261
267
|
@@action_root_class.class_eval(File.read(action_extension_path),action_extension_path) rescue nil
|
262
268
|
end
|
269
|
+
|
270
|
+
@@action_classes = {}
|
263
271
|
|
264
272
|
# database
|
265
273
|
@@database_config = @@options[:database]
|
@@ -267,14 +275,7 @@ class Kiss
|
|
267
275
|
|
268
276
|
self
|
269
277
|
end
|
270
|
-
|
271
|
-
# Returns URL/URI of app's static assets (asset_host or public_uri).
|
272
|
-
def assets(suffix = nil)
|
273
|
-
@@pub ||= @@options[:asset_host]
|
274
|
-
suffix ? @@pub + '/' + suffix : @@pub
|
275
|
-
end
|
276
|
-
alias_method :pub, :assets
|
277
|
-
|
278
|
+
|
278
279
|
# Returns true if specified path is a directory.
|
279
280
|
# Cache result if file_cache_no_reload option is set; otherwise, always check filesystem.
|
280
281
|
def directory_exists?(dir)
|
@@ -313,14 +314,23 @@ class Kiss
|
|
313
314
|
filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
|
314
315
|
end
|
315
316
|
|
317
|
+
# prepend addition just inside the specified tag of document
|
318
|
+
def html_prepend(addition,document,tag)
|
319
|
+
document = document.body unless document.is_a?(String)
|
320
|
+
document.sub(/(\<#{tag}[^\>]*\>)/i, '\1'+addition)
|
321
|
+
end
|
322
|
+
|
316
323
|
# Returns string representation of object with HTML entities escaped.
|
317
|
-
def
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
+
def html_escape(obj)
|
325
|
+
Rack::Utils.escape_html(
|
326
|
+
obj.is_a?(String) ? obj : obj.inspect
|
327
|
+
).gsub(/^(\s+)/) {' ' * $1.length}
|
328
|
+
end
|
329
|
+
|
330
|
+
# Escapes string for use in URLs.
|
331
|
+
def url_escape(string)
|
332
|
+
# encode space to '+'; don't encode letters, numbers, periods
|
333
|
+
string.gsub(/([^A-Za-z0-9\.])/) { sprintf("%%%02X", $&.unpack("C")[0]) }
|
324
334
|
end
|
325
335
|
|
326
336
|
# Returns MIME type corresponding to passed-in extension.
|
@@ -391,34 +401,55 @@ class Kiss
|
|
391
401
|
|
392
402
|
# Given a file path, caches or returns the file's contents or the return value of
|
393
403
|
# the passed block applied to the file's contents.
|
394
|
-
# If file is not found,
|
395
|
-
def file_cache(path,
|
396
|
-
|
397
|
-
return @@file_cache[path]
|
398
|
-
end
|
404
|
+
# If file is not found, the file's contents are nil.
|
405
|
+
def file_cache(path, report_change = false)
|
406
|
+
raise 'nil file path cannot be cached' unless path
|
399
407
|
|
400
|
-
if
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
408
|
+
if @@file_cache.has_key?(path) && @@options[:file_cache_no_reload]
|
409
|
+
# already loaded this path, and no_reload option is on; don't re-cache
|
410
|
+
change = false
|
411
|
+
elsif !@@file_cache.has_key?(path)
|
412
|
+
# haven't loaded this path yet
|
413
|
+
change = true
|
414
|
+
if !File.file?(path)
|
415
|
+
# nil path, of file doesn't exist
|
416
|
+
@@file_cache_time[path] = nil
|
417
|
+
contents = nil
|
418
|
+
else
|
419
|
+
# file exists; mark cache time and read file
|
420
|
+
@@file_cache_time[path] = Time.now
|
421
|
+
contents = File.read(path)
|
422
|
+
end
|
405
423
|
else
|
406
|
-
#
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
424
|
+
# we've read this path before; may need to re-cache
|
425
|
+
if !File.file?(path)
|
426
|
+
if @@file_cache_time[path]
|
427
|
+
# file cached as existing but has been removed; update cache to show no file
|
428
|
+
change = true
|
429
|
+
@@file_cache_time[path] = nil
|
430
|
+
contents = nil
|
431
|
+
else
|
432
|
+
change = false
|
433
|
+
end
|
434
|
+
elsif !@@file_cache_time[path] ||
|
435
|
+
@@file_cache_time[path] < File.mtime(path) ||
|
436
|
+
( File.symlink?(path) && (@@file_cache_time[path] < File.lstat(path).mtime) )
|
437
|
+
# cache shows file missing, or file has been modified since cached
|
438
|
+
change = true
|
439
|
+
@@file_cache_time[path] = Time.now
|
440
|
+
contents = File.read(path)
|
441
|
+
else
|
442
|
+
# cached file is still current; don't re-cache
|
443
|
+
change = false
|
415
444
|
end
|
416
445
|
end
|
417
446
|
|
418
|
-
|
419
|
-
|
447
|
+
if change
|
448
|
+
@@file_cache[path] ||= block_given? ? yield(contents) : contents
|
420
449
|
end
|
421
|
-
|
450
|
+
|
451
|
+
report_change ? [@@file_cache[path], change] : @@file_cache[path]
|
452
|
+
end
|
422
453
|
end # end class methods
|
423
454
|
|
424
455
|
### Instance Methods
|
@@ -438,49 +469,61 @@ class Kiss
|
|
438
469
|
@files_cached_this_request = {}
|
439
470
|
end
|
440
471
|
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
472
|
+
def invoke_action(path,params,render_options = {})
|
473
|
+
action, action_handler = get_action_handler(path,params)
|
474
|
+
catch :kiss_action_done do
|
475
|
+
action_handler.expand_login
|
445
476
|
|
446
|
-
|
477
|
+
if @@options[:authenticate_all]
|
478
|
+
if (!@@authenticate_exclude.is_a?(Array) ||
|
479
|
+
@@authenticate_exclude.select {|a| a == action}.size == 0)
|
480
|
+
action_handler.authenticate
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
action_handler.call
|
485
|
+
action_handler.render(render_options)
|
486
|
+
end
|
487
|
+
action_handler
|
447
488
|
end
|
448
489
|
|
449
490
|
# Processes and responds to a request received via Rack. Should only be called once
|
450
491
|
# for each controller instance (i.e. each controller instance should handle at most
|
451
492
|
# one request, then be discarded). Returns array of response code, headers, and body.
|
452
493
|
def call(env)
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
)
|
457
|
-
|
458
|
-
|
494
|
+
@env = env
|
495
|
+
|
496
|
+
# if @@rack_file && (
|
497
|
+
# (env["PATH_INFO"] == '/favicon.ico') ||
|
498
|
+
# (env["PATH_INFO"].sub!(Regexp.new("\\A#{@@asset_uri}"),''))
|
499
|
+
# )
|
500
|
+
# return @@rack_file.call(env)
|
501
|
+
# end
|
459
502
|
|
460
503
|
# catch and report exceptions in this block
|
461
504
|
|
462
505
|
code, headers, body = begin
|
463
|
-
|
506
|
+
@request = Rack::Request.new(env)
|
464
507
|
@response = Rack::Response.new
|
508
|
+
|
509
|
+
@protocol, @app_host = (@request.server rescue '').split(/\:\/\//, 2)
|
510
|
+
@app_host = @@options[:app_host] if @@options[:app_host]
|
511
|
+
@app_uri = @@options[:app_uri] || @request.script_name || ''
|
512
|
+
|
513
|
+
@host ||= @request.host rescue ''
|
514
|
+
|
515
|
+
path = @request.path_info || '/'
|
516
|
+
params = @request.params
|
465
517
|
|
466
|
-
catch :
|
467
|
-
|
468
|
-
|
469
|
-
env['kiss.parsed_args'] = @args.inspect
|
470
|
-
|
471
|
-
redirect_url(app_url + '/') if @action == '/login'
|
518
|
+
catch :kiss_request_done do
|
519
|
+
action_handler = invoke_action(path,params)
|
520
|
+
extension = action_handler.extension
|
472
521
|
|
473
|
-
if
|
474
|
-
|
475
|
-
elsif @@options[:authenticate_all]
|
476
|
-
if (!@@authenticate_exclude.is_a?(Array) ||
|
477
|
-
@@authenticate_exclude.select {|action| action == @action}.size == 0)
|
478
|
-
authenticate
|
479
|
-
end
|
522
|
+
if content_type = options[:content_type] || (extension ? Kiss.mime_type(extension) : nil)
|
523
|
+
@response['Content-Type'] = "#{content_type}; #{options[:document_encoding] || 'utf-8'}"
|
480
524
|
end
|
481
525
|
|
482
|
-
|
483
|
-
process.render
|
526
|
+
send_response(action_handler.output, action_handler.output_options)
|
484
527
|
end
|
485
528
|
finalize_session if @session
|
486
529
|
|
@@ -511,56 +554,159 @@ class Kiss
|
|
511
554
|
headers['Content-Length'] = body.length.to_s
|
512
555
|
end
|
513
556
|
|
514
|
-
|
557
|
+
if @db
|
558
|
+
@db.kiss_controller = nil
|
559
|
+
@@database_pool.push(@db)
|
560
|
+
end
|
515
561
|
|
516
562
|
[code,headers,body]
|
517
563
|
end
|
518
564
|
|
519
|
-
# Adds debug message to inspect object. Debug messages will be shown at top of
|
520
|
-
# application response body.
|
521
|
-
def debug(object, context = Kernel.caller[0])
|
522
|
-
@debug_messages.push( [object.inspect, context] )
|
523
|
-
object
|
524
|
-
end
|
525
565
|
|
526
|
-
|
527
|
-
# at top of application response body.
|
528
|
-
def bench(label = nil, context = Kernel.caller[0])
|
529
|
-
stop_benchmark(context)
|
530
|
-
@benchmarks.push(
|
531
|
-
:label => label,
|
532
|
-
:start_time => Time.now,
|
533
|
-
:start_context => context
|
534
|
-
)
|
535
|
-
end
|
566
|
+
##### ACTION METHODS #####
|
536
567
|
|
537
|
-
#
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
568
|
+
# Parses request URI to determine action path and arguments, then
|
569
|
+
# instantiates action class to create action handler.
|
570
|
+
def get_action_handler(path,params)
|
571
|
+
action_subdir = '/'
|
572
|
+
action_uri = ''
|
573
|
+
action = nil
|
574
|
+
args = []
|
575
|
+
|
576
|
+
redirect_url(app_url + '/') if path == ''
|
577
|
+
|
578
|
+
path += @@default_action if path =~ /\/\Z/
|
579
|
+
|
580
|
+
parts = path.sub(/^\/*/,'').split('/')
|
581
|
+
|
582
|
+
action_class = Kiss::Action.get_subclass('/', @@action_dir + '/_action.rb', self) ||
|
583
|
+
Kiss::Action
|
584
|
+
|
585
|
+
while part = parts.shift
|
586
|
+
action_uri += '/' + part
|
587
|
+
|
588
|
+
if part =~ /\A(\d+)\Z/ || part =~ /\A\=([\w\.\-]+)\Z/
|
589
|
+
args << $1
|
590
|
+
next
|
591
|
+
end
|
592
|
+
|
593
|
+
raise 'bad action path' if part !~ /\A[a-z][\w\-\.]*\Z/i
|
594
|
+
|
595
|
+
test_path = @@action_dir + action_subdir + part
|
596
|
+
if Kiss.directory_exists?(test_path)
|
597
|
+
action_subdir += part + '/'
|
598
|
+
action_class = action_class.get_subclass(
|
599
|
+
part + '/',
|
600
|
+
@@action_dir + action_subdir + '_action.rb',
|
601
|
+
self
|
602
|
+
)
|
603
|
+
next
|
604
|
+
end
|
605
|
+
|
606
|
+
if part =~ /\A(.+)\.(\w+)\Z/
|
607
|
+
extension = $2
|
608
|
+
part = $1
|
609
|
+
else
|
610
|
+
extension = 'rhtml'
|
611
|
+
end
|
612
|
+
|
613
|
+
action = action_subdir + part
|
614
|
+
|
615
|
+
action_class = action_class.get_subclass(part, @@action_dir + action + '.rb', self)
|
616
|
+
break
|
542
617
|
end
|
543
|
-
end
|
544
|
-
|
545
|
-
# Sets up request-specified variables based in request information received from Rack.
|
546
|
-
def get_request(env)
|
547
|
-
@request = Rack::Request.new(env)
|
548
618
|
|
549
|
-
|
550
|
-
|
551
|
-
|
619
|
+
# if no action, must have traversed all parts to a directory
|
620
|
+
# add a trailing slash and try again
|
621
|
+
redirect_url(app_url + action_subdir) unless action
|
552
622
|
|
553
|
-
|
554
|
-
|
623
|
+
# keep rest of path_info in args
|
624
|
+
args.push(*parts)
|
555
625
|
|
556
|
-
|
557
|
-
|
626
|
+
# return action path and action handler (instance of action class)
|
627
|
+
[action, action_class.new(self,action,action_uri,action_subdir,extension,args,params)]
|
558
628
|
end
|
559
629
|
|
560
|
-
|
561
|
-
|
562
|
-
|
630
|
+
|
631
|
+
##### FILE METHODS #####
|
632
|
+
|
633
|
+
# If file has already been cached in handling the current request, retrieve from cache
|
634
|
+
# and do not check filesystem for updates. Else cache file via Kiss.file_cache.
|
635
|
+
def file_cache(path, *args, &block)
|
636
|
+
return @@file_cache[path] if @files_cached_this_request[path]
|
637
|
+
@files_cached_this_request[path] = true
|
638
|
+
|
639
|
+
self.class.file_cache(path, *args, &block)
|
640
|
+
end
|
641
|
+
|
642
|
+
|
643
|
+
##### DATABASE METHODS #####
|
644
|
+
|
645
|
+
# Acquires and returns a database connection object from the connection pool,
|
646
|
+
# opening a new connection if the pool is empty.
|
647
|
+
#
|
648
|
+
# Tip: `db' is a shorthand alias for `database'.
|
649
|
+
def database
|
650
|
+
@db ||= @@database_pool.shift || begin
|
651
|
+
raise 'database config missing' unless @@database_config
|
652
|
+
|
653
|
+
# open database connection (if not already open)
|
654
|
+
@db = @@database_config.is_a?(String) ? (Sequel.open @@database_config) :
|
655
|
+
@@database_config.is_a?(Hash) ? (Sequel.open @@database_config) : @@database_config
|
656
|
+
|
657
|
+
@@db_extras_loaded ||= begin
|
658
|
+
@db.class.class_eval do
|
659
|
+
attr_accessor :kiss_controller, :kiss_model_cache
|
660
|
+
|
661
|
+
@last_query = nil
|
662
|
+
def last_query #:nodoc:
|
663
|
+
@last_query
|
664
|
+
end
|
665
|
+
|
666
|
+
alias_method :execute_old, :execute
|
667
|
+
def execute(sql, *args, &block) #:nodoc:
|
668
|
+
@last_query = sql
|
669
|
+
execute_old(sql, *args, &block)
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
require 'kiss/model'
|
674
|
+
Kiss::ModelCache.model_dir = @@model_dir
|
675
|
+
|
676
|
+
if @db.class.name == 'Sequel::MySQL::Database'
|
677
|
+
# add fetch_arrays, all_arrays methods
|
678
|
+
require 'kiss/sequel_mysql'
|
679
|
+
# turn off convert_tinyint_to_bool, unless options say otherwise
|
680
|
+
Sequel.convert_tinyint_to_bool = false unless @@options[:convert_tinyint_to_bool]
|
681
|
+
end
|
682
|
+
|
683
|
+
true
|
684
|
+
end
|
685
|
+
|
686
|
+
# TODO: rewrite evolution file exists check for speed
|
687
|
+
check_evolution_number
|
688
|
+
|
689
|
+
# create model cache for this database connection
|
690
|
+
@db.kiss_model_cache = Kiss::ModelCache.new(@db)
|
691
|
+
@db.kiss_controller = self
|
692
|
+
|
693
|
+
@db
|
694
|
+
end
|
563
695
|
end
|
696
|
+
alias_method :db, :database
|
697
|
+
|
698
|
+
# Kiss Model cache, used to invoke and store Kiss database models.
|
699
|
+
#
|
700
|
+
# Example:
|
701
|
+
# models[:users] == database model for `users' table
|
702
|
+
#
|
703
|
+
# Tip: `dbm' (stands for `database models') is a shorthand alias for `models'.
|
704
|
+
def models
|
705
|
+
# make sure we have a database connection
|
706
|
+
# create new model cache unless exists already
|
707
|
+
db.kiss_model_cache
|
708
|
+
end
|
709
|
+
alias_method :dbm, :models
|
564
710
|
|
565
711
|
# Returns Sequel dataset to evolution_number table, which specifies app's current evolution number.
|
566
712
|
# Creates evolution_number table if it does not exist.
|
@@ -595,13 +741,10 @@ class Kiss
|
|
595
741
|
end
|
596
742
|
end
|
597
743
|
|
598
|
-
# Saves session to session store, if session data has changed since load.
|
599
|
-
def finalize_session
|
600
|
-
@session.save if @session_fingerprint != Marshal.dump(@session.data).hash
|
601
|
-
end
|
602
744
|
|
603
|
-
#####
|
745
|
+
##### SESSION METHODS #####
|
604
746
|
|
747
|
+
# Retrieves or generates session data object, based on session ID from cookie value.
|
605
748
|
def session
|
606
749
|
@session ||= begin
|
607
750
|
@@session_class ? begin
|
@@ -629,177 +772,35 @@ class Kiss
|
|
629
772
|
end
|
630
773
|
end
|
631
774
|
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
login.merge!(session[:login]) if (session[:login] && session[:login][:expires_at] &&
|
637
|
-
session[:login][:expires_at] > Time.now)
|
638
|
-
|
639
|
-
login
|
640
|
-
end
|
641
|
-
end
|
642
|
-
|
643
|
-
# Deletes login data from session.
|
644
|
-
def logout
|
645
|
-
session.delete(:login)
|
646
|
-
@login = nil
|
647
|
-
end
|
648
|
-
alias_method :reset_login_session, :logout
|
649
|
-
alias_method :reset_login_data, :logout
|
650
|
-
|
651
|
-
# Merges data hash (key-value pairs) into request login hash.
|
652
|
-
def set_login_data(data)
|
653
|
-
login.merge!(data)
|
654
|
-
login
|
655
|
-
end
|
656
|
-
|
657
|
-
# Merges data hash (key-value pairs) into session login hash.
|
658
|
-
def set_login_session(data)
|
659
|
-
set_login_data(data)
|
660
|
-
session[:login] = (session[:login] || {}).merge(data)
|
661
|
-
end
|
662
|
-
|
663
|
-
# Sets expire time of session login hash, after which time it will be reset (emptied).
|
664
|
-
# If given a FixNum, sets expire time to now plus number of seconds.
|
665
|
-
def set_login_expires(time)
|
666
|
-
time = Time.now + time if time.is_a?(Fixnum)
|
667
|
-
set_login_session(:expires_at => time)
|
668
|
-
end
|
669
|
-
|
670
|
-
# Returns true if login hash is defined and not expired.
|
671
|
-
def login_session_valid?
|
672
|
-
login && !login_session_expired?
|
673
|
-
end
|
674
|
-
|
675
|
-
# Returns true if login hash is expired.
|
676
|
-
def login_session_expired?
|
677
|
-
login && (!login[:expires_at] || login[:expires_at] < Time.now)
|
678
|
-
end
|
679
|
-
|
680
|
-
# Calls login action's load_from_session method to populate request login hash.
|
681
|
-
def load_from_login_session
|
682
|
-
klass = action_class('/login')
|
683
|
-
raise 'load_from_login_session called, but no login action found' unless klass
|
684
|
-
|
685
|
-
action_handler = klass.new(self)
|
686
|
-
action_handler.load_from_session
|
687
|
-
end
|
688
|
-
|
689
|
-
# Returns path to login action.
|
690
|
-
def login_path
|
691
|
-
@@action_dir + '/login.rb'
|
692
|
-
end
|
693
|
-
|
694
|
-
# If valid login session exists, loads login action to populate request login hash data.
|
695
|
-
# Otherwise, loads and calls login action to authenticate user.
|
696
|
-
def authenticate
|
697
|
-
if login_session_valid?
|
698
|
-
load_from_login_session
|
699
|
-
else
|
700
|
-
klass = action_class('/login')
|
701
|
-
raise 'authenticate called, but no login action found' unless klass
|
702
|
-
|
703
|
-
old_template = @template
|
704
|
-
@template = '/login.rhtml'
|
705
|
-
|
706
|
-
process(klass,login_path)
|
707
|
-
|
708
|
-
@template = old_template
|
709
|
-
|
710
|
-
unless login_session_valid?
|
711
|
-
#raise 'login action completed without setting valid login session'
|
712
|
-
end
|
713
|
-
end
|
775
|
+
# Saves session to session store, if session data has changed since load.
|
776
|
+
def finalize_session
|
777
|
+
@session.save if @session_fingerprint != Marshal.dump(@session.data).hash
|
714
778
|
end
|
715
779
|
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
def action_class(action = @action)
|
720
|
-
action_path = @@action_dir + action.to_s + '.rb'
|
721
|
-
return nil unless action_path.is_a?(String) && action_path.length > 0
|
722
|
-
|
723
|
-
file_cache(action_path) do |src|
|
724
|
-
# create new action class, subclass of shared action parent class
|
725
|
-
klass = Class.new(@@action_root_class)
|
726
|
-
klass.class_eval(src,action_path) if src
|
727
|
-
klass
|
728
|
-
end
|
780
|
+
# Returns a Kiss::Login object containing data from session.login.
|
781
|
+
def login
|
782
|
+
@login ||= Kiss::Login.new(session)
|
729
783
|
end
|
730
784
|
|
731
|
-
# Parses request URI to determine action path and arguments.
|
732
|
-
def parse_action_path(path)
|
733
|
-
@action_subdir = ''
|
734
|
-
@action = nil
|
735
|
-
|
736
|
-
redirect_url(app_url + '/') if path == ''
|
737
|
-
|
738
|
-
path += @@default_action if path =~ /\/\Z/
|
739
|
-
|
740
|
-
parts = path.sub(/^\/*/,'').split('/')
|
741
|
-
|
742
|
-
while part = parts.shift
|
743
|
-
raise 'bad action' if part !~ /\A[a-z0-9][\w\-\.]*\Z/i
|
744
|
-
|
745
|
-
test_path = @@action_dir + @action_subdir + '/' + part
|
746
|
-
if Kiss.directory_exists?(test_path)
|
747
|
-
@action_subdir += '/' + part
|
748
|
-
next
|
749
|
-
end
|
750
|
-
|
751
|
-
if part =~ /\A(.+)\.(\w+)\Z/
|
752
|
-
@extension = $2
|
753
|
-
part = $1
|
754
|
-
else
|
755
|
-
@extension = 'rhtml'
|
756
|
-
end
|
757
|
-
|
758
|
-
@action = @action_subdir + '/' + part
|
759
|
-
break
|
760
|
-
end
|
761
|
-
|
762
|
-
# if no action, must have traversed all parts to a directory
|
763
|
-
# add a trailing slash and try again
|
764
|
-
redirect_url(app_url + '/' + path + '/') unless @action
|
765
|
-
|
766
|
-
@action_path = @@action_dir + '/' + @action + '.rb'
|
767
|
-
|
768
|
-
# keep rest of path_info in args
|
769
|
-
@args = parts
|
770
|
-
end
|
771
785
|
|
772
|
-
|
773
|
-
# action class and invoking `call' method on the instance.
|
774
|
-
def process(klass = action_class, action_path = @action_path)
|
775
|
-
action_handler = klass.new(self)
|
776
|
-
action_handler.call
|
777
|
-
|
778
|
-
# return handler to follow with render
|
779
|
-
action_handler
|
780
|
-
end
|
786
|
+
##### OUTPUT METHODS #####
|
781
787
|
|
782
788
|
# Outputs a Kiss::StaticFile object as response to Rack.
|
783
789
|
# Used to return static files efficiently.
|
784
790
|
def send_file(path, mime_type = nil)
|
785
791
|
@response = Kiss::StaticFile.new(path,mime_type)
|
786
792
|
|
787
|
-
throw :
|
793
|
+
throw :kiss_request_done
|
788
794
|
end
|
789
795
|
|
790
796
|
# Prepares Rack::Response object to return application response to Rack.
|
791
797
|
# Raises Kiss::ActionDone exception to bypass caller stack and return directly
|
792
798
|
# to Kiss#call.
|
793
799
|
def send_response(output = '',options = {})
|
794
|
-
content_type = options[:content_type] ||
|
795
|
-
(extension ? Kiss.mime_type(extension) : nil)
|
796
|
-
document_encoding ||= 'utf-8'
|
797
|
-
|
798
|
-
@response['Content-Type'] = "#{content_type}; #{document_encoding}" if content_type
|
799
800
|
@response['Content-Length'] = output.length.to_s
|
800
801
|
@response.body = output
|
801
802
|
|
802
|
-
throw :
|
803
|
+
throw :kiss_request_done
|
803
804
|
|
804
805
|
# back to Kiss#call, which finalizes session and returns @response
|
805
806
|
# (throws exception if no @response set)
|
@@ -809,81 +810,71 @@ class Kiss
|
|
809
810
|
def redirect_url(url)
|
810
811
|
@response.status = 302
|
811
812
|
@response['Location'] = url
|
812
|
-
|
813
|
+
@response.body = ''
|
814
|
+
|
815
|
+
throw :kiss_request_done
|
813
816
|
end
|
814
817
|
|
815
|
-
# Returns new Kiss::Mailer object using specified options.
|
816
|
-
def new_email(options = {})
|
817
|
-
mailer = Kiss::Mailer.new(options)
|
818
|
-
mailer.controller = self
|
819
|
-
mailer
|
820
|
-
end
|
821
818
|
|
822
|
-
|
823
|
-
@db ||= @@database_pool.shift || begin
|
824
|
-
raise 'database config missing' unless @@database_config
|
825
|
-
|
826
|
-
# open database connection (if not already open)
|
827
|
-
@db = @@database_config.is_a?(String) ? (Sequel.open @@database_config) :
|
828
|
-
@@database_config.is_a?(Hash) ? (Sequel.open @@database_config) : @@database_config
|
829
|
-
|
830
|
-
@@db_extras_loaded ||= begin
|
831
|
-
@db.class.class_eval do
|
832
|
-
@last_query = nil
|
833
|
-
def last_query
|
834
|
-
@last_query
|
835
|
-
end
|
819
|
+
##### DEBUG/BENCH OUTPUT #####
|
836
820
|
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
end
|
843
|
-
|
844
|
-
require 'kiss/model'
|
845
|
-
Kiss::ModelCache.model_dir = @@model_dir
|
846
|
-
|
847
|
-
if @db.class.name == 'Sequel::MySQL::Database'
|
848
|
-
# add fetch_arrays, all_arrays methods
|
849
|
-
require 'kiss/sequel_mysql'
|
850
|
-
# turn off convert_tinyint_to_bool, unless options say otherwise
|
851
|
-
Sequel.convert_tinyint_to_bool = false unless @@options[:convert_tinyint_to_bool]
|
852
|
-
end
|
853
|
-
true
|
854
|
-
end
|
855
|
-
|
856
|
-
# TODO: rewrite evolution file exists check for speed
|
857
|
-
check_evolution_number
|
858
|
-
|
859
|
-
@db
|
860
|
-
end
|
821
|
+
# Adds debug message to inspect object. Debug messages will be shown at top of
|
822
|
+
# application response body.
|
823
|
+
def debug(object, context = Kernel.caller[0])
|
824
|
+
@debug_messages.push( [object.inspect, context] )
|
825
|
+
object
|
861
826
|
end
|
827
|
+
alias_method :trace, :debug
|
862
828
|
|
863
|
-
#
|
864
|
-
#
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
829
|
+
# Starts a new benchmark timer, with optional label. Benchmark results will be shown
|
830
|
+
# at top of application response body.
|
831
|
+
def bench(label = nil, context = Kernel.caller[0])
|
832
|
+
stop_benchmark(context)
|
833
|
+
@benchmarks.push(
|
834
|
+
:label => label,
|
835
|
+
:start_time => Time.now,
|
836
|
+
:start_context => context
|
837
|
+
)
|
870
838
|
end
|
871
|
-
alias_method :models, :dbm
|
872
839
|
|
873
|
-
|
874
|
-
|
840
|
+
# Stops last benchmark timer, if still running.
|
841
|
+
def stop_benchmark(end_context = nil)
|
842
|
+
if @benchmarks[-1] && !@benchmarks[-1][:end_time]
|
843
|
+
@benchmarks[-1][:end_time] = Time.now
|
844
|
+
@benchmarks[-1][:end_context] = end_context
|
845
|
+
end
|
875
846
|
end
|
876
847
|
|
877
|
-
|
878
|
-
|
848
|
+
|
849
|
+
##### OTHER METHODS #####
|
850
|
+
|
851
|
+
# Returns URL/URI of app root (corresponding to top level of action_dir).
|
852
|
+
def app_url(options = {})
|
853
|
+
options.empty? ? (@app_url ||= @protocol + '://' + @app_host + @app_uri) :
|
854
|
+
(options[:protocol] || @protocol) + '://' + (options[:host] || @app_host) +
|
855
|
+
(options[:uri] || @app_uri)
|
879
856
|
end
|
880
857
|
|
881
|
-
|
882
|
-
|
858
|
+
# Returns URL/URI of app's static assets (asset_host or public_uri).
|
859
|
+
def pub_url(options = {})
|
860
|
+
options.empty? ?
|
861
|
+
(@pub ||= (@@asset_host ? @protocol + '://' + @@asset_host : '') + @@asset_uri) :
|
862
|
+
(options[:protocol] || @protocol) + '://' + (options[:host] || @@asset_host) +
|
863
|
+
(options[:uri] || @@asset_uri)
|
883
864
|
end
|
865
|
+
alias_method :asset_url, :pub_url
|
884
866
|
|
885
867
|
# Adds data to be displayed in "Cache" section of Kiss exception reports.
|
886
|
-
def
|
887
|
-
@exception_cache.merge!(data)
|
868
|
+
def exception_cache(data = nil)
|
869
|
+
@exception_cache.merge!(data) if data
|
870
|
+
@exception_cache
|
871
|
+
end
|
872
|
+
alias_method :set_exception_cache, :exception_cache
|
873
|
+
|
874
|
+
# Returns new Kiss::Mailer object using specified options.
|
875
|
+
def new_email(options = {})
|
876
|
+
mailer = Kiss::Mailer.new(options)
|
877
|
+
mailer.controller = self
|
878
|
+
mailer
|
888
879
|
end
|
889
880
|
end
|