hobix 0.4
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.
- 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
|