kiss 1.0.4 → 1.1
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/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
|