kiss 0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|