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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/bin/hobix +90 -0
  3. data/lib/hobix/api.rb +91 -0
  4. data/lib/hobix/article.rb +22 -0
  5. data/lib/hobix/base.rb +477 -0
  6. data/lib/hobix/bixwik.rb +200 -0
  7. data/lib/hobix/commandline.rb +661 -0
  8. data/lib/hobix/comments.rb +99 -0
  9. data/lib/hobix/config.rb +39 -0
  10. data/lib/hobix/datamarsh.rb +110 -0
  11. data/lib/hobix/entry.rb +83 -0
  12. data/lib/hobix/facets/comments.rb +74 -0
  13. data/lib/hobix/facets/publisher.rb +314 -0
  14. data/lib/hobix/facets/trackbacks.rb +80 -0
  15. data/lib/hobix/linklist.rb +76 -0
  16. data/lib/hobix/out/atom.rb +92 -0
  17. data/lib/hobix/out/erb.rb +64 -0
  18. data/lib/hobix/out/okaynews.rb +55 -0
  19. data/lib/hobix/out/quick.rb +312 -0
  20. data/lib/hobix/out/rdf.rb +97 -0
  21. data/lib/hobix/out/redrum.rb +26 -0
  22. data/lib/hobix/out/rss.rb +115 -0
  23. data/lib/hobix/plugin/bloglines.rb +73 -0
  24. data/lib/hobix/plugin/calendar.rb +220 -0
  25. data/lib/hobix/plugin/flickr.rb +110 -0
  26. data/lib/hobix/plugin/recent_comments.rb +82 -0
  27. data/lib/hobix/plugin/sections.rb +91 -0
  28. data/lib/hobix/plugin/tags.rb +60 -0
  29. data/lib/hobix/publish/ping.rb +53 -0
  30. data/lib/hobix/publish/replicate.rb +283 -0
  31. data/lib/hobix/publisher.rb +18 -0
  32. data/lib/hobix/search/dictionary.rb +141 -0
  33. data/lib/hobix/search/porter_stemmer.rb +203 -0
  34. data/lib/hobix/search/simple.rb +209 -0
  35. data/lib/hobix/search/vector.rb +100 -0
  36. data/lib/hobix/storage/filesys.rb +398 -0
  37. data/lib/hobix/trackbacks.rb +94 -0
  38. data/lib/hobix/util/objedit.rb +193 -0
  39. data/lib/hobix/util/patcher.rb +155 -0
  40. data/lib/hobix/webapp/cli.rb +195 -0
  41. data/lib/hobix/webapp/htmlform.rb +107 -0
  42. data/lib/hobix/webapp/message.rb +177 -0
  43. data/lib/hobix/webapp/urigen.rb +141 -0
  44. data/lib/hobix/webapp/webrick-servlet.rb +90 -0
  45. data/lib/hobix/webapp.rb +723 -0
  46. data/lib/hobix/weblog.rb +860 -0
  47. data/lib/hobix.rb +223 -0
  48. metadata +87 -0
@@ -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