kiss 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,92 @@
1
+ class Kiss
2
+ # This class creates, renders, and sends email messages.
3
+ class Mailer
4
+ include Kiss::TemplateMethods
5
+
6
+ # Class Methods
7
+
8
+ def self.set_controller(controller)
9
+ @@controller = controller
10
+ end
11
+
12
+ # Instance Methods
13
+
14
+ # Creates new email message object.
15
+ def initialize(options = {})
16
+ @options = {
17
+ :engine => :sendmail
18
+ }.merge(options)
19
+
20
+ @data = {}
21
+ end
22
+
23
+ # Invokes controller's file_cache.
24
+ def file_cache(*args,&block)
25
+ controller.file_cache(*args,&block)
26
+ end
27
+
28
+ def controller
29
+ @@controller
30
+ end
31
+
32
+ # Renders email template to string, unless message option is
33
+ # already set to a string value.
34
+ def prepare_email_message(options = {})
35
+ @options.merge!(options)
36
+
37
+ unless @options[:message].is_a?(String)
38
+ if template_name = @options[:template]
39
+ @template_dir = @@controller.email_template_dir
40
+ raise 'email_template_dir path not set' unless @template_dir
41
+
42
+ data = vars = @options[:data] || @options[:vars]
43
+
44
+ path = "#{@template_dir}/#{template_name}"
45
+ @options[:message] = erubis(path,binding)
46
+ else
47
+ raise 'email message not defined'
48
+ end
49
+ end
50
+
51
+ return @options[:message]
52
+ end
53
+
54
+ # Sets data to be passed to email template.
55
+ def set(key,value)
56
+ @data[key] = value
57
+ end
58
+
59
+ # Attempts to send message using SMTP, unless :engine option is set to
60
+ # :sendmail.
61
+ def send(options)
62
+ @options.merge!(options)
63
+ if options[:engine] == :sendmail
64
+ return sendmail
65
+ else
66
+ return send_smtp
67
+ end
68
+ end
69
+
70
+ # Attempts to send message using /usr/sbin/sendmail.
71
+ def sendmail(options = {})
72
+ prepare_email_message(options)
73
+
74
+ IO.popen(@options[:sendmail_path] || "/usr/sbin/sendmail -t","w") do |pipe|
75
+ pipe.puts(@options[:message])
76
+ end
77
+ end
78
+
79
+ # Attempts to send message using Net::SMTP.
80
+ def send_smtp(options = {})
81
+ prepare_email_message(options)
82
+
83
+ require 'net/smtp' unless defined?(Net::SMTP)
84
+ # begin
85
+ Net::SMTP.start('localhost') do |smtp|
86
+ smtp.sendmail(@options[:message], options[:from], options[:to])
87
+ end
88
+ # rescue
89
+ # end
90
+ end
91
+ end
92
+ end
data/lib/kiss/model.rb ADDED
@@ -0,0 +1,114 @@
1
+ class Kiss
2
+ # This class adds functionality to Sequel::Model and automatically loads
3
+ # model class definitions from model_dir. It also uses Kiss#file_cache
4
+ # to cache database model classes, unless no model_dir is specified.
5
+ class Model < Sequel::Model
6
+ class << self
7
+ def name
8
+ @table.to_s
9
+ end
10
+
11
+ # Name symbol for default foreign key
12
+ def default_remote_key
13
+ :"#{name.singularize.demodulize.underscore}_id"
14
+ end
15
+
16
+ def set_controller(controller)
17
+ @@controller = controller
18
+ end
19
+
20
+ def set_dataset(source)
21
+ super(source)
22
+ end
23
+
24
+ def table=(table)
25
+ @table = table
26
+ end
27
+
28
+ def controller
29
+ @@controller
30
+ end
31
+
32
+ def dbm
33
+ @@controller.dbm
34
+ end
35
+
36
+ # TODO: Fix has_many and many_to_many associations
37
+ def associate(type, name, opts = {}, &block)
38
+ opts = opts.clone
39
+
40
+ unless opts[:class] || opts[:class_name]
41
+ opts[:class_name] = name.to_s.pluralize
42
+ end
43
+
44
+ super(type, name, opts, &block)
45
+
46
+ association_reflections[name]
47
+ end
48
+ end
49
+
50
+ def method_missing(meth)
51
+ raise NoMethodError, "undefined method `#{meth}' for database model `#{self.class.name}'"
52
+ end
53
+
54
+ def controller
55
+ @@controller
56
+ end
57
+
58
+ include Kiss::ControllerAccessors
59
+ end
60
+
61
+ class ModelCache
62
+ def initialize(controller,model_dir = nil)
63
+ @controller = controller
64
+ @model_dir = model_dir && File.directory?(model_dir) ? model_dir : nil
65
+ @cache = {}
66
+ end
67
+
68
+ def [](source)
69
+ (@model_dir && source.is_a?(Symbol)) ? begin
70
+ # use controller's file_cache
71
+ model_path = "#{@model_dir}/#{source}.rb"
72
+ @controller.file_cache(model_path) do |src|
73
+ klass = Class.new(Kiss::Model)
74
+ klass.set_dataset(Model.db[source])
75
+ klass.table = source
76
+ klass.class_eval(src,model_path) if src
77
+ klass
78
+ end
79
+ end : begin
80
+ # no model_dir, or source is not a symbol
81
+ # no mapping from source to filesystem path
82
+ # use ModelCache's own cache
83
+ @cache[source] ||= begin
84
+ klass = Class.new(Kiss::Model)
85
+ klass.set_dataset(Model.db[source])
86
+ klass.table = source if source.is_a?(Symbol)
87
+ klass
88
+ end
89
+ end
90
+ end
91
+
92
+ def db
93
+ Sequel::Model.db
94
+ end
95
+
96
+ def literal(*args)
97
+ Sequel::Model.dataset.literal(*args)
98
+ end
99
+ alias_method :quote, :literal
100
+
101
+ def mdy_to_ymd(*args)
102
+ Kiss.mdy_to_ymd(*args)
103
+ end
104
+ end
105
+ end
106
+
107
+ Sequel::Model::Associations::AssociationReflection.class_eval do
108
+ def associated_class
109
+ self[:class] ||= Kiss::Model.controller.dbm[self[:class_name].to_s.pluralize.to_sym]
110
+ end
111
+ def default_left_key
112
+ :"#{self[:model].name.singularize.underscore}_id"
113
+ end
114
+ end
@@ -0,0 +1,131 @@
1
+ def bench(label = nil)
2
+ Rack::Bench.close_bench_item(Kernel.caller[0])
3
+ Rack::Bench.on
4
+
5
+ if label
6
+ Rack::Bench.push_bench_item(
7
+ :label => label,
8
+ :start_time => Time.now,
9
+ :start_context => Kernel.caller[0]
10
+ )
11
+ end
12
+ end
13
+
14
+ module Rack
15
+ # Rack::Bench adds benchmarking capabilities to Kiss applications.
16
+ #
17
+ # bench(label) starts a new timer, which ends upon the next call to
18
+ # bench, or when execution returns to Rack::Bench.
19
+ #
20
+ # bench can be called without a label to end the previous timer
21
+ # without starting a new one.
22
+ #
23
+ # Total request duration is also displayed for any request in which
24
+ # the bench function is called.
25
+ class Bench
26
+ def self.on
27
+ @@bench = true
28
+ end
29
+
30
+ def self.close_bench_item(end_context = nil)
31
+ if @@bench_items[-1] && !@@bench_items[-1][:end_time]
32
+ @@bench_items[-1][:end_time] = Time.now
33
+ @@bench_items[-1][:end_context] = end_context
34
+ end
35
+ end
36
+
37
+ def self.push_bench_item(item)
38
+ @@bench_items.push(item)
39
+ end
40
+
41
+ def initialize(app)
42
+ @app = app
43
+ end
44
+
45
+ def call(env)
46
+ @@bench = false
47
+ @@bench_items = []
48
+
49
+ start_time = Time.now
50
+ code, headers, body = @app.call(env)
51
+ end_time = Time.now
52
+
53
+ if @@bench
54
+ Rack::Bench.close_bench_item
55
+ contents = <<-EOT
56
+ <style>
57
+ .kiss_bench {
58
+ text-align: left;
59
+ padding: 3px 7px;
60
+ border: 1px solid #ec4;
61
+ border-top: 1px solid #fff4bb;
62
+ border-bottom: 1px solid #d91;
63
+ background-color: #ffe590;
64
+ font-size: 12px;
65
+ color: #101;
66
+ }
67
+ .kiss_bench a {
68
+ color: #930;
69
+ text-decoration: none;
70
+ }
71
+ .kiss_bench a:hover {
72
+ color: #930;
73
+ text-decoration: underline;
74
+ }
75
+ </style>
76
+ EOT
77
+
78
+ contents += @@bench_items.map do |item|
79
+ start_link = context_link(item[:start_context])
80
+ end_link = context_link(item[:end_context])
81
+
82
+ <<-EOT
83
+ <div class="kiss_bench">
84
+ <tt><b>#{item[:label].gsub(/\</,'&lt;')} duration: #{sprintf("%0.3f",item[:end_time].to_f - item[:start_time].to_f)} s</b></tt>
85
+ <small style="line-height: 105%; display: block; padding-bottom: 3px">kiss bench<br/>started at #{start_link}<br/>ended at #{end_link || 'return to kiss bench'}</small>
86
+ </div>
87
+ EOT
88
+ end.join
89
+
90
+ contents += <<-EOT
91
+ <div class="kiss_bench">
92
+ <tt><b>TOTAL request duration: #{sprintf("%0.3f",end_time.to_f - start_time.to_f)} s</b></tt>
93
+ <br><small>kiss bench request total</small>
94
+ </div>
95
+ EOT
96
+ else
97
+ contents = ''
98
+ end
99
+
100
+ body.each {|p| contents += p }
101
+ headers['Content-Length'] = contents.length.to_s
102
+
103
+ [ code, headers, contents ]
104
+ end
105
+
106
+ def absolute_path(filename)
107
+ filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
108
+ end
109
+
110
+ def context_link(context)
111
+ return nil unless context
112
+
113
+ filename, line, method = context.split(/:/)
114
+ textmate_url = "txmt://open?url=file://" + h(absolute_path(filename)) + '&amp;line=' + line
115
+ %Q(<a href="#{textmate_url}">#{filename}:#{line}</a> #{method})
116
+ end
117
+
118
+ def textmate_href(frame)
119
+ "txmt://open?url=file://" + h(absolute_path(context)).sub(/:/,'&amp;line=')
120
+ end
121
+
122
+ def h(obj) # :nodoc:
123
+ case obj
124
+ when String
125
+ Utils.escape_html(obj).gsub(/^(\s+)/) {'&nbsp;' * $1.length}
126
+ else
127
+ Utils.escape_html(obj.inspect)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,64 @@
1
+ module Rack
2
+ # Rack::EmailErrors sends error responses (code 5xx) to email addresses as
3
+ # specified in the Rack::Builder config.
4
+ class EmailErrors
5
+ def initialize(app,agent,app_name,from,*to)
6
+ @app = app
7
+ @agent = agent == :sendmail ? '/usr/sbin/sendmail -t' : agent
8
+ @app_name = app_name
9
+ @from = from
10
+ @to = to.flatten
11
+ end
12
+
13
+ def call(env)
14
+ code, headers, body = @app.call(env)
15
+
16
+ if code >= 500 && code < 600
17
+ begin # rescue any errors in message composition and sending
18
+ error_type = headers['X-Kiss-Error-Type'] || "#{code} Error"
19
+ error_message = headers['X-Kiss-Error-Message']
20
+
21
+ message = <<-EOT
22
+ Content-type: text/html
23
+ From: #{@from}
24
+ To: #{@to.join(', ')}
25
+ Subject: #{@app_name} - #{error_type}#{ error_message ? ": #{error_message}" : ''}
26
+
27
+ EOT
28
+
29
+ body.each do |part|
30
+ message += part
31
+ end
32
+
33
+ if @agent.is_a?(String)
34
+ IO.popen(@agent,"w") do |pipe|
35
+ pipe.puts(message)
36
+ end
37
+ else
38
+ require 'net/smtp' unless defined?(Net::SMTP)
39
+ smtp = @agent.is_a?(Net::SMTP) ? @agent : Net::SMTP.new('localhost')
40
+ smtp.start do |smtp|
41
+ smtp.send_message(message, @from, *@to)
42
+ end
43
+ end
44
+ rescue
45
+ end
46
+
47
+ body = <<-EOT
48
+ <html>
49
+ <head>
50
+ <title>Error</title>
51
+ </head>
52
+ <body>
53
+ <h1>Application Server Error</h1>
54
+ <p>Sorry, an error occurred. Our technical staff has been notified and will investigate this issue.</p>
55
+ </body>
56
+ </html>
57
+ EOT
58
+ headers['Content-Length'] = body.length.to_s
59
+ end
60
+
61
+ [ code, headers, body ]
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,28 @@
1
+ module Rack
2
+ # Rack::Facebook formats HTTP responses to remove certain status codes
3
+ # and HTML entities that are invalid as FBML responses.
4
+ class Facebook
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ code, headers, body = @app.call(env)
11
+
12
+ if code >= 500 && code < 600
13
+ code = 200
14
+ end
15
+
16
+ contents = ''
17
+ body.each {|p| contents += p }
18
+
19
+ contents.gsub!(/txmt:\/\//, 'http://textmate.local/')
20
+ contents.gsub!('<body>','<div class="body">')
21
+ contents.gsub!('</body>','</div>')
22
+
23
+ headers['Content-Length'] = contents.length.to_s
24
+
25
+ [ code, headers, contents ]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ module Rack
2
+ # Rack::FileNotFound rescues Kiss::TemplateFileNotFound exceptions
3
+ # (raised when action template files are not found) and returns an
4
+ # HTTP 404 error response.
5
+ class FileNotFound
6
+ def initialize(app,path = nil)
7
+ @app = app
8
+ @body = path ? (
9
+ ::File.file?(path) ?
10
+ ::File.read(path) :
11
+ template('could not find specified FileNotFound error document')
12
+ ) : template
13
+ end
14
+
15
+ def call(env)
16
+ code, headers, body = @app.call(env)
17
+ rescue Kiss::TemplateFileNotFound => e
18
+ [ 404, {
19
+ "Content-Type" => "text/html",
20
+ "Content-Length" => @body.length.to_s
21
+ }, @body ]
22
+ end
23
+
24
+ def template(error = nil)
25
+ <<EOT
26
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
27
+ <html lang="en">
28
+ <head>
29
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
30
+ <meta name="robots" content="NONE,NOARCHIVE" />
31
+ <title>File Not Found</title>
32
+ </head>
33
+ <body>
34
+ <h1>404 File Not Found</h1>
35
+
36
+ #{error ? "<p>Additionally, #{error}.</p>" : ''}
37
+ </body>
38
+ </html>
39
+ EOT
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ module Rack
2
+ # Rack::ShowExceptions catches exceptions raised from the app,
3
+ # showing a useful backtrace with clickable stack frames and
4
+ # TextMate links to source files, as well as the last database
5
+ # query, GET/POST params, cookies, and Rack environment variables.
6
+ #
7
+ # Be careful using this on public-facing sites as it could reveal
8
+ # potentially sensitive information to malicious users.
9
+
10
+ class LogExceptions
11
+ def initialize(app,path)
12
+ @app = app
13
+ @@file = ::File.open(path,'w')
14
+ end
15
+
16
+ def call(env)
17
+ @app.call(env)
18
+ rescue StandardError, LoadError, SyntaxError => e
19
+ @@file.print Kiss::ExceptionReport.generate(env, e) + "\n--- End of exception report --- \n\n"
20
+ raise
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,82 @@
1
+ $debug_messages = []
2
+ def debug(object)
3
+ $debug_messages.push( [object.inspect, Kernel.caller[0]] )
4
+ object
5
+ end
6
+
7
+ module Rack
8
+ # Rack::ShowDebug displays messages logged by the debug function.
9
+ #
10
+ # Be careful using this on public-facing sites as it could reveal
11
+ # potentially sensitive information to malicious users.
12
+
13
+ class ShowDebug
14
+ def initialize(app)
15
+ @app = app
16
+ end
17
+
18
+ def call(env)
19
+ $debug_messages = []
20
+ code, headers, body = @app.call(env)
21
+
22
+ if $debug_messages.size > 0
23
+ contents = <<-EOT
24
+ <style>
25
+ .kiss_debug {
26
+ text-align: left;
27
+ padding: 3px 7px;
28
+ border: 1px solid #ebe;
29
+ border-top: 1px solid #fdf;
30
+ border-bottom: 1px solid #d6d;
31
+ background-color: #fbf;
32
+ font-size: 12px;
33
+ color: #101;
34
+ }
35
+ .kiss_debug a {
36
+ color: #707;
37
+ text-decoration: none;
38
+ }
39
+ .kiss_debug a:hover {
40
+ color: #707;
41
+ text-decoration: underline;
42
+ }
43
+ </style>
44
+ EOT
45
+ contents += $debug_messages.map do |object,context|
46
+ filename, line, method = context.split(/:/)
47
+ textmate_url = "txmt://open?url=file://" + h(absolute_path(filename)) + '&amp;line=' + line
48
+ <<-EOT
49
+ <div class="kiss_debug">
50
+ <tt><b>#{object.gsub(/\</,'&lt;')}</b></tt>
51
+ <br><small>kiss debug output at <a href="#{textmate_url}">#{filename}:#{line}</a> #{method}</small>
52
+ </div>
53
+ EOT
54
+ end.join
55
+ else
56
+ contents = ''
57
+ end
58
+
59
+ body.each {|p| contents += p }
60
+ headers['Content-Length'] = contents.length.to_s
61
+
62
+ [ code, headers, contents ]
63
+ end
64
+
65
+ def absolute_path(filename)
66
+ filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
67
+ end
68
+
69
+ def textmate_href(frame)
70
+ "txmt://open?url=file://" + h(absolute_path(context)).sub(/:/,'&amp;line=')
71
+ end
72
+
73
+ def h(obj) # :nodoc:
74
+ case obj
75
+ when String
76
+ Utils.escape_html(obj).gsub(/^(\s+)/) {'&nbsp;' * $1.length}
77
+ else
78
+ Utils.escape_html(obj.inspect)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,27 @@
1
+ module Rack
2
+ # Rack::ShowExceptions catches exceptions raised from the app,
3
+ # showing a useful backtrace with clickable stack frames and
4
+ # TextMate links to source files, as well as the last database
5
+ # query, GET/POST params, cookies, and Rack environment variables.
6
+ #
7
+ # Be careful using this on public-facing sites as it could reveal
8
+ # potentially sensitive information to malicious users.
9
+
10
+ class ShowExceptions
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ @app.call(env)
17
+ rescue StandardError, LoadError, SyntaxError => e
18
+ body = Kiss::ExceptionReport.generate(env, e)
19
+ [500, {
20
+ "Content-Type" => "text/html",
21
+ "Content-Length" => body.length.to_s,
22
+ "X-Kiss-Error-Type" => e.class.name,
23
+ "X-Kiss-Error-Message" => e.message.sub(/\n.*/m,'')
24
+ }, body]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ module Sequel
2
+ module MySQL
3
+ class Dataset < Sequel::Dataset
4
+ # Returns results from dataset query as array of arrays,
5
+ # instead of array of hashes.
6
+ def all_arrays(opts = nil, &block)
7
+ a = []
8
+ fetch_arrays(select_sql(opts)) {|r| a << r}
9
+ a.each(&block) if block
10
+ a
11
+ end
12
+
13
+ # Fixes bug in Sequel 1.5; shouldn't be needed for Sequel 2.x
14
+ # (need to double-check, however).
15
+ def fetch_arrays(sql)
16
+ @db.execute_select(sql) do |r|
17
+ r.each_array {|row| yield row}
18
+ end
19
+ self
20
+ end
21
+ end
22
+ end
23
+ end