hobix 0.4

Sign up to get free protection for your applications and to get access to all the features.
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