kiss 1.1 → 1.7
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/LICENSE +1 -1
- data/Rakefile +2 -1
- data/VERSION +1 -1
- data/bin/kiss +151 -34
- data/data/scaffold.tgz +0 -0
- data/lib/kiss.rb +389 -742
- data/lib/kiss/accessors/controller.rb +47 -0
- data/lib/kiss/accessors/request.rb +106 -0
- data/lib/kiss/accessors/template.rb +23 -0
- data/lib/kiss/action.rb +502 -132
- data/lib/kiss/bench.rb +14 -5
- data/lib/kiss/debug.rb +14 -6
- data/lib/kiss/exception_report.rb +22 -299
- data/lib/kiss/ext/core.rb +700 -0
- data/lib/kiss/ext/rack.rb +33 -0
- data/lib/kiss/ext/sequel_database.rb +47 -0
- data/lib/kiss/ext/sequel_mysql_dataset.rb +23 -0
- data/lib/kiss/form.rb +404 -179
- data/lib/kiss/form/field.rb +183 -307
- data/lib/kiss/form/field_types.rb +239 -0
- data/lib/kiss/format.rb +88 -70
- data/lib/kiss/html/exception_report.css +222 -0
- data/lib/kiss/html/exception_report.html +210 -0
- data/lib/kiss/iterator.rb +14 -12
- data/lib/kiss/login.rb +8 -8
- data/lib/kiss/mailer.rb +68 -66
- data/lib/kiss/model.rb +323 -36
- data/lib/kiss/rack/bench.rb +16 -8
- data/lib/kiss/rack/email_errors.rb +25 -15
- data/lib/kiss/rack/errors_ok.rb +2 -2
- data/lib/kiss/rack/facebook.rb +6 -6
- data/lib/kiss/rack/file_not_found.rb +10 -8
- data/lib/kiss/rack/log_exceptions.rb +3 -3
- data/lib/kiss/rack/recorder.rb +2 -2
- data/lib/kiss/rack/show_debug.rb +2 -2
- data/lib/kiss/rack/show_exceptions.rb +2 -2
- data/lib/kiss/request.rb +435 -0
- data/lib/kiss/sequel_session.rb +15 -14
- data/lib/kiss/static_file.rb +20 -13
- data/lib/kiss/template.rb +327 -0
- metadata +60 -25
- data/lib/kiss/controller_accessors.rb +0 -81
- data/lib/kiss/hacks.rb +0 -188
- data/lib/kiss/sequel_mysql.rb +0 -25
- data/lib/kiss/template_methods.rb +0 -167
data/LICENSE
CHANGED
data/Rakefile
CHANGED
@@ -23,8 +23,9 @@ spec = Gem::Specification.new do |s|
|
|
23
23
|
)
|
24
24
|
|
25
25
|
s.add_dependency('rack','=0.4.0')
|
26
|
-
s.add_dependency('sequel','>=
|
26
|
+
s.add_dependency('sequel','>=3.0.0')
|
27
27
|
s.add_dependency('erubis')
|
28
|
+
s.add_dependency('tzinfo')
|
28
29
|
end
|
29
30
|
|
30
31
|
Rake::GemPackageTask.new(spec) do |pkg|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.7
|
data/bin/kiss
CHANGED
@@ -1,45 +1,172 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
require 'rubygems'
|
4
|
+
require 'kiss'
|
5
|
+
require 'optparse'
|
6
|
+
require 'irb'
|
7
|
+
|
8
|
+
def main
|
9
|
+
parse_options
|
10
|
+
Kiss::default_project_dir = $project_dir
|
11
|
+
|
12
|
+
command = ARGV.shift
|
13
|
+
|
14
|
+
case command
|
15
|
+
when 'create'
|
16
|
+
create
|
17
|
+
when 'run'
|
18
|
+
run
|
19
|
+
when 'irb'
|
20
|
+
kiss_irb
|
21
|
+
when 'set'
|
22
|
+
set
|
23
|
+
when 'evolve'
|
24
|
+
evolve
|
25
|
+
when 'help'
|
26
|
+
display_usage(ARGV[0])
|
27
|
+
else
|
28
|
+
display_usage
|
29
|
+
end
|
30
|
+
Kernel::exit
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_options
|
34
|
+
$project_dir = Dir.pwd
|
35
|
+
$rack_builder = []
|
36
|
+
|
37
|
+
opts = OptionParser.new do |opt|
|
38
|
+
opt.program_name = File.basename $0
|
39
|
+
opt.version = '1'
|
40
|
+
opt.release = nil
|
41
|
+
opt.summary_indent = ' ' * 4
|
42
|
+
# opt.banner = <<-EOT
|
43
|
+
#
|
44
|
+
# EOT
|
45
|
+
|
46
|
+
opt.separator nil
|
47
|
+
opt.separator "Options:"
|
48
|
+
opt.separator nil
|
49
|
+
|
50
|
+
opt.on("--evolution=VERSION", "-e",
|
51
|
+
"Specifies the desired evolution number.") do |value|
|
52
|
+
$evolution_number = value.sub(/\A\=/,'')
|
53
|
+
end
|
54
|
+
|
55
|
+
opt.on("--project=DIRECTORY", "-p",
|
56
|
+
"Specifies the desired application/project directory.") do |value|
|
57
|
+
$project_dir = value.sub(/\A\=/,'')
|
58
|
+
end
|
59
|
+
|
60
|
+
opt.on("--builder=RACK_BUILDER", "-b",
|
61
|
+
"Specifies a desired Rack builder module name for kiss run.") do |value|
|
62
|
+
$rack_builder.push( *( value.sub(/\A\=/,'').split(/,/).map{|b| b.to_sym} ))
|
63
|
+
end
|
64
|
+
|
65
|
+
opt.separator nil
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
result = opts.parse! ARGV
|
70
|
+
rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
|
71
|
+
puts opts
|
72
|
+
puts
|
73
|
+
puts e
|
74
|
+
Kernel::exit 1
|
75
|
+
end
|
3
76
|
|
4
77
|
def create
|
5
|
-
|
6
|
-
display_usage('create') unless project_name =~ /\S/
|
78
|
+
archive_path = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))),'data','scaffold.tgz')
|
7
79
|
|
8
|
-
|
80
|
+
Dir.mkdir($project_dir) unless File.directory?($project_dir)
|
9
81
|
|
10
|
-
|
82
|
+
puts "Expanding '#{archive_path}' to '#{$project_dir}'..."
|
83
|
+
`cd '#{$project_dir}'; tar xvfz '#{archive_path}'`
|
84
|
+
|
85
|
+
puts 'Kiss application scaffold created.'
|
86
|
+
end
|
11
87
|
|
12
|
-
|
88
|
+
def load_kiss_project
|
89
|
+
Kiss.load
|
90
|
+
end
|
13
91
|
|
14
|
-
|
92
|
+
def set
|
93
|
+
app = load_kiss_project
|
94
|
+
db = app.database
|
95
|
+
|
96
|
+
if $evolution_number
|
97
|
+
db[:evolution_number].update(:version => $evolution_number)
|
98
|
+
puts "Database evolution number set to #{$evolution_number}."
|
99
|
+
end
|
100
|
+
end
|
15
101
|
|
16
|
-
|
102
|
+
def evolve
|
103
|
+
app = load_kiss_project
|
104
|
+
db = app.database
|
105
|
+
|
106
|
+
db_version = db.evolution_number
|
107
|
+
target_version = app.last_evolution_file_number
|
108
|
+
|
109
|
+
target_version = $evolution_number if $evolution_number &&
|
110
|
+
($evolution_number < target_version)
|
111
|
+
|
112
|
+
while db_version < target_version
|
113
|
+
db_version += 1
|
114
|
+
|
115
|
+
filename = app.evolution_file(db_version)
|
116
|
+
print "Applying evolution file number #{db_version}: #{filename}...\n\n"
|
117
|
+
|
118
|
+
db.transaction do
|
119
|
+
File.read(filename).gsub(/\#[^\n]*/,'').split(';').each do |query|
|
120
|
+
next if query.blank?
|
121
|
+
query.sub!(/\A\s*/,'')
|
122
|
+
print (query + ";\n\n")
|
123
|
+
db << query
|
124
|
+
end
|
125
|
+
end
|
126
|
+
db[:evolution_number].update(:version => db_version)
|
127
|
+
puts "Database at evolution number #{db_version}."
|
128
|
+
end
|
17
129
|
end
|
18
130
|
|
19
131
|
def run
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
require 'rubygems'
|
24
|
-
require 'kiss'
|
132
|
+
Kiss.run( :rack_builder => $rack_builder )
|
133
|
+
end
|
25
134
|
|
26
|
-
|
135
|
+
def kiss_irb
|
136
|
+
catch (:IRB_EXIT) do
|
137
|
+
IRB.start
|
138
|
+
end
|
27
139
|
end
|
28
140
|
|
29
141
|
def display_usage(command = nil)
|
30
142
|
case command
|
31
143
|
when 'create'
|
32
144
|
puts <<-EOT
|
33
|
-
usage: kiss create
|
34
|
-
Creates a
|
145
|
+
usage: kiss create
|
146
|
+
Creates a Kiss application scaffold.
|
35
147
|
EOT
|
36
148
|
when 'run'
|
37
149
|
puts <<-EOT
|
38
|
-
usage: kiss run
|
39
|
-
Runs a Kiss application
|
150
|
+
usage: kiss run
|
151
|
+
Runs a Kiss application.
|
152
|
+
EOT
|
153
|
+
when 'irb'
|
154
|
+
puts <<-EOT
|
155
|
+
usage: kiss irb
|
156
|
+
Begin an interactive Ruby (IRB) session in the context of a
|
157
|
+
Kiss application.
|
40
158
|
|
41
|
-
|
42
|
-
|
159
|
+
Type 'app = Kiss.new' at the irb prompt to load the application.
|
160
|
+
EOT
|
161
|
+
when 'set'
|
162
|
+
puts <<-EOT
|
163
|
+
usage: kiss set [-options]
|
164
|
+
Set application variables as specified in the command options.
|
165
|
+
EOT
|
166
|
+
when 'evolve'
|
167
|
+
puts <<-EOT
|
168
|
+
usage: kiss evolve
|
169
|
+
Applies evolution files to database of Kiss application.
|
43
170
|
EOT
|
44
171
|
else
|
45
172
|
puts <<-EOT
|
@@ -48,6 +175,10 @@ Utility to support and facilitate the Kiss web framework.
|
|
48
175
|
|
49
176
|
Commands:
|
50
177
|
create - Create a new Kiss application project.
|
178
|
+
evolve - Run database evolutions from Kiss project.
|
179
|
+
set - Set application variables such as current database
|
180
|
+
evolution number.
|
181
|
+
irb - Start an interactive Ruby session with Kiss loaded.
|
51
182
|
run - Run a Kiss application.
|
52
183
|
help - Display usage info.
|
53
184
|
|
@@ -56,20 +187,6 @@ For help on any command above, type:
|
|
56
187
|
EOT
|
57
188
|
end
|
58
189
|
puts
|
59
|
-
exit
|
60
190
|
end
|
61
191
|
|
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
|
192
|
+
main
|
data/data/scaffold.tgz
CHANGED
Binary file
|
data/lib/kiss.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# Kiss - A web application framework for Ruby
|
2
|
-
# Copyright (C) 2005-
|
2
|
+
# Copyright (C) 2005-2010 MultiWidget LLC.
|
3
3
|
# See LICENSE for details.
|
4
4
|
|
5
5
|
# Author:: Shawn Van Ittersum, MultiWidget LLC
|
6
|
-
# Copyright:: Copyright (c) 2005-
|
6
|
+
# Copyright:: Copyright (c) 2005-2010 MultiWidget LLC.
|
7
7
|
# License:: MIT X11 License
|
8
8
|
|
9
9
|
require 'rubygems'
|
@@ -12,688 +12,365 @@ require 'rack'
|
|
12
12
|
require 'rack/request'
|
13
13
|
require 'erubis'
|
14
14
|
|
15
|
-
|
15
|
+
# Sequel may not be required anymore for string inflectors (singular, plural, titlecase, etc.),
|
16
|
+
# if database functionality is not used, since inflectors are now in sequel/extensions/inflector.
|
17
|
+
require 'sequel'
|
18
|
+
require 'sequel/extensions/inflector'
|
16
19
|
|
17
|
-
require '
|
18
|
-
require 'kiss/template_methods'
|
19
|
-
require 'kiss/action'
|
20
|
-
|
21
|
-
autoload :Sequel, 'sequel'
|
22
|
-
|
23
|
-
module Rack
|
24
|
-
autoload :Bench, 'kiss/rack/bench'
|
25
|
-
autoload :ErrorsOK, 'kiss/rack/errors_ok'
|
26
|
-
autoload :EmailErrors, 'kiss/rack/email_errors'
|
27
|
-
autoload :Facebook, 'kiss/rack/facebook'
|
28
|
-
autoload :FileNotFound, 'kiss/rack/file_not_found'
|
29
|
-
autoload :LogExceptions, 'kiss/rack/log_exceptions'
|
30
|
-
autoload :Recorder, 'kiss/rack/recorder'
|
31
|
-
autoload :ShowDebug, 'kiss/rack/show_debug'
|
32
|
-
autoload :ShowExceptions, 'kiss/rack/show_exceptions'
|
33
|
-
end
|
20
|
+
require 'tzinfo'
|
34
21
|
|
35
|
-
|
36
|
-
|
37
|
-
class Class
|
38
|
-
# adapted from Rails, re-written for speed (only one class_eval call)
|
39
|
-
def cattr_reader(*syms)
|
40
|
-
class_eval(
|
41
|
-
syms.flatten.map do |sym|
|
42
|
-
sym.is_a?(Hash) ? '' : %Q(
|
43
|
-
unless defined? @@#{sym}
|
44
|
-
@@#{sym} = nil
|
45
|
-
end
|
22
|
+
require 'kiss/ext/core'
|
23
|
+
require 'kiss/ext/rack'
|
46
24
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
def #{sym}
|
52
|
-
@@#{sym}
|
53
|
-
end
|
54
|
-
|
55
|
-
)
|
56
|
-
end.join, __FILE__, __LINE__
|
57
|
-
)
|
58
|
-
end
|
59
|
-
end
|
25
|
+
require 'kiss/accessors/controller'
|
26
|
+
require 'kiss/accessors/request'
|
27
|
+
require 'kiss/accessors/template'
|
28
|
+
require 'kiss/action'
|
60
29
|
|
61
30
|
# Kiss - An MVC web application framework for Ruby, built on:
|
62
31
|
# * Erubis template engine
|
63
32
|
# * Sequel database ORM library
|
64
33
|
# * Rack web server abstraction
|
65
34
|
class Kiss
|
35
|
+
autoload :Bench, 'kiss/bench'
|
36
|
+
autoload :Debug, 'kiss/debug'
|
66
37
|
autoload :ExceptionReport, 'kiss/exception_report'
|
67
|
-
autoload :Mailer, 'kiss/mailer'
|
68
|
-
autoload :Iterator, 'kiss/iterator'
|
69
|
-
autoload :Login, 'kiss/login'
|
70
38
|
autoload :Form, 'kiss/form'
|
71
39
|
autoload :Format, 'kiss/format'
|
40
|
+
autoload :Iterator, 'kiss/iterator'
|
41
|
+
autoload :Login, 'kiss/login'
|
42
|
+
autoload :Mailer, 'kiss/mailer'
|
43
|
+
autoload :Request, 'kiss/request'
|
44
|
+
autoload :SequelDatabase, 'kiss/ext/sequel_database'
|
45
|
+
autoload :SequelMySQLDataset, 'kiss/ext/sequel_mysql_dataset'
|
72
46
|
autoload :SequelSession, 'kiss/sequel_session'
|
73
47
|
autoload :StaticFile, 'kiss/static_file'
|
74
|
-
autoload :
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
:SHA512 => "digest/sha2"
|
84
|
-
}
|
85
|
-
@@digest.each_pair do |type,path|
|
86
|
-
Digest.autoload type, path
|
48
|
+
autoload :Template, 'kiss/template'
|
49
|
+
|
50
|
+
# Exceptions classes.
|
51
|
+
class FileNotFoundError < RuntimeError
|
52
|
+
class InvalidAction < self; end
|
53
|
+
class Action < self; end
|
54
|
+
class Template < self; end
|
55
|
+
class Page < self; end
|
56
|
+
class Object < self; end
|
87
57
|
end
|
88
58
|
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
# attributes below are request-specific
|
94
|
-
attr_reader :protocol, :host, :request, :exception_cache, :env
|
59
|
+
# These supplement the MIME types defined by Rack.
|
60
|
+
MIME_TYPES = {
|
61
|
+
'rhtml' => 'text/html'
|
62
|
+
}
|
95
63
|
|
96
64
|
@@default_action = 'index'
|
97
65
|
@@default_cookie_name = 'Kiss'
|
66
|
+
@@default_project_dir = '.'
|
98
67
|
|
99
|
-
|
100
|
-
@@mime_types = {
|
101
|
-
'rhtml' => 'text/html'
|
102
|
-
}
|
68
|
+
cattr_accessor :default_project_dir
|
103
69
|
|
104
|
-
|
70
|
+
# application-wide attributes
|
71
|
+
_attr_reader :action_dir, :template_dir, :email_template_dir, :model_dir, :upload_dir,
|
72
|
+
:evolution_dir, :asset_host, :asset_uri, :asset_dir, :public_dir, :environment,
|
73
|
+
:rack_file, :default_action, :exception_log_file, :session_class, :cookie_name,
|
74
|
+
:authenticate_all, :authenticate_exclude, :exception_handlers, :project_dir,
|
75
|
+
:config, :mailer_config, :mailer_override
|
76
|
+
|
77
|
+
_attr_accessor :session_setup
|
105
78
|
|
79
|
+
# Registry of classes by file path.
|
80
|
+
@_classes = {}
|
81
|
+
|
82
|
+
### Class Methods
|
106
83
|
class << self
|
107
|
-
|
108
|
-
def call(env)
|
109
|
-
new.call(env)
|
110
|
-
end
|
84
|
+
alias_method :load, :new
|
111
85
|
|
112
|
-
|
113
|
-
|
114
|
-
def run(options = nil)
|
115
|
-
begin
|
116
|
-
if @@options
|
117
|
-
merge_options(options) if options
|
118
|
-
else
|
119
|
-
load(options)
|
120
|
-
end
|
121
|
-
|
122
|
-
app = self
|
123
|
-
builder_options = @@options[:rack_builder] || []
|
124
|
-
rack = Rack::Builder.new do
|
125
|
-
builder_options.each do |builder_option|
|
126
|
-
if builder_option.is_a?(Array)
|
127
|
-
builder_args = builder_option
|
128
|
-
builder_option = builder_args.shift
|
129
|
-
else
|
130
|
-
builder_args = []
|
131
|
-
end
|
132
|
-
|
133
|
-
unless builder_option.is_a?(Class)
|
134
|
-
builder_option = Rack.const_get(builder_option.to_s)
|
135
|
-
end
|
136
|
-
|
137
|
-
use(builder_option,*builder_args)
|
138
|
-
end
|
139
|
-
|
140
|
-
run app
|
141
|
-
end.to_app
|
142
|
-
|
143
|
-
handler = @@options[:rack_handler] || Rack::Handler::WEBrick
|
144
|
-
if !handler.is_a?(Class)
|
145
|
-
handler = Rack::Handler.const_get(handler.to_s)
|
146
|
-
end
|
147
|
-
handler.run(rack,@@options[:rack_handler_options] || {:Port => 4000})
|
148
|
-
rescue StandardError, LoadError, SyntaxError => e
|
149
|
-
if @@options[:rack_handler] == :CGI
|
150
|
-
print "Content-type: text/html\n\n"
|
151
|
-
print $debug_messages.to_s + Kiss::ExceptionReport.generate(e)
|
152
|
-
else
|
153
|
-
print "Content-type: text/plain\n\n"
|
154
|
-
puts "exception:\n" + e.message
|
155
|
-
puts "\ntraceback:\n" + e.backtrace.join("\n")
|
156
|
-
end
|
157
|
-
end
|
86
|
+
def rack(config = {})
|
87
|
+
self.new(config).rack
|
158
88
|
end
|
159
89
|
|
160
|
-
|
161
|
-
|
162
|
-
def load(loader_options = nil)
|
163
|
-
# store cached files
|
164
|
-
@@file_cache = {}
|
165
|
-
@@directory_cache = {}
|
166
|
-
@@file_cache_time = {}
|
167
|
-
|
168
|
-
loader_options ||= {}
|
169
|
-
# if loader_options is string, then it specifies environment
|
170
|
-
# else it should be a hash of config options
|
171
|
-
if loader_options.is_a?(String)
|
172
|
-
loader_options = { :environment => loader_options }
|
173
|
-
end
|
174
|
-
|
175
|
-
# environment
|
176
|
-
@@environment = loader_options[:environment]
|
177
|
-
|
178
|
-
# directories
|
179
|
-
script_dir = $0.sub(/[^\/]+\Z/,'')
|
180
|
-
script_dir = '' if script_dir == './'
|
181
|
-
@@project_dir = script_dir + (loader_options[:project_dir] || loader_options[:root_dir] || '..')
|
182
|
-
Dir.chdir(@@project_dir)
|
183
|
-
|
184
|
-
@@config_dir = loader_options[:config_dir] || 'config'
|
185
|
-
|
186
|
-
# get environment name from config/environment
|
187
|
-
if (@@environment.nil?) && File.file?(env_file = @@config_dir+'/environment')
|
188
|
-
@@environment = File.read(env_file).sub(/\s+\Z/,'')
|
189
|
-
end
|
190
|
-
|
191
|
-
# init options
|
192
|
-
@@options = {
|
193
|
-
:layout => '/_layout'
|
194
|
-
}
|
195
|
-
@@lib_dirs = ['lib']
|
196
|
-
@@gem_dirs = ['gems']
|
197
|
-
@@require = []
|
198
|
-
@@authenticate_exclude = ['/login','/logout']
|
199
|
-
|
200
|
-
# common (shared) config
|
201
|
-
if (File.file?(config_file = @@config_dir+'/common.yml'))
|
202
|
-
merge_options( YAML::load(File.read(config_file)) )
|
203
|
-
end
|
204
|
-
# environment config
|
205
|
-
if (File.file?(config_file = "#{@@config_dir}/environments/#{@@environment}.yml"))
|
206
|
-
merge_options( YAML::load(File.read(config_file)) )
|
207
|
-
end
|
208
|
-
|
209
|
-
merge_options( loader_options )
|
210
|
-
|
211
|
-
# set class vars from options
|
212
|
-
@@action_dir = @@options[:action_dir] || 'actions'
|
213
|
-
@@template_dir = @@options[:template_dir] ? @@options[:template_dir] : @@action_dir
|
214
|
-
|
215
|
-
@@asset_dir = @@public_dir = @@options[:asset_dir] || @@options[:public_dir] || 'public_html'
|
216
|
-
|
217
|
-
@@model_dir = @@options[:model_dir] || 'models'
|
218
|
-
|
219
|
-
@@evolution_dir = @@options[:evolution_dir] || 'evolutions'
|
220
|
-
|
221
|
-
@@email_template_dir = @@options[:email_template_dir] || 'email_templates'
|
222
|
-
@@upload_dir = @@options[:upload_dir] || 'uploads'
|
223
|
-
|
224
|
-
@@cookie_name = @@options[:cookie_name] || @@default_cookie_name
|
225
|
-
@@default_action = @@options[:default_action] || @@default_action
|
226
|
-
@@action_root_class = @@options[:action_class] || Class.new(Kiss::Action)
|
227
|
-
|
228
|
-
# exception log
|
229
|
-
@@exception_log_file = @@options[:exception_log] ? ::File.open(@@options[:exception_log],'a') : nil
|
230
|
-
|
231
|
-
# default layout
|
232
|
-
@@layout = @@options[:layout]
|
233
|
-
|
234
|
-
# app_url: URL of the app actions root
|
235
|
-
@@protocol = ENV['HTTPS'] == 'on' ? 'https' : 'http'
|
236
|
-
@@app_host = @@options[:app_host] ? (@@protocol + '://' + @@options[:app_host]) : ''
|
237
|
-
@@app_uri = @@options[:app_uri] || ''
|
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
|
248
|
-
|
249
|
-
# include lib dirs
|
250
|
-
$LOAD_PATH.unshift(*( @@lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
|
251
|
-
|
252
|
-
# add gem dir to rubygems search path
|
253
|
-
Gem.path.unshift(*( @@gem_dirs.flatten.select {|dir| File.directory?(dir) } ))
|
254
|
-
|
255
|
-
# require libs
|
256
|
-
@@require.flatten.each {|lib| require lib }
|
257
|
-
|
258
|
-
# session class
|
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
|
263
|
-
|
264
|
-
# load extensions to action class
|
265
|
-
action_extension_path = @@action_dir + '/_action.rb'
|
266
|
-
if File.file?(action_extension_path)
|
267
|
-
@@action_root_class.class_eval(File.read(action_extension_path),action_extension_path) rescue nil
|
268
|
-
end
|
269
|
-
|
270
|
-
@@action_classes = {}
|
271
|
-
|
272
|
-
# database
|
273
|
-
@@database_config = @@options[:database]
|
274
|
-
@@database_pool = []
|
275
|
-
|
276
|
-
self
|
277
|
-
end
|
278
|
-
|
279
|
-
# Returns true if specified path is a directory.
|
280
|
-
# Cache result if file_cache_no_reload option is set; otherwise, always check filesystem.
|
281
|
-
def directory_exists?(dir)
|
282
|
-
@@options[:file_cache_no_reload] ? (
|
283
|
-
@@directory_cache.has_key?(path) ?
|
284
|
-
@@directory_cache[dir] :
|
285
|
-
@@directory_cache[dir] = File.directory?(dir)
|
286
|
-
) : File.directory?(dir)
|
287
|
-
end
|
288
|
-
|
289
|
-
# Merges specified options into previously defined/merged Kiss options.
|
290
|
-
def merge_options(config_options)
|
291
|
-
if config_options
|
292
|
-
if env_vars = config_options.delete(:ENV)
|
293
|
-
env_vars.each_pair {|k,v| ENV[k] = v }
|
294
|
-
end
|
295
|
-
if lib_dirs = config_options.delete(:lib_dirs)
|
296
|
-
@@lib_dirs.push( lib_dirs )
|
297
|
-
end
|
298
|
-
if gem_dirs = config_options.delete(:gem_dirs)
|
299
|
-
@@gem_dirs.push( gem_dirs )
|
300
|
-
end
|
301
|
-
if require_libs = config_options.delete(:require)
|
302
|
-
@@require.push( require_libs )
|
303
|
-
end
|
304
|
-
if auth_exclude = config_options.delete(:authenticate_exclude)
|
305
|
-
@@authenticate_exclude.push( *(auth_exclude.map { |action| action =~ /\A\// ? action : '/'+action }) )
|
306
|
-
end
|
307
|
-
|
308
|
-
@@options.merge!( config_options )
|
309
|
-
end
|
90
|
+
def run(config = {})
|
91
|
+
self.new(config).run
|
310
92
|
end
|
311
93
|
|
312
94
|
# Converts passed-in filename to absolute path if it does not start with '/'.
|
313
95
|
def absolute_path(filename)
|
314
|
-
|
315
|
-
end
|
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
|
-
|
323
|
-
# Returns string representation of object with HTML entities escaped.
|
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]) }
|
96
|
+
( filename[0,1] == '/' ) ? filename : "#{Dir.pwd}/#{filename}"
|
334
97
|
end
|
335
98
|
|
336
99
|
# Returns MIME type corresponding to passed-in extension.
|
337
100
|
def mime_type(extension)
|
338
|
-
|
101
|
+
extension = extension.to_s
|
102
|
+
rack_mime_types = Rack::Mime::MIME_TYPES rescue Rack::File::MIME_TYPES
|
103
|
+
rack_mime_types[extension] || rack_mime_types['.' + extension] || Kiss::MIME_TYPES[extension]
|
339
104
|
end
|
340
105
|
|
341
|
-
#
|
342
|
-
def
|
343
|
-
|
344
|
-
@@digest[type] ? Digest.const_get(type) : nil
|
106
|
+
# Register a class by its file path, to enable context_class to work.
|
107
|
+
def register_class_path(klass, path)
|
108
|
+
@_classes[path] = klass
|
345
109
|
end
|
346
110
|
|
347
|
-
#
|
348
|
-
def
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
month, day, year = date.split(/\D+/,3).each {|s| s.to_i}
|
354
|
-
|
355
|
-
return 0 unless month && day
|
356
|
-
|
357
|
-
current_year = Time.now.year
|
358
|
-
if !year || year.length == 0
|
359
|
-
# use current year if year is missing.
|
360
|
-
year = current_year
|
361
|
-
else
|
362
|
-
# convert two-digit years to four-digit years
|
363
|
-
year = year.to_i
|
364
|
-
if year < 100
|
365
|
-
year += 1900
|
366
|
-
year += 100 if year < current_year - 95
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
|
-
return sprintf("%04d-%02d-%02d",year,month.to_i,day.to_i)
|
371
|
-
end
|
372
|
-
|
373
|
-
# Validates value against specified format.
|
374
|
-
# If required is true, value must contain a non-whitespace character.
|
375
|
-
# If required is false, value need not match format if and only if value contains only whitespace.
|
376
|
-
def validate_value(value, format, required = false, label = nil)
|
377
|
-
if required && (value !~ /\S/)
|
378
|
-
# value required
|
379
|
-
raise "#{label || 'value'} required"
|
380
|
-
elsif format && (value =~ /\S/)
|
381
|
-
format = Kiss::Format.lookup(format)
|
382
|
-
|
383
|
-
begin
|
384
|
-
format.validate(value)
|
385
|
-
rescue Kiss::Format::ValidateError => e
|
386
|
-
raise e.class, "#{label} validation error: #{e.message}"
|
111
|
+
# Finds the class defined by the file path of the execution context.
|
112
|
+
def context_class
|
113
|
+
caller.each do |frame|
|
114
|
+
if klass = @_classes[frame.sub(/\:.*/, '')]
|
115
|
+
return klass
|
387
116
|
end
|
388
117
|
end
|
389
118
|
end
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
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
|
444
|
-
end
|
445
|
-
end
|
446
|
-
|
447
|
-
if change
|
448
|
-
@@file_cache[path] ||= block_given? ? yield(contents) : contents
|
449
|
-
end
|
450
|
-
|
451
|
-
report_change ? [@@file_cache[path], change] : @@file_cache[path]
|
452
|
-
end
|
453
|
-
end # end class methods
|
454
|
-
|
455
|
-
### Instance Methods
|
456
|
-
|
457
|
-
# Creates a new controller instance, and also configures the application with the
|
458
|
-
# specified options.
|
459
|
-
def initialize(options = nil)
|
460
|
-
if @@options
|
461
|
-
self.class.merge_options(options) if options
|
462
|
-
else
|
463
|
-
self.class.load(options)
|
119
|
+
end # class methods
|
120
|
+
|
121
|
+
|
122
|
+
# Creates a new application controller instance, and also configures the
|
123
|
+
# application from config file options and any passed-in options.
|
124
|
+
def initialize(options = {})
|
125
|
+
# init config
|
126
|
+
@_config = {
|
127
|
+
:layout => '/_layout',
|
128
|
+
:file_cache_reload => true
|
129
|
+
}
|
130
|
+
@_lib_dirs = ['lib']
|
131
|
+
@_gem_dirs = ['gems']
|
132
|
+
@_require = []
|
133
|
+
@_authenticate_exclude = ['/login', '/logout']
|
134
|
+
@_exception_handlers = {}
|
135
|
+
@_mailer_config = {}
|
136
|
+
@_mailer_override = {}
|
137
|
+
|
138
|
+
# store for cached files and directories
|
139
|
+
@_file_cache = {}
|
140
|
+
@_directory_cache = {}
|
141
|
+
@_file_cache_time = {}
|
142
|
+
|
143
|
+
|
144
|
+
# If options is string, then it specifies an environment
|
145
|
+
# (else it should be a hash of config options)
|
146
|
+
options = { :environment => options } if options.is_a?(String)
|
147
|
+
|
148
|
+
# project dir
|
149
|
+
# all other files and directories are relative to the project dir
|
150
|
+
Dir.chdir(options[:project_dir] || options[:root_dir] || @@default_project_dir)
|
151
|
+
# save current path to force return there in case an action changes directory
|
152
|
+
@_project_dir = Dir.pwd
|
153
|
+
|
154
|
+
# directory containing the config files
|
155
|
+
@_config_dir = options[:config_dir] || 'config'
|
156
|
+
|
157
|
+
# get environment name from options or config/environment
|
158
|
+
@_environment = options[:environment] || if File.file?(env_file = @_config_dir + '/environment')
|
159
|
+
File.read(env_file).sub(/\s+\Z/, '')
|
464
160
|
end
|
465
161
|
|
466
|
-
|
467
|
-
@
|
468
|
-
|
469
|
-
@
|
470
|
-
end
|
471
|
-
|
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
|
476
|
-
|
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
|
162
|
+
# read common (shared) config
|
163
|
+
merge_config_file(@_config_dir + '/common.yml')
|
164
|
+
# read environment config
|
165
|
+
merge_config_file(@_config_dir + "/environments/#{@_environment}.yml") if @_environment
|
483
166
|
|
484
|
-
|
485
|
-
|
486
|
-
end
|
487
|
-
action_handler
|
488
|
-
end
|
167
|
+
# merge options passed in to override config files
|
168
|
+
merge_config( options )
|
489
169
|
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
@
|
170
|
+
# set app instance variables from config data and defaults
|
171
|
+
@_action_dir = @_config[:action_dir] || 'actions'
|
172
|
+
@_template_dir = (@_config[:template_dir] ? @_config[:template_dir] : @_action_dir)
|
173
|
+
@_model_dir = @_config[:model_dir] || 'models'
|
174
|
+
@_evolution_dir = @_config[:evolution_dir] || 'evolutions'
|
495
175
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
# )
|
500
|
-
# return @@rack_file.call(env)
|
501
|
-
# end
|
176
|
+
@_asset_dir = @_public_dir = @_config[:asset_dir] || @_config[:public_dir] || 'public_html'
|
177
|
+
@_email_template_dir = @_config[:email_template_dir] || 'email_templates'
|
178
|
+
@_upload_dir = @_config[:upload_dir] || 'uploads'
|
502
179
|
|
503
|
-
|
180
|
+
@_cookie_name = @_config[:cookie_name] || @@default_cookie_name
|
181
|
+
@_default_action = @_config[:default_action] || @@default_action
|
504
182
|
|
505
|
-
|
506
|
-
|
507
|
-
@response = Rack::Response.new
|
183
|
+
# exception log
|
184
|
+
@_exception_log_file = @_config[:exception_log] ? ::File.open(@_config[:exception_log], 'a') : nil
|
508
185
|
|
509
|
-
|
510
|
-
|
511
|
-
@app_uri = @@options[:app_uri] || @request.script_name || ''
|
186
|
+
# authenticate all actions?
|
187
|
+
@_authenticate_all = @_config[:authenticate_all]
|
512
188
|
|
513
|
-
|
189
|
+
# don't require authentication on exception actions
|
190
|
+
@_authenticate_exclude << @_config.exception_action if @_config.exception_action
|
191
|
+
@_authenticate_exclude << @_config.file_not_found_action if @_config.file_not_found_action
|
514
192
|
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
action_handler = invoke_action(path,params)
|
520
|
-
extension = action_handler.extension
|
521
|
-
|
522
|
-
if content_type = options[:content_type] || (extension ? Kiss.mime_type(extension) : nil)
|
523
|
-
@response['Content-Type'] = "#{content_type}; #{options[:document_encoding] || 'utf-8'}"
|
524
|
-
end
|
525
|
-
|
526
|
-
send_response(action_handler.output, action_handler.output_options)
|
527
|
-
end
|
528
|
-
finalize_session if @session
|
529
|
-
|
530
|
-
@response.finish
|
531
|
-
rescue StandardError, LoadError, SyntaxError => e
|
532
|
-
body = Kiss::ExceptionReport.generate(e, env, @exception_cache, @db ? @db.last_query : nil)
|
533
|
-
if @@exception_log_file
|
534
|
-
@@exception_log_file.print(body + "\n--- End of exception report --- \n\n")
|
535
|
-
end
|
536
|
-
[500, {
|
537
|
-
"Content-Type" => "text/html",
|
538
|
-
"Content-Length" => body.length.to_s,
|
539
|
-
"X-Kiss-Error-Type" => e.class.name,
|
540
|
-
"X-Kiss-Error-Message" => e.message.sub(/\n.*/m,'')
|
541
|
-
}, body]
|
542
|
-
end
|
193
|
+
# app host: default hostname of application
|
194
|
+
@_app_host = @_config[:app_host]
|
195
|
+
# app uri: default URI of application
|
196
|
+
@_app_uri = @_config[:app_uri]
|
543
197
|
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
198
|
+
# asset host: hostname of static assets
|
199
|
+
@_asset_host = @_config[:asset_host]
|
200
|
+
|
201
|
+
# public_uri: URI of requests to serve from public_dir
|
202
|
+
@_asset_uri = @_config[:asset_uri] || @_config[:public_uri] || nil
|
203
|
+
@_rack_file = Rack::File.new(@_asset_dir) if @_asset_uri
|
549
204
|
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
205
|
+
# add lib dirs to load path
|
206
|
+
$LOAD_PATH.unshift(*( @_lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
|
207
|
+
|
208
|
+
# add gem dirs to rubygems search path
|
209
|
+
Gem.path.unshift(*( @_gem_dirs.flatten.select {|dir| File.directory?(dir) } ))
|
210
|
+
|
211
|
+
# require specified libs
|
212
|
+
@_require.flatten.each {|lib| require lib }
|
556
213
|
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
214
|
+
# session class
|
215
|
+
@_session_class = @_config[:session_class]
|
216
|
+
@_session_class = @_session_class.to_const if @_session_class && !@_session_class.is_a?(Class)
|
217
|
+
|
218
|
+
# database
|
219
|
+
@_database_config = @_config[:database]
|
220
|
+
@_database_pool = []
|
561
221
|
|
562
|
-
|
222
|
+
self
|
563
223
|
end
|
564
224
|
|
225
|
+
private
|
226
|
+
|
227
|
+
# TODO: Turn exception handlers into a new class;
|
228
|
+
# move exception handling methods from Request to the new class
|
229
|
+
def prepare_exception_handler(value)
|
230
|
+
# start with defaults
|
231
|
+
{
|
232
|
+
:send_email => true
|
233
|
+
}.merge(
|
234
|
+
# add data from the handler config value
|
235
|
+
if value.is_a?(Hash)
|
236
|
+
value
|
237
|
+
elsif value.is_a?(Array)
|
238
|
+
value.hash_with_keys(:action, :send_email)
|
239
|
+
elsif value.is_a?(String)
|
240
|
+
{ :action => value }
|
241
|
+
else
|
242
|
+
raise 'Invalid exception handler config setting.'
|
243
|
+
end
|
244
|
+
)
|
245
|
+
end
|
565
246
|
|
566
|
-
|
247
|
+
def merge_config_file(filename)
|
248
|
+
if File.file?(filename)
|
249
|
+
merge_config( YAML::load(File.read(filename)) )
|
250
|
+
end
|
251
|
+
end
|
567
252
|
|
568
|
-
#
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
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
|
253
|
+
# Merges specified config options into previously defined/merged Kiss config.
|
254
|
+
def merge_config(new_config)
|
255
|
+
if new_config
|
256
|
+
if env_vars = new_config.delete(:ENV)
|
257
|
+
env_vars.each_pair {|k, v| ENV[k] = v }
|
258
|
+
end
|
259
|
+
if lib_dirs = new_config.delete(:lib_dirs)
|
260
|
+
@_lib_dirs += lib_dirs
|
261
|
+
end
|
262
|
+
if gem_dirs = new_config.delete(:gem_dirs)
|
263
|
+
@_gem_dirs += gem_dirs
|
264
|
+
end
|
265
|
+
if require_libs = new_config.delete(:require)
|
266
|
+
@_require += require_libs
|
591
267
|
end
|
592
268
|
|
593
|
-
|
594
|
-
|
595
|
-
|
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
|
269
|
+
if auth_exclude = new_config.delete(:authenticate_exclude)
|
270
|
+
auth_exclude = auth_exclude.map {|action| (action[0,1] == '/') ? action : '/' + action }
|
271
|
+
@_authenticate_exclude += auth_exclude
|
604
272
|
end
|
605
273
|
|
606
|
-
if
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
274
|
+
if exception_handler = new_config.delete(:exception_handler)
|
275
|
+
exception_handlers[Exception] = prepare_exception_handler(value)
|
276
|
+
end
|
277
|
+
if exception_handlers = new_config.delete(:exception_handlers)
|
278
|
+
exception_handlers.each_pair do |key, value|
|
279
|
+
@_exception_handlers[key == :generic ? Exception : key.to_s.to_const] = prepare_exception_handler(value)
|
280
|
+
end
|
611
281
|
end
|
612
282
|
|
613
|
-
|
283
|
+
if mailer_config = new_config.delete(:mailer)
|
284
|
+
@_mailer_override.merge!(mailer_config.delete(:override) || {})
|
285
|
+
@_mailer_config.merge!(mailer_config)
|
286
|
+
end
|
287
|
+
if mailer_override = new_config.delete(:mailer_override)
|
288
|
+
@_mailer_override.merge!(mailer_override)
|
289
|
+
end
|
614
290
|
|
615
|
-
|
616
|
-
break
|
291
|
+
@_config.merge!( new_config )
|
617
292
|
end
|
618
|
-
|
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
|
622
|
-
|
623
|
-
# keep rest of path_info in args
|
624
|
-
args.push(*parts)
|
625
|
-
|
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)]
|
628
293
|
end
|
629
294
|
|
295
|
+
public
|
630
296
|
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
297
|
+
def rack(config = nil)
|
298
|
+
merge_config(config)
|
299
|
+
|
300
|
+
app = self
|
301
|
+
builder_options = @_config[:rack_builder] || []
|
302
|
+
rack = Rack::Builder.new do
|
303
|
+
builder_options.each do |builder_option|
|
304
|
+
if builder_option.is_a?(Array)
|
305
|
+
builder_args = builder_option
|
306
|
+
builder_option = builder_args.shift
|
307
|
+
else
|
308
|
+
builder_args = []
|
309
|
+
end
|
638
310
|
|
639
|
-
|
311
|
+
unless builder_option.is_a?(Class)
|
312
|
+
builder_option = Rack.const_get(builder_option.to_s)
|
313
|
+
end
|
314
|
+
|
315
|
+
use(builder_option, *builder_args)
|
316
|
+
end
|
317
|
+
|
318
|
+
run app
|
319
|
+
end.to_app
|
640
320
|
end
|
641
321
|
|
322
|
+
# Runs Kiss application found at project_dir (default: '..'), with config
|
323
|
+
# read from config files plus additional options if passed in.
|
324
|
+
def run(options = nil)
|
325
|
+
merge_config(options)
|
326
|
+
|
327
|
+
handler = @_config[:rack_handler] || Rack::Handler::WEBrick
|
328
|
+
handler = Rack::Handler.const_get(handler.to_s) unless handler.is_a?(Class)
|
329
|
+
|
330
|
+
handler.run(rack, @_config[:rack_handler_options] || {:Port => 4000})
|
331
|
+
end
|
642
332
|
|
643
|
-
|
333
|
+
# Creates new controller instance to handle Rack request.
|
334
|
+
def call(env)
|
335
|
+
Kiss::Request.new(env, self, @_config).call(env)
|
336
|
+
end
|
644
337
|
|
645
338
|
# Acquires and returns a database connection object from the connection pool,
|
646
339
|
# opening a new connection if the pool is empty.
|
647
|
-
#
|
648
|
-
# Tip: `db' is a shorthand alias for `database'.
|
649
340
|
def database
|
650
|
-
@
|
651
|
-
raise 'database config missing' unless
|
341
|
+
@_database_pool.shift || begin
|
342
|
+
raise 'database config missing' unless @_database_config
|
343
|
+
|
344
|
+
# open database connection
|
345
|
+
db = Sequel.connect @_database_config
|
346
|
+
load_db_class_extensions(db.class)
|
652
347
|
|
653
|
-
#
|
654
|
-
|
655
|
-
|
348
|
+
# create model cache for this database connection
|
349
|
+
db.kiss_controller = self
|
350
|
+
db.kiss_model_cache = Kiss::ModelCache.new(db, @_model_dir)
|
656
351
|
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
@last_query = nil
|
662
|
-
def last_query #:nodoc:
|
663
|
-
@last_query
|
664
|
-
end
|
352
|
+
db
|
353
|
+
end
|
354
|
+
end
|
355
|
+
alias_method :db, :database
|
665
356
|
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
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
|
357
|
+
def load_db_class_extensions(db_class)
|
358
|
+
@_db_class_extensions_loaded ||= {}
|
359
|
+
@_db_class_extensions_loaded[db_class] ||= begin
|
360
|
+
db_class.class_eval { include Kiss::SequelDatabase }
|
361
|
+
|
362
|
+
if db_class.name == 'Sequel::MySQL::Database'
|
363
|
+
# add fetch_arrays, all_arrays methods
|
364
|
+
Sequel::MySQL::Dataset.class_eval { include Kiss::SequelMySQLDataset }
|
682
365
|
|
683
|
-
|
366
|
+
# turn off convert_tinyint_to_bool, unless app config says otherwise
|
367
|
+
Sequel::MySQL.convert_tinyint_to_bool = false unless @_config[:convert_tinyint_to_bool]
|
684
368
|
end
|
685
369
|
|
686
|
-
|
687
|
-
|
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
|
370
|
+
require 'kiss/model'
|
371
|
+
true
|
694
372
|
end
|
695
373
|
end
|
696
|
-
alias_method :db, :database
|
697
374
|
|
698
375
|
# Kiss Model cache, used to invoke and store Kiss database models.
|
699
376
|
#
|
@@ -708,173 +385,143 @@ class Kiss
|
|
708
385
|
end
|
709
386
|
alias_method :dbm, :models
|
710
387
|
|
711
|
-
|
712
|
-
|
713
|
-
def evolution_number_table
|
714
|
-
unless db.table_exists?(:evolution_number)
|
715
|
-
db.create_table :evolution_number do
|
716
|
-
column :version, :integer, :null=> false
|
717
|
-
end
|
718
|
-
db[:evolution_number].insert(:version => 0)
|
719
|
-
end
|
720
|
-
db[:evolution_number]
|
721
|
-
end
|
722
|
-
|
723
|
-
# Returns app's current evolution number.
|
724
|
-
def evolution_number
|
725
|
-
evolution_number_table.first.version
|
388
|
+
def return_database(db)
|
389
|
+
@_database_pool.push(db)
|
726
390
|
end
|
727
|
-
|
728
|
-
# Sets app's current evolution number.
|
729
|
-
def evolution_number=(version)
|
730
|
-
load unless @@options
|
731
|
-
evolution_number_table.update(:version => version)
|
732
|
-
end
|
733
|
-
|
734
|
-
# Check whether there exists a file in evolution_dir whose number is greater than app's
|
735
|
-
# current evolution number. If so, raise an error to indicate need to apply new evolutions.
|
736
|
-
def check_evolution_number
|
737
|
-
version = evolution_number
|
738
|
-
if Kiss.directory_exists?(@@evolution_dir) &&
|
739
|
-
Dir.entries(@@evolution_dir).select { |f| f =~ /\A0*#{version+1}_/ }.size > 0
|
740
|
-
raise "current evolution #{version} is outdated; apply evolutions or update evolution number"
|
741
|
-
end
|
742
|
-
end
|
743
|
-
|
744
391
|
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
session = @@session_class.persist(self,@request.cookies[@@cookie_name])
|
758
|
-
@session_fingerprint = Marshal.dump(session.data).hash
|
759
|
-
|
760
|
-
cookie_vars = {
|
761
|
-
:value => session.values[:session_id],
|
762
|
-
:path => @@options[:cookie_path] || @app_uri,
|
763
|
-
:domain => @@options[:cookie_domain] || @request.host
|
764
|
-
}
|
765
|
-
cookie_vars[:expires] = Time.now + @@options[:cookie_lifespan] if @@options[:cookie_lifespan]
|
766
|
-
|
767
|
-
# set_cookie here or at render time
|
768
|
-
@response.set_cookie @@cookie_name, cookie_vars
|
769
|
-
|
770
|
-
session
|
771
|
-
end : {}
|
392
|
+
# Gets the number of the last file in the evolution dir, or 0 if the directory
|
393
|
+
# does not exist.
|
394
|
+
def last_evolution_file_number
|
395
|
+
version = 0
|
396
|
+
|
397
|
+
if directory_exists?(@_evolution_dir)
|
398
|
+
digits = 1
|
399
|
+
while ( entries = Dir.glob("#{@_evolution_dir}/#{'[0-9]' * digits}*") ).size > 0
|
400
|
+
version = entries.sort.last.sub(/.*\//, '').sub(/\D.*/, '').to_i
|
401
|
+
digits += 1
|
402
|
+
end
|
772
403
|
end
|
773
|
-
end
|
774
|
-
|
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
|
778
|
-
end
|
779
|
-
|
780
|
-
# Returns a Kiss::Login object containing data from session.login.
|
781
|
-
def login
|
782
|
-
@login ||= Kiss::Login.new(session)
|
783
|
-
end
|
784
|
-
|
785
|
-
|
786
|
-
##### OUTPUT METHODS #####
|
787
|
-
|
788
|
-
# Outputs a Kiss::StaticFile object as response to Rack.
|
789
|
-
# Used to return static files efficiently.
|
790
|
-
def send_file(path, mime_type = nil)
|
791
|
-
@response = Kiss::StaticFile.new(path,mime_type)
|
792
404
|
|
793
|
-
|
405
|
+
version
|
794
406
|
end
|
795
407
|
|
796
|
-
#
|
797
|
-
#
|
798
|
-
|
799
|
-
|
800
|
-
@
|
801
|
-
|
802
|
-
|
803
|
-
throw :kiss_request_done
|
408
|
+
# Returns an array of evolution filenames (relative to project dir) matching
|
409
|
+
# evolution number specified by index.
|
410
|
+
def evolution_file(index)
|
411
|
+
# find files matching ev_dir/.*next_version_number
|
412
|
+
files = Dir.glob("#{@_evolution_dir}/*#{index}[^0-9]*").
|
413
|
+
# make sure we have a match for ev_dir/0*next_version_number
|
414
|
+
select {|f| f =~ /\/0*#{index}[_\.][^\/]*\Z/ }
|
804
415
|
|
805
|
-
|
806
|
-
# (throws exception if no @response set)
|
807
|
-
end
|
808
|
-
|
809
|
-
# Sends HTTP 302 response to redirect client browser agent to specified URL.
|
810
|
-
def redirect_url(url)
|
811
|
-
@response.status = 302
|
812
|
-
@response['Location'] = url
|
813
|
-
@response.body = ''
|
416
|
+
raise "multiple evolution files for evolution number #{index}" if files.size > 1
|
814
417
|
|
815
|
-
|
418
|
+
files[0]
|
816
419
|
end
|
817
420
|
|
818
421
|
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
alias_method :trace, :debug
|
828
|
-
|
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
|
-
)
|
422
|
+
# Returns true if specified path is a directory.
|
423
|
+
# Always check filesystem if file_cache_reload option is set; otherwise, cache result.
|
424
|
+
def directory_exists?(dir)
|
425
|
+
@_config[:file_cache_reload] ? File.directory?(dir) : (
|
426
|
+
@_directory_cache.has_key?(dir) ?
|
427
|
+
@_directory_cache[dir] :
|
428
|
+
@_directory_cache[dir] = File.directory?(dir)
|
429
|
+
)
|
838
430
|
end
|
839
431
|
|
840
|
-
#
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
432
|
+
# TODO: Move file and directory caching to new class(es)
|
433
|
+
# TODO: File cache should store hashes of file info, keyed by path,
|
434
|
+
# instead of using separate hashes (cache time, contents, etc)
|
435
|
+
#
|
436
|
+
# TODO: FIX BUG: The file cache keeps old classes after they are removed from
|
437
|
+
# the action/model class hierarchies. Will the class hierarchies force reload
|
438
|
+
# these paths, or get the old cached versions? Answer: They reload the classes
|
439
|
+
# because they only cache the source text in the file cache. They cache the
|
440
|
+
# classes in the hierarchy.
|
441
|
+
#
|
442
|
+
# TODO: Need a generic class for Action/Template/Model class hierarchy caches.
|
443
|
+
#
|
444
|
+
# Given a file path, caches or returns the file's contents or the return value of
|
445
|
+
# the passed block applied to the file's contents.
|
446
|
+
# If file is not found, the file's contents are nil.
|
447
|
+
def file_cache(path = nil, return_changed_state = false)
|
448
|
+
return @_file_cache unless path
|
449
|
+
|
450
|
+
cache_changed = false
|
451
|
+
|
452
|
+
if @_file_cache.has_key?(path)
|
453
|
+
# we've loaded this path before
|
454
|
+
if @_config[:file_cache_reload]
|
455
|
+
# check to see if there's been a change that needs to be reloaded
|
456
|
+
if !File.file?(path)
|
457
|
+
if @_file_cache_time[path]
|
458
|
+
# file cached as existing but has been removed; update cache to show no file
|
459
|
+
cache_changed = true
|
460
|
+
contents = nil
|
461
|
+
end
|
462
|
+
elsif !@_file_cache_time[path] ||
|
463
|
+
@_file_cache_time[path] < File.mtime(path) ||
|
464
|
+
( File.symlink?(path) && (@_file_cache_time[path] < File.lstat(path).mtime) )
|
465
|
+
# cache shows file missing, or file has been modified since cached
|
466
|
+
cache_changed = true
|
467
|
+
contents = File.read(path)
|
468
|
+
end
|
469
|
+
end
|
470
|
+
else
|
471
|
+
# haven't loaded this path yet
|
472
|
+
cache_changed = true
|
473
|
+
if !File.file?(path)
|
474
|
+
# nil path, of file doesn't exist
|
475
|
+
contents = nil
|
476
|
+
else
|
477
|
+
# file exists; mark cache time and read file
|
478
|
+
contents = File.read(path)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
if cache_changed
|
483
|
+
@_file_cache_time[path] = contents ? Time.now : nil
|
484
|
+
@_file_cache[path] = block_given? ? yield(contents) : contents
|
845
485
|
end
|
486
|
+
|
487
|
+
return_changed_state ? [@_file_cache[path], cache_changed] : @_file_cache[path]
|
846
488
|
end
|
847
489
|
|
490
|
+
# Returns new Kiss::Mailer object using specified options.
|
491
|
+
def new_email(options = {})
|
492
|
+
Kiss::Mailer.new({
|
493
|
+
:controller => self,
|
494
|
+
:request => self
|
495
|
+
}.merge(options))
|
496
|
+
end
|
848
497
|
|
849
|
-
|
498
|
+
def send_email(options = {})
|
499
|
+
new_email(options).send
|
500
|
+
end
|
850
501
|
|
851
502
|
# Returns URL/URI of app root (corresponding to top level of action_dir).
|
503
|
+
# Part of Kiss class to be available to kiss irb.
|
852
504
|
def app_url(options = {})
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
505
|
+
# cache return values by unique options input
|
506
|
+
@_app_url_cache ||= {}
|
507
|
+
@_app_url_cache[options.inspect] ||= begin
|
508
|
+
url_settings = {
|
509
|
+
:protocol => @_protocol || 'http',
|
510
|
+
:host => @_app_host,
|
511
|
+
:uri => @_app_uri
|
512
|
+
}.merge(options)
|
513
|
+
|
514
|
+
raise 'host missing' unless url_settings[:host]
|
515
|
+
|
516
|
+
"#{url_settings[:protocol]}://#{url_settings[:host]}#{url_settings[:uri]}"
|
517
|
+
end
|
864
518
|
end
|
865
|
-
alias_method :asset_url, :pub_url
|
866
519
|
|
867
|
-
|
868
|
-
|
869
|
-
@exception_cache.merge!(data) if data
|
870
|
-
@exception_cache
|
520
|
+
def login
|
521
|
+
{}
|
871
522
|
end
|
872
|
-
alias_method :set_exception_cache, :exception_cache
|
873
523
|
|
874
|
-
|
875
|
-
|
876
|
-
mailer = Kiss::Mailer.new(options)
|
877
|
-
mailer.controller = self
|
878
|
-
mailer
|
524
|
+
def debug(obj, *args)
|
525
|
+
gdebug obj
|
879
526
|
end
|
880
527
|
end
|