kiss 0.9
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 +19 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/lib/kiss/action.rb +204 -0
- data/lib/kiss/controller_accessors.rb +101 -0
- data/lib/kiss/exception_report.rb +359 -0
- data/lib/kiss/form/field.rb +296 -0
- data/lib/kiss/form.rb +414 -0
- data/lib/kiss/format.rb +80 -0
- data/lib/kiss/hacks.rb +140 -0
- data/lib/kiss/iterator.rb +56 -0
- data/lib/kiss/mailer.rb +92 -0
- data/lib/kiss/model.rb +114 -0
- data/lib/kiss/rack/bench.rb +131 -0
- data/lib/kiss/rack/email_errors.rb +64 -0
- data/lib/kiss/rack/facebook.rb +28 -0
- data/lib/kiss/rack/file_not_found.rb +42 -0
- data/lib/kiss/rack/log_exceptions.rb +23 -0
- data/lib/kiss/rack/show_debug.rb +82 -0
- data/lib/kiss/rack/show_exceptions.rb +27 -0
- data/lib/kiss/sequel_mysql.rb +23 -0
- data/lib/kiss/sequel_session.rb +166 -0
- data/lib/kiss/template_methods.rb +125 -0
- data/lib/kiss.rb +725 -0
- metadata +108 -0
data/lib/kiss.rb
ADDED
@@ -0,0 +1,725 @@
|
|
1
|
+
# Kiss - A web application framework for Ruby
|
2
|
+
# Copyright (C) 2005-2008 MultiWidget LLC.
|
3
|
+
# See LICENSE for details.
|
4
|
+
|
5
|
+
# Author:: Shawn Van Ittersum, MultiWidget LLC
|
6
|
+
# Copyright:: Copyright (c) 2005-2008 MultiWidget LLC.
|
7
|
+
# License:: MIT X11 License
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'yaml'
|
11
|
+
require 'rack'
|
12
|
+
require 'rack/request'
|
13
|
+
require 'sequel'
|
14
|
+
require 'erubis'
|
15
|
+
|
16
|
+
require 'kiss/hacks'
|
17
|
+
|
18
|
+
require 'kiss/controller_accessors'
|
19
|
+
require 'kiss/template_methods'
|
20
|
+
require 'kiss/action'
|
21
|
+
|
22
|
+
require 'kiss/model'
|
23
|
+
|
24
|
+
module Rack
|
25
|
+
autoload :Bench, 'kiss/rack/bench'
|
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 :ShowDebug, 'kiss/rack/show_debug'
|
31
|
+
autoload :ShowExceptions, 'kiss/rack/show_exceptions'
|
32
|
+
end
|
33
|
+
|
34
|
+
# Kiss - An MVC web application framework for Ruby, built on:
|
35
|
+
# * Erubis template engine
|
36
|
+
# * Sequel database ORM library
|
37
|
+
# * Rack web server abstraction
|
38
|
+
class Kiss
|
39
|
+
autoload :ExceptionReport, 'kiss/exception_report'
|
40
|
+
autoload :Mailer, 'kiss/mailer'
|
41
|
+
autoload :Iterator, 'kiss/iterator'
|
42
|
+
autoload :Form, 'kiss/form'
|
43
|
+
autoload :SequelSession, 'kiss/sequel_session'
|
44
|
+
|
45
|
+
attr_reader :action, :action_subdir, :action_path, :extension, :host, :template_dir,
|
46
|
+
:email_template_dir, :upload_dir, :evolution_dir, :public_dir, :params, :args, :db,
|
47
|
+
:login, :request, :environment, :content_types
|
48
|
+
|
49
|
+
@@default_action = 'index'
|
50
|
+
|
51
|
+
@@cookie_name = 'Kiss'
|
52
|
+
|
53
|
+
@@content_types = {
|
54
|
+
:rhtml => 'text/html',
|
55
|
+
:html => 'text/html',
|
56
|
+
:txt => 'text/plain',
|
57
|
+
:xml => 'application/xml',
|
58
|
+
:doc => 'application/msword',
|
59
|
+
:xls => 'application/application/vnd.ms-excel'
|
60
|
+
}
|
61
|
+
|
62
|
+
# Purposely empty class.
|
63
|
+
# ActionDone exception is raised when render complete to abort execution.
|
64
|
+
# Could probably use throw...catch instead.
|
65
|
+
class ActionDone < Exception; end
|
66
|
+
|
67
|
+
### Class Methods
|
68
|
+
|
69
|
+
# Creates a new Kiss instance and runs it.
|
70
|
+
def self.run(options = {})
|
71
|
+
new(options).run
|
72
|
+
end
|
73
|
+
|
74
|
+
# Converts date strings from m/d/y to y/m/d. Useful for MySQL.
|
75
|
+
def self.mdy_to_ymd(date)
|
76
|
+
return '0000-00-00' unless date && date =~ /\S/
|
77
|
+
date.sub!(/\A\D+/,'')
|
78
|
+
date.sub!(/\D+\Z/,'')
|
79
|
+
|
80
|
+
month, day, year = date.split(/\D+/,3).each {|s| s.to_i}
|
81
|
+
|
82
|
+
return 0 unless month && day
|
83
|
+
|
84
|
+
current_year = Time.now.year
|
85
|
+
if !year || year.length == 0
|
86
|
+
# use current year if year is missing.
|
87
|
+
year = current_year
|
88
|
+
else
|
89
|
+
# convert two-digit years to four-digit years
|
90
|
+
year = year.to_i
|
91
|
+
if year < 100
|
92
|
+
year += 1900
|
93
|
+
year += 100 if year < current_year - 95
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
return sprintf("%04d-%02d-%02d",year,month.to_i,day.to_i)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns regexp and error message for validating specified format.
|
101
|
+
# Will likely be rewritten in a future version of Kiss.
|
102
|
+
def self.format_details(format)
|
103
|
+
if format.is_a?(Regexp)
|
104
|
+
return format, 'invalid value'
|
105
|
+
end
|
106
|
+
|
107
|
+
case format
|
108
|
+
when :integer
|
109
|
+
return /\A\-?\d+\Z/,
|
110
|
+
'must be an integer'
|
111
|
+
|
112
|
+
when :integer_positive, :positive_integer, :id
|
113
|
+
return /\A\d*[1-9]\d*\Z/,
|
114
|
+
'must be a positive integer'
|
115
|
+
|
116
|
+
when :integer_unsigned, :unsigned_integer, :id_or_zero, :id_zero
|
117
|
+
return /\A\d+\Z/,
|
118
|
+
'must be a positive integer or zero'
|
119
|
+
|
120
|
+
when :integer_negative, :negative_integer
|
121
|
+
return /\A\-\d*[1-9]\d*\Z/,
|
122
|
+
'must be a negative integer'
|
123
|
+
|
124
|
+
when :decimal
|
125
|
+
return /\A\-?(\d+(\.\d*)?|\.\d+)\Z/,
|
126
|
+
'must be a decimal number'
|
127
|
+
|
128
|
+
when :alphanum
|
129
|
+
return /\A[a-z0-9]+\Z/,
|
130
|
+
'only letters and numbers'
|
131
|
+
|
132
|
+
when :word
|
133
|
+
return /\A\w+\Z/,
|
134
|
+
'only letters, numbers, and _'
|
135
|
+
|
136
|
+
when :email_address
|
137
|
+
return /\A[A-Z0-9._%+-]+\@([A-Z0-9-]+\.)+[A-Z]{2,4}\Z/i,
|
138
|
+
'must be a valid email address'
|
139
|
+
|
140
|
+
when :date
|
141
|
+
return /\A\d+\D\d+(\D\d+)?\Z/,
|
142
|
+
'must be a valid date'
|
143
|
+
|
144
|
+
when :time
|
145
|
+
return /\A\d+\:\d+\s*[ap]m\Z/i,
|
146
|
+
'must be a valid time'
|
147
|
+
|
148
|
+
when :datetime
|
149
|
+
return /\A\d+\D\d+(\D\d+)?\s+\d{1,2}\:\d{2}\s*[ap]m\Z/i,
|
150
|
+
'must be a valid date and time'
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Validates value against specified format.
|
156
|
+
# If required is true, value must contain a non-whitespace character.
|
157
|
+
# If required is false, value need not match format if and only if value contains only whitespace.
|
158
|
+
def self.validate_value(value,format,required = false)
|
159
|
+
if required && (value !~ /\S/)
|
160
|
+
# value required
|
161
|
+
return 'required'
|
162
|
+
elsif format && (value =~ /\S/)
|
163
|
+
regexp,error = format_details(format)
|
164
|
+
return error unless value =~ regexp
|
165
|
+
end
|
166
|
+
|
167
|
+
return nil
|
168
|
+
end
|
169
|
+
|
170
|
+
# Generates string of random text of the specified length.
|
171
|
+
def self.random_text(length)
|
172
|
+
chars = ('A'..'Z').to_a + ('0'..'9').to_a # array
|
173
|
+
|
174
|
+
text = ''
|
175
|
+
size = chars.size
|
176
|
+
length.times { text += chars[rand(size)] }
|
177
|
+
|
178
|
+
text
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns exception cache, for use in Kiss::ExceptionReport.
|
182
|
+
def self.exception_cache
|
183
|
+
@@exception_cache
|
184
|
+
end
|
185
|
+
|
186
|
+
### Instance Methods
|
187
|
+
|
188
|
+
# Adds specified data to exception cache, to be included in reports generated
|
189
|
+
# by Kiss::ExceptionReport in case of an exception.
|
190
|
+
def set_exception_cache(data)
|
191
|
+
@@exception_cache.merge!(data)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Clears exception cache.
|
195
|
+
def clear_exception_cache
|
196
|
+
@@exception_cache = {}
|
197
|
+
end
|
198
|
+
|
199
|
+
# Generates string of random text of the specified length.
|
200
|
+
def random_text(*args)
|
201
|
+
self.class.random_text(*args)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Returns URL/URI of app root (corresponding to top level of action_dir).
|
205
|
+
def app(suffix = nil)
|
206
|
+
suffix ? @app_url + suffix : @app_url
|
207
|
+
end
|
208
|
+
|
209
|
+
# Returns path of current action, under action_dir.
|
210
|
+
def action
|
211
|
+
@action
|
212
|
+
end
|
213
|
+
|
214
|
+
# Kiss Model cache, used to invoke and store Kiss database models.
|
215
|
+
#
|
216
|
+
# Example:
|
217
|
+
# models[:users] : database model for `users' table
|
218
|
+
def models
|
219
|
+
@dbm
|
220
|
+
end
|
221
|
+
alias_method :dbm, :models
|
222
|
+
|
223
|
+
# Returns URL/URI of app's static assets (asset_host or public_uri).
|
224
|
+
def pub(suffix = nil)
|
225
|
+
@pub ||= @options[:asset_host] || (@request.server + @options[:public_uri].to_s)
|
226
|
+
suffix ? @pub + '/' + suffix : @pub
|
227
|
+
end
|
228
|
+
|
229
|
+
# Returns true if specified path is a directory.
|
230
|
+
# Cache result if file_cache_no_reload option is set; otherwise, always check filesystem.
|
231
|
+
def directory_exists?(dir)
|
232
|
+
@options[:file_cache_no_reload] ? (
|
233
|
+
@directory_cache.has_key?(path) ?
|
234
|
+
@directory_cache[dir] :
|
235
|
+
@directory_cache[dir] = File.directory?(dir)
|
236
|
+
) : File.directory?(dir)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Caches the specified file and return its contents.
|
240
|
+
# If block given, executes block on contents, then cache and return block result.
|
241
|
+
# If fnf_file_type given, raises exception (of type fnf_exception_class) if file is not found.
|
242
|
+
def file_cache(path, fnf_file_type = nil, fnf_exception_class = Kiss::FileNotFound)
|
243
|
+
if (@options[:file_cache_no_reload] && @file_cache.has_key?(path)) || @files_cached_this_request[path]
|
244
|
+
return @file_cache[path]
|
245
|
+
end
|
246
|
+
|
247
|
+
@files_cached_this_request[path] = true
|
248
|
+
|
249
|
+
if !File.file?(path)
|
250
|
+
raise fnf_exception_class, "#{fnf_file_type} file missing: '#{path}'" if fnf_file_type
|
251
|
+
|
252
|
+
@file_cache[path] = nil
|
253
|
+
contents = nil
|
254
|
+
else
|
255
|
+
# expire cache if file (or symlink) modified
|
256
|
+
# TODO: what about symlinks to symlinks?
|
257
|
+
if !@file_cache_time[path] ||
|
258
|
+
@file_cache_time[path] < File.mtime(path) ||
|
259
|
+
( File.symlink?(path) && (@file_cache_time[path] < File.lstat(path).mtime) )
|
260
|
+
|
261
|
+
@file_cache[path] = nil
|
262
|
+
@file_cache_time[path] = Time.now
|
263
|
+
contents = File.read(path)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
@file_cache[path] ||= begin
|
268
|
+
(block_given?) ? yield(contents) : contents
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Merges specified options into previously defined/merged Kiss options.
|
273
|
+
def merge_options(config_options)
|
274
|
+
if config_options
|
275
|
+
if env_vars = config_options.delete(:ENV)
|
276
|
+
env_vars.each_pair {|k,v| ENV[k] = v }
|
277
|
+
end
|
278
|
+
if lib_dirs = config_options.delete(:lib_dirs)
|
279
|
+
@lib_dirs.push( lib_dirs )
|
280
|
+
end
|
281
|
+
if gem_dirs = config_options.delete(:gem_dirs)
|
282
|
+
@gem_dirs.push( gem_dirs )
|
283
|
+
end
|
284
|
+
if require_libs = config_options.delete(:require)
|
285
|
+
@require.push( require_libs )
|
286
|
+
end
|
287
|
+
|
288
|
+
@options.merge!( config_options )
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Create a new Kiss application instance, based on specified options.
|
293
|
+
def initialize(loader_options = {})
|
294
|
+
# store cached files
|
295
|
+
@file_cache = {}
|
296
|
+
@directory_cache = {}
|
297
|
+
@file_cache_time = {}
|
298
|
+
|
299
|
+
# if loader_options is string, then it specifies environment
|
300
|
+
# else it should be a hash of config options
|
301
|
+
if loader_options.is_a?(String)
|
302
|
+
loader_options = { :environment => loader_options }
|
303
|
+
end
|
304
|
+
|
305
|
+
# environment
|
306
|
+
@environment = loader_options[:environment]
|
307
|
+
|
308
|
+
# directories
|
309
|
+
script_dir = $0.sub(/[^\/]+\Z/,'')
|
310
|
+
script_dir = '' if script_dir == './'
|
311
|
+
@project_dir = script_dir + (loader_options[:project_dir] || loader_options[:root_dir] || '..')
|
312
|
+
Dir.chdir(@project_dir)
|
313
|
+
|
314
|
+
@config_dir = loader_options[:config_dir] || 'config'
|
315
|
+
|
316
|
+
# get environment name from config/environment
|
317
|
+
if (@environment.nil?) && File.file?(env_file = @config_dir+'/environment')
|
318
|
+
@environment = File.read(env_file).sub(/\s+\Z/,'')
|
319
|
+
end
|
320
|
+
|
321
|
+
# init options
|
322
|
+
@options = {}
|
323
|
+
@lib_dirs = ['lib']
|
324
|
+
@gem_dirs = ['gems']
|
325
|
+
@require = []
|
326
|
+
|
327
|
+
# common (shared) config
|
328
|
+
if (File.file?(config_file = @config_dir+'/common.yml'))
|
329
|
+
merge_options( YAML::load(File.read(config_file)) )
|
330
|
+
end
|
331
|
+
# environment config
|
332
|
+
if (File.file?(config_file = "#{@config_dir}/environments/#{@environment}.yml"))
|
333
|
+
merge_options( YAML::load(File.read(config_file)) )
|
334
|
+
end
|
335
|
+
|
336
|
+
merge_options( loader_options )
|
337
|
+
|
338
|
+
# set class vars from options
|
339
|
+
@action_dir = @options[:action_dir] || 'actions'
|
340
|
+
@template_dir = @options[:template_dir] ? @options[:template_dir] : @action_dir
|
341
|
+
|
342
|
+
@public_dir = @options[:public_dir] || 'public_html'
|
343
|
+
|
344
|
+
@model_dir = @options[:model_dir] || 'models'
|
345
|
+
@evolution_dir = @options[:evolution_dir] || 'evolutions'
|
346
|
+
|
347
|
+
@email_template_dir = @options[:email_template_dir] || 'email_templates'
|
348
|
+
@upload_dir = @options[:upload_dir] || 'uploads'
|
349
|
+
|
350
|
+
@cookie_name = @options[:cookie_name] || 'Kiss'
|
351
|
+
@default_action = @options[:default_action] || @@default_action
|
352
|
+
@action_root_class = @options[:action_class] || Class.new(Kiss::Action)
|
353
|
+
|
354
|
+
# include lib dirs
|
355
|
+
$LOAD_PATH.unshift(*( @lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
|
356
|
+
|
357
|
+
# add gem dir to rubygems search path
|
358
|
+
Gem.path.unshift(*( @gem_dirs.flatten.select {|dir| File.directory?(dir) } ))
|
359
|
+
|
360
|
+
# require libs
|
361
|
+
@require.flatten.each {|lib| require lib }
|
362
|
+
|
363
|
+
# session class
|
364
|
+
if (@options[:session_class])
|
365
|
+
@session_class = (@options[:session_class].class == Class) ? @options[:session_class] : @options[:session_class].to_const
|
366
|
+
end
|
367
|
+
|
368
|
+
# load extensions to action class
|
369
|
+
action_extension_path = @action_dir + '/_action.rb'
|
370
|
+
if File.file?(action_extension_path)
|
371
|
+
@action_root_class.class_eval(File.read(action_extension_path),action_extension_path) rescue nil
|
372
|
+
end
|
373
|
+
|
374
|
+
# set controller access variables
|
375
|
+
@action_root_class.set_controller(self)
|
376
|
+
Kiss::Model.set_controller(self)
|
377
|
+
Kiss::Mailer.set_controller(self)
|
378
|
+
|
379
|
+
# database
|
380
|
+
if sequel = @options[:database]
|
381
|
+
# open database connection (if not already open)
|
382
|
+
@db = sequel.is_a?(String) ? (Sequel.open sequel) : sequel.is_a?(Hash) ? (Sequel.open sequel) : sequel
|
383
|
+
# add query logging to database class
|
384
|
+
@db.class.class_eval do
|
385
|
+
@@query = nil
|
386
|
+
def self.last_query
|
387
|
+
@@query
|
388
|
+
end
|
389
|
+
|
390
|
+
alias_method :execute_old, :execute
|
391
|
+
def execute(sql, *args, &block)
|
392
|
+
@@query = sql
|
393
|
+
execute_old(sql, *args, &block)
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
if @db.class.name == 'Sequel::MySQL::Database'
|
398
|
+
# fix sequel mysql bugs; add all_rows
|
399
|
+
require 'kiss/sequel_mysql'
|
400
|
+
Sequel.convert_tinyint_to_bool = false unless @options[:convert_tinyint_to_bool]
|
401
|
+
end
|
402
|
+
|
403
|
+
# create models cache
|
404
|
+
@dbm = Kiss::ModelCache.new(self,@model_dir)
|
405
|
+
end
|
406
|
+
|
407
|
+
# setup session storage, if session class specified in config
|
408
|
+
@session_class.setup_storage(self) if @session_class
|
409
|
+
|
410
|
+
self
|
411
|
+
end
|
412
|
+
|
413
|
+
# Sets up Rack builder options and begins Rack execution.
|
414
|
+
def run(options = {})
|
415
|
+
merge_options(options)
|
416
|
+
|
417
|
+
app = self
|
418
|
+
builder_options = @options[:rack_builder] || []
|
419
|
+
rack = Rack::Builder.new do
|
420
|
+
builder_options.each do |builder_option|
|
421
|
+
if builder_option.is_a?(Array)
|
422
|
+
builder_args = builder_option
|
423
|
+
builder_option = builder_args.shift
|
424
|
+
else
|
425
|
+
builder_args = []
|
426
|
+
end
|
427
|
+
|
428
|
+
unless builder_option.is_a?(Class)
|
429
|
+
builder_option = Rack.const_get(builder_option.to_s)
|
430
|
+
end
|
431
|
+
|
432
|
+
use(builder_option,*builder_args)
|
433
|
+
end
|
434
|
+
|
435
|
+
run app
|
436
|
+
end
|
437
|
+
|
438
|
+
handler = @options[:rack_handler] || Rack::Handler::WEBrick
|
439
|
+
if !handler.is_a?(Class)
|
440
|
+
handler = Rack::Handler.const_get(handler.to_s)
|
441
|
+
end
|
442
|
+
handler.run(rack,@options[:rack_handler_options] || {:Port => 4000})
|
443
|
+
end
|
444
|
+
|
445
|
+
# Processes and responds to requests received via Rack.
|
446
|
+
# Returns Rack::Response object.
|
447
|
+
def call(env)
|
448
|
+
clear_exception_cache
|
449
|
+
|
450
|
+
check_evolution_number if @db
|
451
|
+
|
452
|
+
@files_cached_this_request = {}
|
453
|
+
|
454
|
+
get_request(env)
|
455
|
+
|
456
|
+
@response = Rack::Response.new
|
457
|
+
|
458
|
+
begin
|
459
|
+
parse_action_path(@path)
|
460
|
+
|
461
|
+
setup_session
|
462
|
+
if login_session_valid?
|
463
|
+
load_from_login_session
|
464
|
+
elsif @options[:authenticate_all]
|
465
|
+
if (!@options[:authenticate_exclude].is_a?(Array) ||
|
466
|
+
@options[:authenticate_exclude].select {|action| action == @action}.size == 0)
|
467
|
+
authenticate
|
468
|
+
end
|
469
|
+
end
|
470
|
+
process.render
|
471
|
+
rescue Kiss::ActionDone
|
472
|
+
end
|
473
|
+
finalize_session if @session
|
474
|
+
|
475
|
+
@response.finish
|
476
|
+
end
|
477
|
+
|
478
|
+
# Sets up request-specified variables based in request information received from Rack.
|
479
|
+
def get_request(env)
|
480
|
+
@request = Rack::Request.new(env)
|
481
|
+
|
482
|
+
@app_host = @options[:app_host] ? ('http://' + @options[:app_host]) : @request.server
|
483
|
+
@app_uri = @options[:app_uri] || @request.script_name || ''
|
484
|
+
@app_url = @app_host + @app_uri
|
485
|
+
|
486
|
+
@path = @request.path_info || '/'
|
487
|
+
@params = @request.params
|
488
|
+
|
489
|
+
@host ||= @request.host
|
490
|
+
@protocol = env['HTTPS'] == 'on' ? 'https' : 'http'
|
491
|
+
end
|
492
|
+
|
493
|
+
# Returns Sequel dataset to evolution_number table, which specifies app's current evolution number.
|
494
|
+
# Creates evolution_number table if it does not exist.
|
495
|
+
def evolution_number_table
|
496
|
+
unless db.table_exists?(:evolution_number)
|
497
|
+
db.create_table :evolution_number do
|
498
|
+
column :version, :integer, :null=> false
|
499
|
+
end
|
500
|
+
db[:evolution_number].insert(:version => 0)
|
501
|
+
end
|
502
|
+
db[:evolution_number]
|
503
|
+
end
|
504
|
+
|
505
|
+
# Returns app's current evolution number.
|
506
|
+
def evolution_number
|
507
|
+
evolution_number_table.first.version
|
508
|
+
end
|
509
|
+
|
510
|
+
# Sets app's current evolution number.
|
511
|
+
def evolution_number=(version)
|
512
|
+
evolution_number_table.update(:version => version)
|
513
|
+
end
|
514
|
+
|
515
|
+
# Check whether there exists a file in evolution_dir whose number is greater than app's
|
516
|
+
# current evolution number. If so, raise an error to indicate need to apply new evolutions.
|
517
|
+
def check_evolution_number
|
518
|
+
version = evolution_number
|
519
|
+
if directory_exists?(@evolution_dir) &&
|
520
|
+
Dir.entries(@evolution_dir).select { |f| f =~ /\A0*#{version+1}_/ }.size > 0
|
521
|
+
raise "current evolution #{version} is outdated; apply evolutions or update evolution number"
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
# Loads session from session store (specified by session_class).
|
526
|
+
def setup_session
|
527
|
+
@login = {}
|
528
|
+
@session = @session_class ? begin
|
529
|
+
session = @session_class.persist(@request.cookies[@cookie_name])
|
530
|
+
@_fingerprint = Marshal.dump(session.data).hash
|
531
|
+
|
532
|
+
cookie_vars = {
|
533
|
+
:value => session.values[:session_id],
|
534
|
+
:path => @options[:cookie_path] || @app_uri,
|
535
|
+
:domain => @options[:cookie_domain] || @request.host
|
536
|
+
}
|
537
|
+
cookie_vars[:expires] = Time.now + @options[:cookie_lifespan] if @options[:cookie_lifespan]
|
538
|
+
|
539
|
+
# set_cookie here or at render time
|
540
|
+
@response.set_cookie @cookie_name, cookie_vars
|
541
|
+
@login.merge!(session[:login]) if session[:login]
|
542
|
+
|
543
|
+
session
|
544
|
+
end : {}
|
545
|
+
end
|
546
|
+
|
547
|
+
# Saves session to session store, if session data has changed since load.
|
548
|
+
def finalize_session
|
549
|
+
@session.save if @_fingerprint != Marshal.dump(@session.data).hash
|
550
|
+
end
|
551
|
+
|
552
|
+
def session
|
553
|
+
@session
|
554
|
+
end
|
555
|
+
|
556
|
+
##### LOGIN SESSION #####
|
557
|
+
|
558
|
+
# Empties request and session login hashes.
|
559
|
+
def reset_login_session
|
560
|
+
@session[:login] = @login = {}
|
561
|
+
end
|
562
|
+
alias_method :reset_login_data, :reset_login_session
|
563
|
+
|
564
|
+
# Merges data hash (key-value pairs) into request login hash.
|
565
|
+
def set_login_data(data)
|
566
|
+
@login = @login.merge(data)
|
567
|
+
end
|
568
|
+
|
569
|
+
# Merges data hash (key-value pairs) into session login hash.
|
570
|
+
def set_login_session(data)
|
571
|
+
set_login_data(data)
|
572
|
+
@session[:login] = (@session[:login] || {}).merge(data)
|
573
|
+
end
|
574
|
+
|
575
|
+
# Sets expire time of session login hash, after which time it will be reset (emptied).
|
576
|
+
# If given a FixNum, sets expire time to now plus number of seconds.
|
577
|
+
def set_login_expires(time)
|
578
|
+
time = Time.now + time if time.is_a?(Fixnum)
|
579
|
+
set_login_session(:expires_at => time)
|
580
|
+
end
|
581
|
+
|
582
|
+
# Returns true if login hash is defined and not expired.
|
583
|
+
def login_session_valid?
|
584
|
+
@login && !login_session_expired?
|
585
|
+
end
|
586
|
+
|
587
|
+
# Returns true if login hash is expired.
|
588
|
+
def login_session_expired?
|
589
|
+
@login && (!@login[:expires_at] || @login[:expires_at] < Time.now)
|
590
|
+
end
|
591
|
+
|
592
|
+
# Calls login action's load_from_session method to populate request login hash.
|
593
|
+
def load_from_login_session
|
594
|
+
klass = action_class('login')
|
595
|
+
raise 'load_from_login_session called, but no login action found' unless klass
|
596
|
+
|
597
|
+
action_handler = klass.new
|
598
|
+
action_handler.load_from_session
|
599
|
+
end
|
600
|
+
|
601
|
+
# Returns path to login action.
|
602
|
+
def login_path
|
603
|
+
@action_dir + '/login.rb'
|
604
|
+
end
|
605
|
+
|
606
|
+
# If valid login session exists, loads login action to populate request login hash data.
|
607
|
+
# Otherwise, loads and calls login action to authenticate user.
|
608
|
+
def authenticate
|
609
|
+
if login_session_valid?
|
610
|
+
load_from_login_session
|
611
|
+
else
|
612
|
+
klass = action_class('login')
|
613
|
+
raise 'authenticate called, but no login action found' unless klass
|
614
|
+
old_extension = @extension
|
615
|
+
@extension = 'rhtml'
|
616
|
+
process(klass,login_path)
|
617
|
+
@extension = old_extension
|
618
|
+
|
619
|
+
unless login_session_valid?
|
620
|
+
#raise 'login action completed without setting valid login session'
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|
624
|
+
|
625
|
+
##### ACTION METHODS #####
|
626
|
+
|
627
|
+
def action_dir
|
628
|
+
@action_dir
|
629
|
+
end
|
630
|
+
|
631
|
+
# Creates and caches anonymous class with which to invoke specified (or current) action.
|
632
|
+
def action_class(action = @action)
|
633
|
+
action_path = @action_dir + '/' + action.to_s + '.rb'
|
634
|
+
return nil unless action_path.is_a?(String) && action_path.length > 0
|
635
|
+
|
636
|
+
file_cache(action_path) do |src|
|
637
|
+
# create new action class, subclass of shared action parent class
|
638
|
+
klass = Class.new(@action_root_class)
|
639
|
+
klass.class_eval(src,action_path) if src
|
640
|
+
klass
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
# Parses request URI to determine action path and arguments.
|
645
|
+
def parse_action_path(path)
|
646
|
+
@action_subdir = ''
|
647
|
+
@action = nil
|
648
|
+
|
649
|
+
redirect_url(app + '/') if path == ''
|
650
|
+
|
651
|
+
path += @default_action if path =~ /\/\Z/
|
652
|
+
|
653
|
+
parts = path.sub(/^\/*/,'').split('/')
|
654
|
+
|
655
|
+
while part = parts.shift
|
656
|
+
raise 'bad action' if part !~ /\A[a-z0-9][\w\-\.]*\Z/i
|
657
|
+
|
658
|
+
test_path = @action_dir + @action_subdir + '/' + part
|
659
|
+
if directory_exists?(test_path)
|
660
|
+
@action_subdir += '/' + part
|
661
|
+
next
|
662
|
+
end
|
663
|
+
|
664
|
+
if part =~ /\A(.+)\.(\w+)\Z/
|
665
|
+
@extension = $2
|
666
|
+
part = $1
|
667
|
+
else
|
668
|
+
@extension = 'rhtml'
|
669
|
+
end
|
670
|
+
|
671
|
+
@action = @action_subdir + '/' + part
|
672
|
+
break
|
673
|
+
end
|
674
|
+
|
675
|
+
# if no action, must have traversed all parts to a directory
|
676
|
+
# add a trailing slash and try again
|
677
|
+
redirect_url(app + '/' + path + '/') unless @action
|
678
|
+
|
679
|
+
@action_path = @action_dir + '/' + @action + '.rb'
|
680
|
+
|
681
|
+
# keep rest of path_info in args
|
682
|
+
@args = parts
|
683
|
+
end
|
684
|
+
|
685
|
+
# Processes specified (or current) action, by instantiating its anonymous
|
686
|
+
# action class and invoking `call' method on the instance.
|
687
|
+
def process(klass = action_class, action_path = @action_path)
|
688
|
+
action_handler = klass.new
|
689
|
+
action_handler.call
|
690
|
+
|
691
|
+
# return handler to follow with render
|
692
|
+
action_handler
|
693
|
+
end
|
694
|
+
|
695
|
+
##### RACK OUTPUT METHODS #####
|
696
|
+
|
697
|
+
# Prepares Rack::Response object to be returned to Rack.
|
698
|
+
# Raises Kiss::ActionDone exception to bypass caller stack and return directly
|
699
|
+
# to Kiss#call.
|
700
|
+
def send_response(output = '',options = {})
|
701
|
+
content_type = options[:content_type] || @content_type || extension ? @@content_types[extension.to_sym] : nil
|
702
|
+
document_encoding ||= 'utf-8'
|
703
|
+
|
704
|
+
@response['Content-Type'] = "#{content_type}; #{document_encoding}" if content_type
|
705
|
+
@response['Content-Length'] = output.length.to_s
|
706
|
+
@response.body = output
|
707
|
+
|
708
|
+
raise Kiss::ActionDone
|
709
|
+
|
710
|
+
# back to Kiss#call, which finalizes session and returns @response
|
711
|
+
# (throws exception if no @response set)
|
712
|
+
end
|
713
|
+
|
714
|
+
# Sends HTTP 302 response to redirect client browser agent to specified URL.
|
715
|
+
def redirect_url(url)
|
716
|
+
@response.status = 302
|
717
|
+
@response['Location'] = url
|
718
|
+
send_response
|
719
|
+
end
|
720
|
+
|
721
|
+
# Returns new Kiss::Mailer object using specified options.
|
722
|
+
def new_email(*options)
|
723
|
+
Kiss::Mailer.new(*options)
|
724
|
+
end
|
725
|
+
end
|