hobix 0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/hobix +90 -0
- data/lib/hobix/api.rb +91 -0
- data/lib/hobix/article.rb +22 -0
- data/lib/hobix/base.rb +477 -0
- data/lib/hobix/bixwik.rb +200 -0
- data/lib/hobix/commandline.rb +661 -0
- data/lib/hobix/comments.rb +99 -0
- data/lib/hobix/config.rb +39 -0
- data/lib/hobix/datamarsh.rb +110 -0
- data/lib/hobix/entry.rb +83 -0
- data/lib/hobix/facets/comments.rb +74 -0
- data/lib/hobix/facets/publisher.rb +314 -0
- data/lib/hobix/facets/trackbacks.rb +80 -0
- data/lib/hobix/linklist.rb +76 -0
- data/lib/hobix/out/atom.rb +92 -0
- data/lib/hobix/out/erb.rb +64 -0
- data/lib/hobix/out/okaynews.rb +55 -0
- data/lib/hobix/out/quick.rb +312 -0
- data/lib/hobix/out/rdf.rb +97 -0
- data/lib/hobix/out/redrum.rb +26 -0
- data/lib/hobix/out/rss.rb +115 -0
- data/lib/hobix/plugin/bloglines.rb +73 -0
- data/lib/hobix/plugin/calendar.rb +220 -0
- data/lib/hobix/plugin/flickr.rb +110 -0
- data/lib/hobix/plugin/recent_comments.rb +82 -0
- data/lib/hobix/plugin/sections.rb +91 -0
- data/lib/hobix/plugin/tags.rb +60 -0
- data/lib/hobix/publish/ping.rb +53 -0
- data/lib/hobix/publish/replicate.rb +283 -0
- data/lib/hobix/publisher.rb +18 -0
- data/lib/hobix/search/dictionary.rb +141 -0
- data/lib/hobix/search/porter_stemmer.rb +203 -0
- data/lib/hobix/search/simple.rb +209 -0
- data/lib/hobix/search/vector.rb +100 -0
- data/lib/hobix/storage/filesys.rb +398 -0
- data/lib/hobix/trackbacks.rb +94 -0
- data/lib/hobix/util/objedit.rb +193 -0
- data/lib/hobix/util/patcher.rb +155 -0
- data/lib/hobix/webapp/cli.rb +195 -0
- data/lib/hobix/webapp/htmlform.rb +107 -0
- data/lib/hobix/webapp/message.rb +177 -0
- data/lib/hobix/webapp/urigen.rb +141 -0
- data/lib/hobix/webapp/webrick-servlet.rb +90 -0
- data/lib/hobix/webapp.rb +723 -0
- data/lib/hobix/weblog.rb +860 -0
- data/lib/hobix.rb +223 -0
- metadata +87 -0
data/lib/hobix/webapp.rb
ADDED
@@ -0,0 +1,723 @@
|
|
1
|
+
#
|
2
|
+
# A trimmed-down version of akr's incredibly great WebApp library.
|
3
|
+
# The documentation is here: <http://cvs.m17n.org/~akr/webapp/doc/index.html>
|
4
|
+
# All the docs still apply since I only trimmed out undocumented stuff.
|
5
|
+
#
|
6
|
+
require 'stringio'
|
7
|
+
require 'pathname'
|
8
|
+
require 'zlib'
|
9
|
+
require 'time'
|
10
|
+
require 'hobix'
|
11
|
+
require 'hobix/webapp/urigen'
|
12
|
+
require 'hobix/webapp/message'
|
13
|
+
require 'hobix/webapp/htmlform'
|
14
|
+
|
15
|
+
class Regexp
|
16
|
+
def disable_capture
|
17
|
+
re = ''
|
18
|
+
self.source.scan(/\\.|[^\\\(]+|\(\?|\(/m) {|s|
|
19
|
+
if s == '('
|
20
|
+
re << '(?:'
|
21
|
+
else
|
22
|
+
re << s
|
23
|
+
end
|
24
|
+
}
|
25
|
+
Regexp.new(re, self.options, self.kcode)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Kernel
|
30
|
+
def puts( *args )
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module Hobix
|
35
|
+
class WebApp
|
36
|
+
NameChar = /[-A-Za-z0-9._:]/
|
37
|
+
NameExp = /[A-Za-z_:]#{NameChar}*/
|
38
|
+
XmlVersionNum = /[a-zA-Z0-9_.:-]+/
|
39
|
+
XmlVersionInfo_C = /\s+version\s*=\s*(?:'(#{XmlVersionNum})'|"(#{XmlVersionNum})")/
|
40
|
+
XmlVersionInfo = XmlVersionInfo_C.disable_capture
|
41
|
+
XmlEncName = /[A-Za-z][A-Za-z0-9._-]*/
|
42
|
+
XmlEncodingDecl_C = /\s+encoding\s*=\s*(?:"(#{XmlEncName})"|'(#{XmlEncName})')/
|
43
|
+
XmlEncodingDecl = XmlEncodingDecl_C.disable_capture
|
44
|
+
XmlSDDecl_C = /\s+standalone\s*=\s*(?:'(yes|no)'|"(yes|no)")/
|
45
|
+
XmlSDDecl = XmlSDDecl_C.disable_capture
|
46
|
+
XmlDecl_C = /<\?xml#{XmlVersionInfo_C}#{XmlEncodingDecl_C}?#{XmlSDDecl_C}?\s*\?>/
|
47
|
+
XmlDecl = /<\?xml#{XmlVersionInfo}#{XmlEncodingDecl}?#{XmlSDDecl}?\s*\?>/
|
48
|
+
SystemLiteral_C = /"([^"]*)"|'([^']*)'/
|
49
|
+
PubidLiteral_C = %r{"([\sa-zA-Z0-9\-'()+,./:=?;!*\#@$_%]*)"|'([\sa-zA-Z0-9\-()+,./:=?;!*\#@$_%]*)'}
|
50
|
+
ExternalID_C = /(?:SYSTEM|PUBLIC\s+#{PubidLiteral_C})(?:\s+#{SystemLiteral_C})?/
|
51
|
+
DocType_C = /<!DOCTYPE\s+(#{NameExp})(?:\s+#{ExternalID_C})?\s*(?:\[.*?\]\s*)?>/m
|
52
|
+
DocType = DocType_C.disable_capture
|
53
|
+
|
54
|
+
WebAPPDevelopHost = ENV['WEBAPP_DEVELOP_HOST']
|
55
|
+
|
56
|
+
def initialize(manager, request, response) # :nodoc:
|
57
|
+
@manager = manager
|
58
|
+
@request = request
|
59
|
+
@request_header = request.header_object
|
60
|
+
@request_body = request.body_object
|
61
|
+
@response = response
|
62
|
+
@response_header = response.header_object
|
63
|
+
@response_body = response.body_object
|
64
|
+
@urigen = URIGen.new('http', # xxx: https?
|
65
|
+
@request.server_name, @request.server_port,
|
66
|
+
File.dirname(@request.script_name), @request.path_info)
|
67
|
+
end
|
68
|
+
|
69
|
+
def <<(str) @response_body << str end
|
70
|
+
def print(*strs) @response_body.print(*strs) end
|
71
|
+
def printf(fmt, *args) @response_body.printf(fmt, *args) end
|
72
|
+
def putc(ch) @response_body.putc ch end
|
73
|
+
def puts(*strs) @response_body.puts(*strs) end
|
74
|
+
def write(str) @response_body.write str end
|
75
|
+
|
76
|
+
def each_request_header(&block) # :yields: field_name, field_body
|
77
|
+
@request_header.each(&block)
|
78
|
+
end
|
79
|
+
def get_request_header(field_name) @request_header[field_name] end
|
80
|
+
|
81
|
+
def request_method() @request.request_method end
|
82
|
+
def request_body() @request_body.string end
|
83
|
+
def server_name() @request.server_name end
|
84
|
+
def server_port() @request.server_port end
|
85
|
+
def script_name() @request.script_name end
|
86
|
+
def path_info() @request.path_info end
|
87
|
+
def query_string() @request.query_string end
|
88
|
+
def server_protocol() @request.server_protocol end
|
89
|
+
def remote_addr() @request.remote_addr end
|
90
|
+
def request_content_type() @request.content_type end
|
91
|
+
def request_uri() @request.request_uri end
|
92
|
+
def action_uri() @request.action_uri end
|
93
|
+
|
94
|
+
def _GET()
|
95
|
+
unless @_get_vars
|
96
|
+
@_get_vars = {}
|
97
|
+
query_html_get_application_x_www_form_urlencoded.each do |k, v|
|
98
|
+
v.gsub!( /\r\n/, "\n" ) if defined? v.gsub!
|
99
|
+
@_get_vars[k] = v
|
100
|
+
end
|
101
|
+
end
|
102
|
+
@_get_vars
|
103
|
+
end
|
104
|
+
|
105
|
+
def _POST()
|
106
|
+
unless @_post_vars
|
107
|
+
@_post_vars = {}
|
108
|
+
query_html_post_application_x_www_form_urlencoded.each do |k, v|
|
109
|
+
v.gsub!( /\r\n/, "\n" ) if defined? v.gsub!
|
110
|
+
@_post_vars[k] = v
|
111
|
+
end
|
112
|
+
end
|
113
|
+
@_post_vars
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_header(field_name, field_body) @response_header.set(field_name, field_body) end
|
117
|
+
def add_header(field_name, field_body) @response_header.add(field_name, field_body) end
|
118
|
+
def remove_header(field_name) @response_header.remove(field_name) end
|
119
|
+
def clear_header() @response_header.clear end
|
120
|
+
def has_header?(field_name) @response_header.has?(field_name) end
|
121
|
+
def get_header(field_name) @response_header[field_name] end
|
122
|
+
def each_header(&block) # :yields: field_name, field_body
|
123
|
+
@response_header.each(&block)
|
124
|
+
end
|
125
|
+
|
126
|
+
def content_type=(media_type)
|
127
|
+
@response_header.set 'Content-Type', media_type
|
128
|
+
end
|
129
|
+
def content_type
|
130
|
+
@response_header['Content-Type']
|
131
|
+
end
|
132
|
+
|
133
|
+
# returns a Pathname object.
|
134
|
+
# _path_ is interpreted as a relative path from the directory
|
135
|
+
# which a web application exists.
|
136
|
+
#
|
137
|
+
# If /home/user/public_html/foo/bar.cgi is a web application which
|
138
|
+
# WebApp {} calls, webapp.resource_path("baz") returns a pathname points to
|
139
|
+
# /home/user/public_html/foo/baz.
|
140
|
+
#
|
141
|
+
# _path_ must not have ".." component and must not be absolute.
|
142
|
+
# Otherwise ArgumentError is raised.
|
143
|
+
def resource_path(arg)
|
144
|
+
path = Pathname.new(arg)
|
145
|
+
raise ArgumentError, "absolute path: #{arg.inspect}" if !path.relative?
|
146
|
+
path.each_filename {|f|
|
147
|
+
raise ArgumentError, "path contains .. : #{arg.inspect}" if f == '..'
|
148
|
+
}
|
149
|
+
@manager.resource_basedir + path
|
150
|
+
end
|
151
|
+
|
152
|
+
# call-seq:
|
153
|
+
# open_resource(path)
|
154
|
+
# open_resource(path) {|io| ... }
|
155
|
+
#
|
156
|
+
# opens _path_ as relative from a web application directory.
|
157
|
+
def open_resource(path, &block)
|
158
|
+
resource_path(path).open(&block)
|
159
|
+
end
|
160
|
+
|
161
|
+
# call-seq:
|
162
|
+
# send_resource(path)
|
163
|
+
#
|
164
|
+
# send the resource indicated by _path_.
|
165
|
+
# Last-Modified: and If-Modified-Since: header is supported.
|
166
|
+
def send_resource(path)
|
167
|
+
path = resource_path(path)
|
168
|
+
begin
|
169
|
+
mtime = path.mtime
|
170
|
+
rescue Errno::ENOENT
|
171
|
+
send_not_found "Resource not found: #{path}"
|
172
|
+
return
|
173
|
+
end
|
174
|
+
check_last_modified(path.mtime) {
|
175
|
+
path.open {|f|
|
176
|
+
@response_body << f.read
|
177
|
+
}
|
178
|
+
}
|
179
|
+
end
|
180
|
+
|
181
|
+
def send_not_found(msg)
|
182
|
+
@response.status_line = '404 Not Found'
|
183
|
+
@response_body << <<End
|
184
|
+
<html>
|
185
|
+
<head><title>404 Not Found</title></head>
|
186
|
+
<body>
|
187
|
+
<h1>404 Not Found</h1>
|
188
|
+
<p>#{msg}</p>
|
189
|
+
<hr />
|
190
|
+
<small><a href="http://hobix.com/">hobix</a> #{ Hobix::VERSION } / <a href="http://docs.hobix.com">docs</a> / <a href="http://let.us.all.hobix.com">wiki</a> / <a href="http://google.com/search?q=hobix+#{ URI.escape action_uri }">search google for this action</a></small>
|
191
|
+
</body>
|
192
|
+
</html>
|
193
|
+
End
|
194
|
+
end
|
195
|
+
|
196
|
+
def send_unauthorized
|
197
|
+
@response.status_line = '401 Unauthorized'
|
198
|
+
@response_body << <<End
|
199
|
+
<html>
|
200
|
+
<head><title>401 Unauthorized</title></head>
|
201
|
+
<body>
|
202
|
+
<h1>401 Authorized</h1>
|
203
|
+
<p>You lack decent credentials to enter herein.</p>
|
204
|
+
</body>
|
205
|
+
</html>
|
206
|
+
End
|
207
|
+
end
|
208
|
+
|
209
|
+
def check_last_modified(last_modified)
|
210
|
+
if ims = @request_header['If-Modified-Since'] and
|
211
|
+
((ims = Time.httpdate(ims)) rescue nil) and
|
212
|
+
last_modified <= ims
|
213
|
+
@response.status_line = '304 Not Modified'
|
214
|
+
return
|
215
|
+
end
|
216
|
+
@response_header.set 'Last-Modified', last_modified.httpdate
|
217
|
+
yield
|
218
|
+
end
|
219
|
+
|
220
|
+
# call-seq:
|
221
|
+
# reluri(:script=>string, :path_info=>string, :query=>query, :fragment=>string) -> URI
|
222
|
+
# make_relative_uri(:script=>string, :path_info=>string, :query=>query, :fragment=>string) -> URI
|
223
|
+
#
|
224
|
+
# make_relative_uri returns a relative URI which base URI is the URI the
|
225
|
+
# web application is invoked.
|
226
|
+
#
|
227
|
+
# The argument should be a hash which may have following components.
|
228
|
+
# - :script specifies script_name relative from the directory containing
|
229
|
+
# the web application script.
|
230
|
+
# If it is not specified, the web application itself is assumed.
|
231
|
+
# - :path_info specifies path_info component for calling web application.
|
232
|
+
# It should begin with a slash.
|
233
|
+
# If it is not specified, "" is assumed.
|
234
|
+
# - :query specifies query a component.
|
235
|
+
# It should be a Hash or a WebApp::QueryString.
|
236
|
+
# - :fragment specifies a fragment identifier.
|
237
|
+
# If it is not specified, a fragment identifier is not appended to
|
238
|
+
# the result URL.
|
239
|
+
#
|
240
|
+
# Since the method escapes the components properly,
|
241
|
+
# you should specify them in unescaped form.
|
242
|
+
#
|
243
|
+
# In the example follow, assume that the web application bar.cgi is invoked
|
244
|
+
# as http://host/foo/bar.cgi/baz/qux.
|
245
|
+
#
|
246
|
+
# webapp.reluri(:path_info=>"/hoge") => URI("../hoge")
|
247
|
+
# webapp.reluri(:path_info=>"/baz/fuga") => URI("fuga")
|
248
|
+
# webapp.reluri(:path_info=>"/baz/") => URI("./")
|
249
|
+
# webapp.reluri(:path_info=>"/") => URI("../")
|
250
|
+
# webapp.reluri() => URI("../../bar.cgi")
|
251
|
+
# webapp.reluri(:script=>"funyo.cgi") => URI("../../funyo.cgi")
|
252
|
+
# webapp.reluri(:script=>"punyo/gunyo.cgi") => URI("../../punyo/gunyo.cgi")
|
253
|
+
# webapp.reluri(:script=>"../genyo.cgi") => URI("../../../genyo.cgi")
|
254
|
+
# webapp.reluri(:fragment=>"sec1") => URI("../../bar.cgi#sec1")
|
255
|
+
#)
|
256
|
+
# webapp.reluri(:path_info=>"/h?#o/x y") => URI("../h%3F%23o/x%20y")
|
257
|
+
# webapp.reluri(:script=>"ho%o.cgi") => URI("../../ho%25o.cgi")
|
258
|
+
# webapp.reluri(:fragment=>"sp ce") => URI("../../bar.cgi#sp%20ce")
|
259
|
+
#
|
260
|
+
def make_relative_uri(hash={})
|
261
|
+
@urigen.make_relative_uri(hash)
|
262
|
+
end
|
263
|
+
alias reluri make_relative_uri
|
264
|
+
|
265
|
+
# call-seq:
|
266
|
+
# make_absolute_uri(:script=>string, :path_info=>string, :query=>query, :fragment=>string) -> URI
|
267
|
+
#
|
268
|
+
# make_absolute_uri returns a absolute URI which base URI is the URI of the
|
269
|
+
# web application is invoked.
|
270
|
+
#
|
271
|
+
# The argument is same as make_relative_uri.
|
272
|
+
def make_absolute_uri(hash={})
|
273
|
+
@urigen.make_absolute_uri(hash)
|
274
|
+
end
|
275
|
+
alias absuri make_absolute_uri
|
276
|
+
|
277
|
+
# :stopdoc:
|
278
|
+
StatusMessage = { # RFC 2616
|
279
|
+
100 => 'Continue',
|
280
|
+
101 => 'Switching Protocols',
|
281
|
+
200 => 'OK',
|
282
|
+
201 => 'Created',
|
283
|
+
202 => 'Accepted',
|
284
|
+
203 => 'Non-Authoritative Information',
|
285
|
+
204 => 'No Content',
|
286
|
+
205 => 'Reset Content',
|
287
|
+
206 => 'Partial Content',
|
288
|
+
300 => 'Multiple Choices',
|
289
|
+
301 => 'Moved Permanently',
|
290
|
+
302 => 'Found',
|
291
|
+
303 => 'See Other',
|
292
|
+
304 => 'Not Modified',
|
293
|
+
305 => 'Use Proxy',
|
294
|
+
307 => 'Temporary Redirect',
|
295
|
+
400 => 'Bad Request',
|
296
|
+
401 => 'Unauthorized',
|
297
|
+
402 => 'Payment Required',
|
298
|
+
403 => 'Forbidden',
|
299
|
+
404 => 'Not Found',
|
300
|
+
405 => 'Method Not Allowed',
|
301
|
+
406 => 'Not Acceptable',
|
302
|
+
407 => 'Proxy Authentication Required',
|
303
|
+
408 => 'Request Timeout',
|
304
|
+
409 => 'Conflict',
|
305
|
+
410 => 'Gone',
|
306
|
+
411 => 'Length Required',
|
307
|
+
412 => 'Precondition Failed',
|
308
|
+
413 => 'Request Entity Too Large',
|
309
|
+
414 => 'Request-URI Too Long',
|
310
|
+
415 => 'Unsupported Media Type',
|
311
|
+
416 => 'Requested Range Not Satisfiable',
|
312
|
+
417 => 'Expectation Failed',
|
313
|
+
500 => 'Internal Server Error',
|
314
|
+
501 => 'Not Implemented',
|
315
|
+
502 => 'Bad Gateway',
|
316
|
+
503 => 'Service Unavailable',
|
317
|
+
504 => 'Gateway Timeout',
|
318
|
+
505 => 'HTTP Version Not Supported',
|
319
|
+
}
|
320
|
+
# :startdoc:
|
321
|
+
|
322
|
+
# setup_redirect makes a status line and a Location header appropriate as
|
323
|
+
# redirection.
|
324
|
+
#
|
325
|
+
# _status_ specifies the status line.
|
326
|
+
# It should be a Fixnum 3xx or String '3xx ...'.
|
327
|
+
#
|
328
|
+
# _uri_ specifies the Location header body.
|
329
|
+
# It should be a URI, String or Hash.
|
330
|
+
# If a Hash is given, make_absolute_uri is called to convert to URI.
|
331
|
+
# If given URI is relative, it is converted as absolute URI.
|
332
|
+
def setup_redirection(status, uri)
|
333
|
+
case status
|
334
|
+
when Fixnum
|
335
|
+
if status < 300 || 400 <= status
|
336
|
+
raise ArgumentError, "unexpected status: #{status.inspect}"
|
337
|
+
end
|
338
|
+
status = "#{status} #{StatusMessage[status]}"
|
339
|
+
when String
|
340
|
+
unless /\A3\d\d(\z| )/ =~ status
|
341
|
+
raise ArgumentError, "unexpected status: #{status.inspect}"
|
342
|
+
end
|
343
|
+
if status.length == 3
|
344
|
+
status = "#{status} #{StatusMessage[status.to_i]}"
|
345
|
+
end
|
346
|
+
else
|
347
|
+
raise ArgumentError, "unexpected status: #{status.inspect}"
|
348
|
+
end
|
349
|
+
case uri
|
350
|
+
when URI
|
351
|
+
uri = @urigen.base_uri + uri if uri.relative?
|
352
|
+
when String
|
353
|
+
uri = URI.parse(uri)
|
354
|
+
uri = @urigen.base_uri + uri if uri.relative?
|
355
|
+
when Hash
|
356
|
+
uri = make_absolute_uri(uri)
|
357
|
+
else
|
358
|
+
raise ArgumentError, "unexpected uri: #{uri.inspect}"
|
359
|
+
end
|
360
|
+
@response.status_line = status
|
361
|
+
@response_header.set 'Location', uri.to_s
|
362
|
+
end
|
363
|
+
|
364
|
+
def query_html_get_application_x_www_form_urlencoded
|
365
|
+
@request.query_string.decode_as_application_x_www_form_urlencoded
|
366
|
+
end
|
367
|
+
|
368
|
+
def query_html_post_application_x_www_form_urlencoded
|
369
|
+
if /\Apost\z/i =~ @request.request_method # xxx: should not check?
|
370
|
+
q = QueryString.primitive_new_for_raw_query_string(@request.body_object.read)
|
371
|
+
if %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n.match(request_content_type)
|
372
|
+
boundary = $1.dup
|
373
|
+
q.decode_as_multipart_form_data boundary
|
374
|
+
else
|
375
|
+
q.decode_as_application_x_www_form_urlencoded
|
376
|
+
end
|
377
|
+
else
|
378
|
+
# xxx: warning?
|
379
|
+
HTMLFormQuery.new
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
class QueryValidationFailure < StandardError
|
384
|
+
end
|
385
|
+
|
386
|
+
# QueryString represents a query component of URI.
|
387
|
+
class QueryString
|
388
|
+
class << self
|
389
|
+
alias primitive_new_for_raw_query_string new
|
390
|
+
undef new
|
391
|
+
end
|
392
|
+
|
393
|
+
def initialize(escaped_query_string)
|
394
|
+
@escaped_query_string = escaped_query_string
|
395
|
+
end
|
396
|
+
|
397
|
+
def inspect
|
398
|
+
"#<#{self.class}: #{@escaped_query_string}>"
|
399
|
+
end
|
400
|
+
alias to_s inspect
|
401
|
+
end
|
402
|
+
|
403
|
+
# :stopdoc:
|
404
|
+
def WebApp.make_frozen_string(str)
|
405
|
+
raise ArgumentError, "not a string: #{str.inspect}" unless str.respond_to? :to_str
|
406
|
+
str = str.to_str
|
407
|
+
str = str.dup.freeze unless str.frozen?
|
408
|
+
str
|
409
|
+
end
|
410
|
+
|
411
|
+
LoadedWebAppProcedures = {}
|
412
|
+
def WebApp.load_webapp_procedure(path)
|
413
|
+
unless LoadedWebAppProcedures[path]
|
414
|
+
begin
|
415
|
+
Thread.current[:webapp_delay] = true
|
416
|
+
load path, true
|
417
|
+
LoadedWebAppProcedures[path] = Thread.current[:webapp_proc]
|
418
|
+
ensure
|
419
|
+
Thread.current[:webapp_delay] = nil
|
420
|
+
Thread.current[:webapp_proc] = nil
|
421
|
+
end
|
422
|
+
end
|
423
|
+
unless LoadedWebAppProcedures[path]
|
424
|
+
raise RuntimeError, "not a web application: #{path}"
|
425
|
+
end
|
426
|
+
LoadedWebAppProcedures[path]
|
427
|
+
end
|
428
|
+
|
429
|
+
def WebApp.run_webapp_via_stub(path)
|
430
|
+
if Thread.current[:webrick_load_servlet]
|
431
|
+
load path, true
|
432
|
+
return
|
433
|
+
end
|
434
|
+
WebApp.load_webapp_procedure(path).call
|
435
|
+
end
|
436
|
+
|
437
|
+
class Manager
|
438
|
+
def initialize(app_block)
|
439
|
+
@app_block = app_block
|
440
|
+
@resource_basedir = Pathname.new(eval("__FILE__", app_block)).dirname
|
441
|
+
end
|
442
|
+
attr_reader :resource_basedir
|
443
|
+
|
444
|
+
# CGI, Esehttpd
|
445
|
+
def run_cgi
|
446
|
+
setup_request = lambda {|req|
|
447
|
+
req.make_request_header_from_cgi_env(ENV)
|
448
|
+
if ENV.include?('CONTENT_LENGTH')
|
449
|
+
len = ENV['CONTENT_LENGTH'].to_i
|
450
|
+
req.body_object << $stdin.read(len)
|
451
|
+
end
|
452
|
+
}
|
453
|
+
output_response = lambda {|res|
|
454
|
+
res.output_cgi_status_field($stdout)
|
455
|
+
res.output_message($stdout)
|
456
|
+
}
|
457
|
+
primitive_run(setup_request, output_response)
|
458
|
+
end
|
459
|
+
|
460
|
+
# FastCGI
|
461
|
+
def run_fcgi
|
462
|
+
require 'fcgi'
|
463
|
+
FCGI.each_request {|fcgi_request|
|
464
|
+
setup_request = lambda {|req|
|
465
|
+
req.make_request_header_from_cgi_env(fcgi_request.env)
|
466
|
+
if content = fcgi_request.in.read
|
467
|
+
req.body_object << content
|
468
|
+
end
|
469
|
+
}
|
470
|
+
output_response = lambda {|res|
|
471
|
+
res.output_cgi_status_field(fcgi_request.out)
|
472
|
+
res.output_message(fcgi_request.out)
|
473
|
+
fcgi_request.finish
|
474
|
+
}
|
475
|
+
primitive_run(setup_request, output_response)
|
476
|
+
}
|
477
|
+
end
|
478
|
+
|
479
|
+
# mod_ruby with Apache::RubyRun
|
480
|
+
def run_rbx
|
481
|
+
rbx_request = Apache.request
|
482
|
+
setup_request = lambda {|req|
|
483
|
+
req.make_request_header_from_cgi_env(rbx_request.subprocess_env)
|
484
|
+
if content = rbx_request.read
|
485
|
+
req.body_object << content
|
486
|
+
end
|
487
|
+
}
|
488
|
+
output_response = lambda {|res|
|
489
|
+
rbx_request.status_line = "#{res.status_line}"
|
490
|
+
res.header_object.each {|k, v|
|
491
|
+
case k
|
492
|
+
when /\AContent-Type\z/i
|
493
|
+
rbx_request.content_type = v
|
494
|
+
else
|
495
|
+
rbx_request.headers_out[k] = v
|
496
|
+
end
|
497
|
+
}
|
498
|
+
rbx_request.write res.body_object.string
|
499
|
+
}
|
500
|
+
primitive_run(setup_request, output_response)
|
501
|
+
end
|
502
|
+
|
503
|
+
# WEBrick with webapp/webrick-servlet.rb
|
504
|
+
def run_webrick
|
505
|
+
Thread.current[:webrick_load_servlet] = lambda {|webrick_req, webrick_res|
|
506
|
+
setup_request = lambda {|req|
|
507
|
+
req.make_request_header_from_cgi_env(webrick_req.meta_vars)
|
508
|
+
webrick_req.body {|chunk|
|
509
|
+
req.body_object << chunk
|
510
|
+
}
|
511
|
+
}
|
512
|
+
output_response = lambda {|res|
|
513
|
+
webrick_res.status = res.status_line.to_i
|
514
|
+
res.header_object.each {|k, v|
|
515
|
+
webrick_res[k] = v
|
516
|
+
}
|
517
|
+
webrick_res.body = res.body_object.string
|
518
|
+
}
|
519
|
+
primitive_run(setup_request, output_response)
|
520
|
+
}
|
521
|
+
end
|
522
|
+
|
523
|
+
def primitive_run(setup_request, output_response)
|
524
|
+
req = Request.new
|
525
|
+
res = Response.new
|
526
|
+
trap_exception(req, res) {
|
527
|
+
setup_request.call(req)
|
528
|
+
req.freeze
|
529
|
+
req.body_object.rewind
|
530
|
+
webapp = WebApp.new(self, req, res)
|
531
|
+
@app_block.call(webapp)
|
532
|
+
complete_response(webapp, res)
|
533
|
+
}
|
534
|
+
output_response.call(res)
|
535
|
+
end
|
536
|
+
|
537
|
+
def complete_response(webapp, res)
|
538
|
+
unless res.header_object.has? 'Content-Type'
|
539
|
+
case res.body_object.string
|
540
|
+
when /\A\z/
|
541
|
+
content_type = nil
|
542
|
+
when /\A\211PNG\r\n\032\n/
|
543
|
+
content_type = 'image/png'
|
544
|
+
when /\A#{XmlDecl_C}\s*#{DocType_C}/io
|
545
|
+
charset = $3 || $4
|
546
|
+
rootelem = $7
|
547
|
+
content_type = make_xml_content_type(rootelem, charset)
|
548
|
+
when /\A#{XmlDecl_C}\s*<(#{NameExp})[\s>]/io
|
549
|
+
charset = $3 || $4
|
550
|
+
rootelem = $7
|
551
|
+
content_type = make_xml_content_type(rootelem, charset)
|
552
|
+
when /\A<html[\s>]/io
|
553
|
+
content_type = 'text/html'
|
554
|
+
when /\0/
|
555
|
+
content_type = 'application/octet-stream'
|
556
|
+
else
|
557
|
+
content_type = 'text/plain'
|
558
|
+
end
|
559
|
+
res.header_object.set 'Content-Type', content_type if content_type
|
560
|
+
end
|
561
|
+
gzip_content(webapp, res) unless res.header_object.has? 'Content-Encoding'
|
562
|
+
unless res.header_object.has? 'Content-Length'
|
563
|
+
res.header_object.set 'Content-Length', res.body_object.length.to_s
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
def gzip_content(webapp, res, level=nil)
|
568
|
+
# xxx: parse the Accept-Encoding field body
|
569
|
+
if accept_encoding = webapp.get_request_header('Accept-Encoding') and
|
570
|
+
/gzip/ =~ accept_encoding and
|
571
|
+
/\A\037\213/ !~ res.body_object.string # already gzipped
|
572
|
+
level ||= Zlib::DEFAULT_COMPRESSION
|
573
|
+
content = res.body_object.string
|
574
|
+
Zlib::GzipWriter.wrap(StringIO.new(gzipped = ''), level) {|gz|
|
575
|
+
gz << content
|
576
|
+
}
|
577
|
+
if gzipped.length < content.length
|
578
|
+
content.replace gzipped
|
579
|
+
res.header_object.set 'Content-Encoding', 'gzip'
|
580
|
+
end
|
581
|
+
end
|
582
|
+
end
|
583
|
+
|
584
|
+
def make_xml_content_type(rootelem, charset)
|
585
|
+
case rootelem
|
586
|
+
when /\Ahtml\z/i
|
587
|
+
result = 'text/html'
|
588
|
+
else
|
589
|
+
result = 'application/xml'
|
590
|
+
end
|
591
|
+
result << "; charset=\"#{charset}\"" if charset
|
592
|
+
result
|
593
|
+
end
|
594
|
+
|
595
|
+
def trap_exception(req, res)
|
596
|
+
begin
|
597
|
+
yield
|
598
|
+
rescue Exception => e
|
599
|
+
if devlopper_host? req.remote_addr
|
600
|
+
generate_debug_page(req, res, e)
|
601
|
+
else
|
602
|
+
generate_error_page(req, res, e)
|
603
|
+
end
|
604
|
+
end
|
605
|
+
end
|
606
|
+
|
607
|
+
def devlopper_host?(addr)
|
608
|
+
return true if addr == '127.0.0.1'
|
609
|
+
return false if %r{\A(\d+)\.(\d+)\.(\d+)\.(\d+)\z} !~ addr
|
610
|
+
addr_arr = [$1.to_i, $2.to_i, $3.to_i, $4.to_i]
|
611
|
+
addr_bin = addr_arr.pack("CCCC").unpack("B*")[0]
|
612
|
+
case WebAPPDevelopHost
|
613
|
+
when %r{\A(\d+)\.(\d+)\.(\d+)\.(\d+)\z}
|
614
|
+
dev_arr = [$1.to_i, $2.to_i, $3.to_i, $4.to_i]
|
615
|
+
return true if dev_arr == addr_arr
|
616
|
+
when %r{\A(\d+)\.(\d+)\.(\d+)\.(\d+)/(\d+)\z}
|
617
|
+
dev_arr = [$1.to_i, $2.to_i, $3.to_i, $4.to_i]
|
618
|
+
dev_bin = dev_arr.pack("CCCC").unpack("B*")[0]
|
619
|
+
dev_len = $5.to_i
|
620
|
+
return true if addr_bin[0, dev_len] == dev_bin[0, dev_len]
|
621
|
+
end
|
622
|
+
return false
|
623
|
+
end
|
624
|
+
|
625
|
+
def generate_error_page(req, res, exc)
|
626
|
+
backtrace = "#{exc.message} (#{exc.class})\n"
|
627
|
+
exc.backtrace.each {|f| backtrace << f << "\n" }
|
628
|
+
res.status_line = '500 Internal Server Error'
|
629
|
+
header = res.header_object
|
630
|
+
header.clear
|
631
|
+
header.add 'Content-Type', 'text/html'
|
632
|
+
body = res.body_object
|
633
|
+
body.rewind
|
634
|
+
body.truncate(0)
|
635
|
+
body.puts <<'End'
|
636
|
+
<html><head><title>500 Internal Server Error</title></head>
|
637
|
+
<body><h1>500 Internal Server Error</h1>
|
638
|
+
<p>The dynamic page you requested is failed to generate.</p></body>
|
639
|
+
</html>
|
640
|
+
End
|
641
|
+
end
|
642
|
+
|
643
|
+
def generate_debug_page(req, res, exc)
|
644
|
+
backtrace = "#{exc.message} (#{exc.class})\n"
|
645
|
+
exc.backtrace.each {|f| backtrace << f << "\n" }
|
646
|
+
res.status_line = '500 Internal Server Error'
|
647
|
+
header = res.header_object
|
648
|
+
header.clear
|
649
|
+
header.add 'Content-Type', 'text/plain'
|
650
|
+
body = res.body_object
|
651
|
+
body.rewind
|
652
|
+
body.truncate(0)
|
653
|
+
body.puts backtrace
|
654
|
+
end
|
655
|
+
end
|
656
|
+
# :startdoc:
|
657
|
+
end
|
658
|
+
|
659
|
+
# WebApp is a main routine of web application.
|
660
|
+
# It should be called from a toplevel of a CGI/FastCGI/mod_ruby/WEBrick script.
|
661
|
+
#
|
662
|
+
# WebApp is used as follows.
|
663
|
+
#
|
664
|
+
# #!/usr/bin/env ruby
|
665
|
+
#
|
666
|
+
# require 'webapp'
|
667
|
+
#
|
668
|
+
# ... class/method definitions ... # run once per process.
|
669
|
+
#
|
670
|
+
# WebApp {|webapp| # This block runs once per request.
|
671
|
+
# ... process a request ...
|
672
|
+
# }
|
673
|
+
#
|
674
|
+
# WebApp yields with an object of the class WebApp.
|
675
|
+
# The object contains request and response.
|
676
|
+
#
|
677
|
+
# WebApp rise $SAFE to 1.
|
678
|
+
#
|
679
|
+
# WebApp catches all kind of exception raised in the block.
|
680
|
+
# If HTTP connection is made from localhost or a developper host,
|
681
|
+
# the backtrace is sent back to the browser.
|
682
|
+
# Otherwise, the backtrace is sent to stderr usually which is redirected to
|
683
|
+
# error.log.
|
684
|
+
# The developper hosts are specified by the environment variable
|
685
|
+
# WEBAPP_DEVELOP_HOST.
|
686
|
+
# It may be an IP address such as "111.222.333.444" or
|
687
|
+
# an network address such as "111.222.333.0/24".
|
688
|
+
# (An environment variable for CGI can be set by SetEnv directive in Apache.)
|
689
|
+
#
|
690
|
+
def self.WebApp(&block) # :yields: webapp
|
691
|
+
$SAFE = 1 if $SAFE < 1
|
692
|
+
manager = WebApp::Manager.new(block)
|
693
|
+
if defined?(Apache::Request) && Apache.request.kind_of?(Apache::Request)
|
694
|
+
run = lambda { manager.run_rbx }
|
695
|
+
elsif Thread.current[:webrick_load_servlet]
|
696
|
+
run = lambda { manager.run_webrick }
|
697
|
+
elsif STDIN.respond_to?(:stat) && STDIN.stat.socket? &&
|
698
|
+
begin
|
699
|
+
# getpeername(FCGI_LISTENSOCK_FILENO) causes ENOTCONN on FastCGI
|
700
|
+
# cf. http://www.fastcgi.com/devkit/doc/fcgi-spec.html
|
701
|
+
require 'socket'
|
702
|
+
sock = Socket.for_fd(0)
|
703
|
+
sock.getpeername
|
704
|
+
false
|
705
|
+
rescue Errno::ENOTCONN
|
706
|
+
true
|
707
|
+
rescue SystemCallError
|
708
|
+
false
|
709
|
+
end
|
710
|
+
run = lambda { manager.run_fcgi }
|
711
|
+
elsif ENV.include?('REQUEST_METHOD')
|
712
|
+
run = lambda { manager.run_cgi }
|
713
|
+
else
|
714
|
+
require 'hobix/webapp/cli'
|
715
|
+
run = lambda { manager.run_cli }
|
716
|
+
end
|
717
|
+
if Thread.current[:webapp_delay]
|
718
|
+
Thread.current[:webapp_proc] = run
|
719
|
+
else
|
720
|
+
run.call
|
721
|
+
end
|
722
|
+
end
|
723
|
+
end
|