harbor 0.16.1

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.
Files changed (84) hide show
  1. data/Rakefile +76 -0
  2. data/bin/harbor +7 -0
  3. data/lib/harbor.rb +17 -0
  4. data/lib/harbor/accessor_injector.rb +30 -0
  5. data/lib/harbor/application.rb +172 -0
  6. data/lib/harbor/auth/basic.rb +51 -0
  7. data/lib/harbor/block_io.rb +63 -0
  8. data/lib/harbor/cache.rb +90 -0
  9. data/lib/harbor/cache/disk.rb +99 -0
  10. data/lib/harbor/cache/item.rb +48 -0
  11. data/lib/harbor/cache/memory.rb +35 -0
  12. data/lib/harbor/cascade.rb +75 -0
  13. data/lib/harbor/console.rb +34 -0
  14. data/lib/harbor/container.rb +134 -0
  15. data/lib/harbor/contrib/debug.rb +236 -0
  16. data/lib/harbor/contrib/session/data_mapper.rb +74 -0
  17. data/lib/harbor/daemon.rb +105 -0
  18. data/lib/harbor/errors.rb +49 -0
  19. data/lib/harbor/events.rb +45 -0
  20. data/lib/harbor/exception_notifier.rb +59 -0
  21. data/lib/harbor/file.rb +66 -0
  22. data/lib/harbor/file_store.rb +69 -0
  23. data/lib/harbor/file_store/file.rb +100 -0
  24. data/lib/harbor/file_store/local.rb +71 -0
  25. data/lib/harbor/file_store/mosso.rb +154 -0
  26. data/lib/harbor/file_store/mosso/private.rb +8 -0
  27. data/lib/harbor/generator.rb +56 -0
  28. data/lib/harbor/generator/help.rb +34 -0
  29. data/lib/harbor/generator/setup.rb +82 -0
  30. data/lib/harbor/generator/skeletons/basic/config.ru.skel +21 -0
  31. data/lib/harbor/generator/skeletons/basic/lib/appname.rb.skel +49 -0
  32. data/lib/harbor/generator/skeletons/basic/lib/appname/controllers/home.rb +9 -0
  33. data/lib/harbor/generator/skeletons/basic/lib/appname/views/home/index.html.erb.skel +23 -0
  34. data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/application.html.erb.skel +48 -0
  35. data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/exception.html.erb.skel +13 -0
  36. data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/login.html.erb.skel +11 -0
  37. data/lib/harbor/generator/skeletons/basic/log/development.log +0 -0
  38. data/lib/harbor/hooks.rb +105 -0
  39. data/lib/harbor/json_cookies.rb +37 -0
  40. data/lib/harbor/layouts.rb +61 -0
  41. data/lib/harbor/locale.rb +50 -0
  42. data/lib/harbor/locales.txt +22 -0
  43. data/lib/harbor/logging.rb +39 -0
  44. data/lib/harbor/logging/appenders/email.rb +84 -0
  45. data/lib/harbor/logging/request_logger.rb +34 -0
  46. data/lib/harbor/mail_servers/abstract.rb +12 -0
  47. data/lib/harbor/mail_servers/sendmail.rb +19 -0
  48. data/lib/harbor/mail_servers/smtp.rb +25 -0
  49. data/lib/harbor/mail_servers/test.rb +17 -0
  50. data/lib/harbor/mailer.rb +96 -0
  51. data/lib/harbor/messages.rb +17 -0
  52. data/lib/harbor/mime.rb +206 -0
  53. data/lib/harbor/plugin.rb +52 -0
  54. data/lib/harbor/plugin_list.rb +38 -0
  55. data/lib/harbor/request.rb +138 -0
  56. data/lib/harbor/response.rb +281 -0
  57. data/lib/harbor/router.rb +112 -0
  58. data/lib/harbor/script.rb +155 -0
  59. data/lib/harbor/session.rb +75 -0
  60. data/lib/harbor/session/abstract.rb +27 -0
  61. data/lib/harbor/session/cookie.rb +17 -0
  62. data/lib/harbor/shellwords.rb +26 -0
  63. data/lib/harbor/test/mailer.rb +10 -0
  64. data/lib/harbor/test/request.rb +28 -0
  65. data/lib/harbor/test/response.rb +17 -0
  66. data/lib/harbor/test/session.rb +11 -0
  67. data/lib/harbor/test/test.rb +22 -0
  68. data/lib/harbor/version.rb +3 -0
  69. data/lib/harbor/view.rb +89 -0
  70. data/lib/harbor/view_context.rb +134 -0
  71. data/lib/harbor/view_context/helpers.rb +7 -0
  72. data/lib/harbor/view_context/helpers/cache.rb +77 -0
  73. data/lib/harbor/view_context/helpers/form.rb +34 -0
  74. data/lib/harbor/view_context/helpers/html.rb +26 -0
  75. data/lib/harbor/view_context/helpers/text.rb +120 -0
  76. data/lib/harbor/view_context/helpers/url.rb +11 -0
  77. data/lib/harbor/xml_view.rb +57 -0
  78. data/lib/harbor/zipped_io.rb +203 -0
  79. data/lib/vendor/cloudfiles-1.3.0/cloudfiles.rb +77 -0
  80. data/lib/vendor/cloudfiles-1.3.0/cloudfiles/authentication.rb +46 -0
  81. data/lib/vendor/cloudfiles-1.3.0/cloudfiles/connection.rb +280 -0
  82. data/lib/vendor/cloudfiles-1.3.0/cloudfiles/container.rb +260 -0
  83. data/lib/vendor/cloudfiles-1.3.0/cloudfiles/storage_object.rb +253 -0
  84. metadata +155 -0
