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 CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'rubygems'
2
- Gem::manage_gems
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 << '--main' << 'Kiss' <<
20
- '--title' << 'Kiss: Ruby MVC Web Framework -- RDoc Reference Documentation'
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.2.0')
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.0.4
1
+ 1.1
@@ -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
Binary file
@@ -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 :params, :args, :action, :action_subdir, :action_path, :extension, :host, :request,
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
- @@app_host = @@options[:app_host] ? ('http://' + @@options[:app_host]) : ''
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
- if (@@options[:session_class])
254
- @@session_class = (@@options[:session_class].class == Class) ? @@options[:session_class] :
255
- @@options[:session_class].to_const
256
- end
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 h(obj)
318
- case obj
319
- when String
320
- Rack::Utils.escape_html(obj).gsub(/^(\s+)/) {'&nbsp;' * $1.length}
321
- else
322
- Rack::Utils.escape_html(obj.inspect)
323
- end
324
+ def html_escape(obj)
325
+ Rack::Utils.escape_html(
326
+ obj.is_a?(String) ? obj : obj.inspect
327
+ ).gsub(/^(\s+)/) {'&nbsp;' * $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, raises exception of type fnf_exception_class.
395
- def file_cache(path, fnf_file_type = nil, fnf_exception_class = Kiss::FileNotFound)
396
- if (@@options[:file_cache_no_reload] && @@file_cache.has_key?(path))
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 !File.file?(path)
401
- raise fnf_exception_class, "#{fnf_file_type} file missing: '#{path}'" if fnf_file_type
402
-
403
- @@file_cache[path] = nil
404
- contents = nil
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
- # expire cache if file (or symlink) modified
407
- # TODO: what about symlinks to symlinks?
408
- if !@@file_cache_time[path] ||
409
- @@file_cache_time[path] < File.mtime(path) ||
410
- ( File.symlink?(path) && (@@file_cache_time[path] < File.lstat(path).mtime) )
411
-
412
- @@file_cache[path] = nil
413
- @@file_cache_time[path] = Time.now
414
- contents = File.read(path)
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
- @@file_cache[path] ||= begin
419
- (block_given?) ? yield(contents) : contents
447
+ if change
448
+ @@file_cache[path] ||= block_given? ? yield(contents) : contents
420
449
  end
421
- end
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
- # Caches the specified file and return its contents. See Kiss.file_cache above.
442
- def file_cache(path, fnf_file_type = nil, fnf_exception_class = Kiss::FileNotFound, &block)
443
- return @@file_cache[path] if @files_cached_this_request[path]
444
- @files_cached_this_request[path] = true
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
- self.class.file_cache(path, fnf_file_type, fnf_exception_class, &block)
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
- if @@rack_file && (
454
- (env["PATH_INFO"] == '/favicon.ico') ||
455
- (env["PATH_INFO"].sub!(Regexp.new("\\A#{@@asset_uri}"),''))
456
- )
457
- return @@rack_file.call(env)
458
- end
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
- get_request(env)
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 :kiss_action_done do
467
- parse_action_path(@path)
468
- env['kiss.parsed_action'] = @action
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 login_session_valid?
474
- load_from_login_session
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
- env['kiss.processed_action'] = @action
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
- @@database_pool.push(@db) if @db
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
- # Starts a new benchmark timer, with optional label. Benchmark results will be shown
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
- # Stops last benchmark timer, if still running.
538
- def stop_benchmark(end_context = nil)
539
- if @benchmarks[-1] && !@benchmarks[-1][:end_time]
540
- @benchmarks[-1][:end_time] = Time.now
541
- @benchmarks[-1][:end_context] = end_context
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
- @app_host = @@options[:app_host] ? ('http://' + @@options[:app_host]) : @request.server rescue ''
550
- @app_uri = @@options[:app_uri] || @request.script_name || ''
551
- @app_url = @app_host + @app_uri
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
- @path = @request.path_info || '/'
554
- @params = @request.params
623
+ # keep rest of path_info in args
624
+ args.push(*parts)
555
625
 
556
- @host ||= @request.host rescue ''
557
- @protocol = env['HTTPS'] == 'on' ? 'https' : 'http'
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
- # Returns URL/URI of app root (corresponding to top level of action_dir).
561
- def app_url(suffix = nil)
562
- suffix ? @app_url + suffix : @app_url
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
- ##### LOGIN SESSION #####
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
- def login
633
- @login ||= begin
634
- login = {}
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
- ##### ACTION METHODS #####
717
-
718
- # Creates and caches anonymous class with which to invoke specified (or current) action.
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
- # Processes specified (or current) action, by instantiating its anonymous
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 :kiss_action_done
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 :kiss_action_done
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
- send_response
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
- def db
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
- alias_method :execute_old, :execute
838
- def execute(sql, *args, &block)
839
- @last_query = sql
840
- execute_old(sql, *args, &block)
841
- end
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
- # Kiss Model cache, used to invoke and store Kiss database models.
864
- #
865
- # Example:
866
- # models[:users] : database model for `users' table
867
- def dbm
868
- db unless @db
869
- @dbm ||= Kiss::ModelCache.new(self)
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
- def random_text(*args)
874
- self.class.random_text(*args)
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
- def h(*args)
878
- self.class.h(*args)
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
- def template
882
- @template || @action
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 set_exception_cache(data)
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