@@ -0,0 +1,52 @@
1
+ require "harbor/accessor_injector"
2
+
3
+ module Harbor
4
+ class Plugin
5
+
6
+ class VariableMissingError < StandardError
7
+ def initialize(klass, variable)
8
+ super("#{klass} expected #{variable.inspect} to be present, but it wasn't.")
9
+ end
10
+ end
11
+
12
+ include Harbor::AccessorInjector
13
+ include Harbor::Hooks
14
+
15
+ attr_accessor :context
16
+
17
+ def self.prepare(plugin, context, variables)
18
+ if plugin.is_a?(Class)
19
+ plugin.new(context).inject(variables)
20
+ else
21
+ plugin.inject({ :context => context }.merge(variables))
22
+ end
23
+ end
24
+
25
+ def initialize(context)
26
+ @context = context
27
+ end
28
+
29
+ def self.requires(key)
30
+ before(:to_s) do |instance|
31
+ raise VariableMissingError.new(self, key) unless instance.instance_variable_defined?("@#{key}")
32
+ end
33
+ end
34
+
35
+ def to_s
36
+ raise NotImplementedError.new("You must define your own #to_s method.")
37
+ end
38
+
39
+ class String < Plugin
40
+
41
+ def initialize(string)
42
+ @string = string
43
+ end
44
+
45
+ def to_s
46
+ @string
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,38 @@
1
+ module Harbor
2
+ class PluginList
3
+
4
+ include Enumerable
5
+
6
+ def initialize
7
+ @plugins = []
8
+ end
9
+
10
+ def each
11
+ @plugins.each do |plugin|
12
+ yield plugin
13
+ end
14
+ end
15
+
16
+ def size
17
+ @plugins.size
18
+ end
19
+
20
+ def clear
21
+ @plugins.clear
22
+ end
23
+
24
+ def <<(plugin)
25
+ case plugin
26
+ when String
27
+ plugin = Harbor::Plugin::String.new(plugin)
28
+ when Class
29
+ raise ArgumentError.new("#{plugin} must be a Plugin") unless Plugin > plugin
30
+ else
31
+ raise ArgumentError.new("#{plugin} must include Harbor::AccessorInjector") unless AccessorInjector > plugin
32
+ end
33
+
34
+ @plugins << plugin
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,138 @@
1
+ require "rack/request"
2
+ require Pathname(__FILE__).dirname + "session"
3
+
4
+ module Harbor
5
+ class Request < Rack::Request
6
+
7
+ BOT_AGENTS = [
8
+ /yahoo.*slurp/i,
9
+ /googlebot/i,
10
+ /msnbot/i,
11
+ /charlotte.*searchme/i,
12
+ /twiceler.*robot/i,
13
+ /dotbot/i,
14
+ /gigabot/i,
15
+ /yanga.*bot/i,
16
+ /gaisbot/i,
17
+ /becomebot/i,
18
+ /yandex/i,
19
+ /catchbot/i,
20
+ /cazoodlebot/i,
21
+ /jumblebot/i,
22
+ /librabot/i,
23
+ /jyxobot/i,
24
+ /mlbot/i,
25
+ /cipinetbot/i,
26
+ /funnelbot/i,
27
+ /mj12bot/i,
28
+ /spinn3r/i,
29
+ /nutch.*bot/i,
30
+ /oozbot/i,
31
+ /robotgenius/i,
32
+ /snapbot/i,
33
+ /tmangobot/i,
34
+ /yacybot/i,
35
+ /rpt.*httpclient/i,
36
+ /indy.*library/i,
37
+ /baiduspider/i,
38
+ /WhistleBlower/i,
39
+ /Pingdom/
40
+ ].freeze
41
+
42
+ attr_accessor :layout
43
+ attr_accessor :application
44
+
45
+ def initialize(application, env)
46
+ raise ArgumentError.new("+env+ must be a Rack Environment Hash") unless env.is_a?(Hash)
47
+ @application = application
48
+ super(env)
49
+ end
50
+
51
+ def fetch(key, default_value = nil)
52
+ if (value = self[key]).nil? || value == ''
53
+ default_value
54
+ else
55
+ value
56
+ end
57
+ end
58
+
59
+ def bot?
60
+ user_agent = env["HTTP_USER_AGENT"]
61
+ BOT_AGENTS.any? { |bot_agent| user_agent =~ bot_agent }
62
+ end
63
+
64
+ def session
65
+ @session ||= Harbor::Session.new(self)
66
+ end
67
+
68
+ def session?
69
+ @session
70
+ end
71
+
72
+ def remote_ip
73
+ env["REMOTE_ADDR"] || env["HTTP_CLIENT_IP"] || env["HTTP_X_FORWARDED_FOR"]
74
+ end
75
+
76
+ def request_method
77
+ @env['REQUEST_METHOD'] = self.POST['_method'].upcase if request_method_in_params?
78
+ @env['REQUEST_METHOD']
79
+ end
80
+
81
+ def environment
82
+ @env['APP_ENVIRONMENT'] || (@application ? @application.environment : "development")
83
+ end
84
+
85
+ def params
86
+ params = begin
87
+ self.GET && self.GET.update(self.POST || {})
88
+ rescue EOFError => e
89
+ self.GET
90
+ end
91
+
92
+ params || {}
93
+ end
94
+
95
+ def protocol
96
+ ssl? ? 'https://' : 'http://'
97
+ end
98
+
99
+ def ssl?
100
+ @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
101
+ end
102
+
103
+ def referer
104
+ @env['HTTP_REFERER']
105
+ end
106
+
107
+ def uri
108
+ @env['REQUEST_URI'] || @env['REQUEST_PATH']
109
+ end
110
+
111
+ def messages
112
+ @messages ||= if session?
113
+ session[:messages] = Messages.new(session[:messages])
114
+ else
115
+ params["messages"] = Messages.new(params["messages"])
116
+ end
117
+ end
118
+
119
+ def message(key)
120
+ messages[key]
121
+ end
122
+
123
+ # ==== Returns
124
+ # String::
125
+ # The URI without the query string. Strips trailing "/" and reduces
126
+ # duplicate "/" to a single "/".
127
+ def path
128
+ path = (uri.empty? ? '/' : uri.split('?').first).squeeze("/")
129
+ path = path[0..-2] if (path[-1] == ?/) && path.size > 1
130
+ path
131
+ end
132
+
133
+ private
134
+ def request_method_in_params?
135
+ @env["REQUEST_METHOD"] == "POST" && self.POST && %w(PUT DELETE).include?((self.POST['_method'] || "").upcase)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,281 @@
1
+ require "stringio"
2
+ require Pathname(__FILE__).dirname + "view"
3
+
4
+ module Harbor
5
+ class Response
6
+
7
+ attr_accessor :status, :headers, :errors
8
+
9
+ def initialize(request)
10
+ @request = request
11
+ @headers = {}
12
+ @headers["Content-Type"] = "text/html"
13
+ @status = 200
14
+ @errors = Harbor::Errors.new
15
+ end
16
+
17
+ def headers
18
+ @headers
19
+ end
20
+
21
+ def flush
22
+ @io = nil
23
+ end
24
+
25
+ def size=(size)
26
+ @headers["Content-Length"] = size.to_s
27
+ end
28
+
29
+ def size
30
+ (@headers["Content-Length"] || buffer.size).to_i
31
+ end
32
+
33
+ def content_type=(content_type)
34
+ @headers["Content-Type"] = content_type
35
+ end
36
+
37
+ def content_type
38
+ @headers["Content-Type"]
39
+ end
40
+
41
+ def puts(value)
42
+ string.puts(value)
43
+ self.size = string.length
44
+ end
45
+
46
+ def print(value)
47
+ string.print(value)
48
+ self.size = string.length
49
+ end
50
+
51
+ def buffer
52
+ if @io.is_a?(StringIO)
53
+ @io.string
54
+ else
55
+ @io || ""
56
+ end
57
+ end
58
+
59
+ def stream_file(path_or_io, content_type = nil)
60
+ io = BlockIO.new(path_or_io)
61
+ content_type ||= Harbor::Mime.mime_type(::File.extname(io.path.to_s))
62
+
63
+ if io.path && @request.env.has_key?("HTTP_X_SENDFILE_TYPE")
64
+ @headers["X-Sendfile"] = io.path
65
+ else
66
+ @io = io
67
+ end
68
+
69
+ self.size = io.size
70
+ self.content_type = content_type
71
+ nil
72
+ end
73
+
74
+ def send_file(name, path_or_io, content_type = nil)
75
+ stream_file(path_or_io, content_type)
76
+
77
+ @headers["Content-Disposition"] = "attachment; filename=\"#{escape_filename_for_http_header(name)}\""
78
+ nil
79
+ end
80
+
81
+ # Zip up the files (with no compression) and send it the client
82
+ # files should be an enumerable collection of Harbor::File instances
83
+ def send_files(name, files)
84
+ @io = ZippedIO.new(files)
85
+ self.size = @io.size
86
+ self.content_type = "application/zip"
87
+ @headers["Content-Disposition"] = "attachment; filename=\"#{escape_filename_for_http_header(name)}\""
88
+ end
89
+
90
+ def cache(key, last_modified, ttl = nil, max_age = nil)
91
+ raise ArgumentError.new("You must provide a block of code to cache.") unless block_given?
92
+
93
+ store = nil
94
+ if key && (ttl || max_age)
95
+ store = Harbor::View.cache
96
+
97
+ unless store
98
+ raise ArgumentError.new("Cache Store Not Defined. Please set Harbor::View.cache to your desired cache store.")
99
+ end
100
+
101
+ key = "page-#{key}"
102
+ end
103
+
104
+ last_modified = last_modified.httpdate
105
+ @headers["Last-Modified"] = last_modified
106
+ @headers["Cache-Control"] = "max-age=#{ttl}, must-revalidate" if ttl
107
+
108
+ modified_since = @request.env["HTTP_IF_MODIFIED_SINCE"]
109
+
110
+ if modified_since == last_modified && (!store || store.get(key))
111
+ not_modified!
112
+ elsif store && item = store.get(key)
113
+ return puts(item.content)
114
+ end
115
+
116
+ yield self
117
+ store.put(key, buffer, ttl, max_age) if store
118
+ end
119
+
120
+ def render(view, context = {})
121
+ if context[:layout].is_a?(Array)
122
+ warn "Passing multiple layouts to response.render has been deprecated. See Harbor::Layouts."
123
+ context[:layout] = context[:layout].first
124
+ end
125
+
126
+ case view
127
+ when View
128
+ view.context.merge(context)
129
+ else
130
+ view = View.new(view, context.merge({ :request => @request, :response => self }))
131
+ end
132
+
133
+ self.content_type = view.content_type
134
+
135
+ if context.has_key?(:layout) || @request.xhr?
136
+ puts view.to_s(context[:layout])
137
+ else
138
+ puts view.to_s(:search)
139
+ end
140
+ end
141
+
142
+ def redirect(url, params = {})
143
+ if @request && !@request.session? && !messages.empty? && !messages.expired?
144
+ messages.each { |key, value| params["messages[#{key}]"] = value }
145
+ end
146
+
147
+ self.status = 303
148
+ self.headers = {
149
+ "Location" => (params && params.any? ? "#{url}?#{Rack::Utils::build_query(params)}" : url),
150
+ "Content-Type" => "text/html"
151
+ }
152
+ self.flush
153
+ self
154
+ end
155
+
156
+ def redirect!(url, params = nil)
157
+ redirect(url, params) and throw(:abort_request)
158
+ end
159
+
160
+ def abort!(code)
161
+ if Harbor::View.exists?("exceptions/#{code}.html.erb")
162
+ render "exceptions/#{code}.html.erb"
163
+ end
164
+
165
+ self.status = code
166
+ throw(:abort_request)
167
+ end
168
+
169
+ def unauthorized
170
+ self.status = 401
171
+ end
172
+
173
+ def unauthorized!
174
+ unauthorized and throw(:abort_request)
175
+ end
176
+
177
+ # Headers that MUST NOT be included with 304 Not Modified responses.
178
+ #
179
+ # http://tools.ietf.org/html/rfc2616#section-10.3.5
180
+ NOT_MODIFIED_OMIT_HEADERS = %w[
181
+ Allow
182
+ Content-Encoding
183
+ Content-Language
184
+ Content-Length
185
+ Content-MD5
186
+ Content-Type
187
+ Last-Modified
188
+ ].to_set
189
+
190
+ def not_modified!
191
+ NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
192
+ self.status = 304
193
+ throw(:abort_request)
194
+ end
195
+
196
+ def inspect
197
+ "<#{self.class} headers=#{headers.inspect} content_type=#{content_type.inspect} status=#{status.inspect} body=#{buffer.inspect}>"
198
+ end
199
+
200
+ def messages
201
+ @messages ||= @request.messages
202
+ end
203
+
204
+ def message(key, message)
205
+ messages[key] = message
206
+ end
207
+
208
+ def to_a
209
+ messages.clear if messages.expired?
210
+
211
+ if @request.session?
212
+ session = @request.session
213
+ set_cookie(session.key, session.save)
214
+ end
215
+
216
+ [self.status, self.headers, self.buffer]
217
+ end
218
+
219
+ def [](key)
220
+ headers[key]
221
+ end
222
+
223
+ def []=(key, value)
224
+ headers[key] = value
225
+ end
226
+
227
+ def set_cookie(key, value)
228
+ raise ArgumentError.new("+key+ must not be blank") if key.nil? || key.size == 0
229
+
230
+ case value
231
+ when Hash
232
+ domain = "; domain=" + value[:domain] if value[:domain]
233
+ path = "; path=" + value[:path] if value[:path]
234
+ http_only = value[:http_only] ? "; HTTPOnly=" : nil
235
+ # According to RFC 2109, we need dashes here.
236
+ # N.B.: cgi.rb uses spaces...
237
+ expires_on = if (defined?(DateTime) && value[:expires].is_a?(DateTime))
238
+ value[:expires].clone.new_offset(0)
239
+ elsif value[:expires].is_a?(Time)
240
+ value[:expires].clone.gmtime
241
+ elsif value[:expires].nil?
242
+ nil
243
+ else
244
+ raise ArgumentError.new("The value hash for set_cookie contains an invalid +expires+ attribute, Time or DateTime expected, but got: #{value[:expires].inspect}")
245
+ end
246
+ expires = "; expires=" + expires_on.strftime("%a, %d-%b-%Y %H:%M:%S GMT") if expires_on
247
+
248
+ value = value[:value]
249
+ end
250
+ value = [value] unless Array === value
251
+ cookie = Rack::Utils.escape(key) + "=" +
252
+ value.map { |v| Rack::Utils.escape v }.join("&") +
253
+ "#{domain}#{path}#{expires}#{http_only}"
254
+
255
+ case self["Set-Cookie"]
256
+ when Array
257
+ self["Set-Cookie"] << cookie
258
+ when String
259
+ self["Set-Cookie"] = [self["Set-Cookie"], cookie]
260
+ when nil
261
+ self["Set-Cookie"] = cookie
262
+ end
263
+ end
264
+
265
+ private
266
+
267
+ def string
268
+ @io ||= StringIO.new("")
269
+ end
270
+
271
+ #
272
+ def escape_filename_for_http_header(filename)
273
+ # This would work great if IE6 could unescape the Content-Disposition filename field properly,
274
+ # but it can't, so we use the terribly weak version instead, until IE6 dies off...
275
+ #filename.gsub(/["\\\x0]/,'\\\\\0')
276
+
277
+ filename.gsub(/[^\w\.]/, '_')
278
+ end
279
+
280
+ end
281
+ end