wunderbar 0.8.0
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.
- data/COPYING +18 -0
- data/Manifest +13 -0
- data/README +61 -0
- data/Rakefile +26 -0
- data/lib/cgi-spa.rb +11 -0
- data/lib/wunderbar/builder.rb +107 -0
- data/lib/wunderbar/cgi-methods.rb +117 -0
- data/lib/wunderbar/environment.rb +108 -0
- data/lib/wunderbar/html-methods.rb +208 -0
- data/lib/wunderbar/installation.rb +54 -0
- data/lib/wunderbar/job-control.rb +30 -0
- data/lib/wunderbar/version.rb +9 -0
- data/test/test_html_markup.rb +120 -0
- data/wunderbar.gemspec +36 -0
- metadata +120 -0
data/COPYING
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2009 Sam Ruby <rubys@intertwingly.net>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
COPYING
|
2
|
+
README
|
3
|
+
Rakefile
|
4
|
+
lib/cgi-spa.rb
|
5
|
+
lib/wunderbar/builder.rb
|
6
|
+
lib/wunderbar/cgi-methods.rb
|
7
|
+
lib/wunderbar/environment.rb
|
8
|
+
lib/wunderbar/html-methods.rb
|
9
|
+
lib/wunderbar/installation.rb
|
10
|
+
lib/wunderbar/job-control.rb
|
11
|
+
lib/wunderbar/version.rb
|
12
|
+
test/test_html_markup.rb
|
13
|
+
Manifest
|
data/README
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
= cgi-spa: Common Gateway Interface for Single Page Applications
|
2
|
+
|
3
|
+
CGI-SPA provides a number of globals, helper methods, and monkey patches which
|
4
|
+
simplify the development of single page applications in the form of CGI
|
5
|
+
scripts.
|
6
|
+
|
7
|
+
== Globals provided
|
8
|
+
* $cgi - Common Gateway Interface
|
9
|
+
* $param - Access to parameters (read-only OpenStruct like interface)
|
10
|
+
* $env - Access to environment variables (read-only OpenStruct like interface)
|
11
|
+
* $x - XmlBuilder instance
|
12
|
+
* $USER - Host user id
|
13
|
+
* $HOME - Home directory
|
14
|
+
* $SERVER- Server name
|
15
|
+
* SELF - Request URI
|
16
|
+
* SELF? - Request URI with '?' appended (avoids spoiling the cache)
|
17
|
+
|
18
|
+
* $USER - user
|
19
|
+
* $HOME - user's home directory
|
20
|
+
* $HOST - server host
|
21
|
+
|
22
|
+
* $HTTP_GET - request is an HTTP GET
|
23
|
+
* $HTTP_POST - request is an HTTP POST
|
24
|
+
* $XHR_JSON - request is XmlHttpRequest for JSON
|
25
|
+
* $XHTML - user agent accepts XHTML responses
|
26
|
+
|
27
|
+
== HTML methods
|
28
|
+
* style! - argument is indented text/data
|
29
|
+
* system! - run command and capture output
|
30
|
+
* script! - argument is indented text/data
|
31
|
+
* body? - capture exceptions, and produce a stack traceback
|
32
|
+
|
33
|
+
== CGI methods
|
34
|
+
* json - produce JSON output using the block specified
|
35
|
+
* json! - produce JSON output using the block specified and exit
|
36
|
+
* html - produce HTML output using the block specified
|
37
|
+
* html! - produce HTML output using the block specified and exit
|
38
|
+
* post - execute block only if method is POST
|
39
|
+
* post! - if POST, produce HTML output using the block specified and exit
|
40
|
+
|
41
|
+
== Helper methods
|
42
|
+
* submit: runs command (or block) as a deamon process
|
43
|
+
|
44
|
+
== OpenStruct methods (for $params and $env)
|
45
|
+
* untaint_if_match: untaints value if it matches a regular expression
|
46
|
+
|
47
|
+
== Builder extensions
|
48
|
+
* indented_text: matches text indentation to markup
|
49
|
+
* indented_data: useful for script and styles in HTML syntax
|
50
|
+
* traceback!: formats an exception traceback
|
51
|
+
* method_missing: patched to ensure open tags are closed
|
52
|
+
|
53
|
+
== Command line options
|
54
|
+
When run from the command line, CGI name=value pairs can be specified.
|
55
|
+
Additionally, the following options are supported:
|
56
|
+
* --html: HTML (HTTP GET) output is expected
|
57
|
+
* --post: HTML (HTTP POST) output is expected
|
58
|
+
* --json: JSON (XML HTTP Request) output is expected
|
59
|
+
* --xhtml: XHTML output is expected
|
60
|
+
* --prompt: prompt for key/value pairs using stdin
|
61
|
+
* --install=path: produce an suexec-callable wrapper script
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'echoe'
|
4
|
+
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + "/lib/wunderbar/version")
|
6
|
+
|
7
|
+
Echoe.new('wunderbar', Wunderbar::VERSION::STRING) do |p|
|
8
|
+
p.summary = "CGI Builder"
|
9
|
+
p.description = <<-EOF
|
10
|
+
Provides a number of globals, helper methods, and monkey patches which
|
11
|
+
simplify the development of generation of HTML and other artifacts for
|
12
|
+
CGI scripts.
|
13
|
+
EOF
|
14
|
+
p.url = "http://github.com/rubys/wunderbar"
|
15
|
+
p.author = "Sam Ruby"
|
16
|
+
p.email = "rubys@intertwingly.net"
|
17
|
+
p.dependencies = %w(
|
18
|
+
builder
|
19
|
+
json
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
Rake::TestTask.new do |t|
|
24
|
+
t.libs << 'test'
|
25
|
+
t.test_files = FileList['test/test*.rb']
|
26
|
+
end
|
data/lib/cgi-spa.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# add indented_text!, indented_data! and traceback! methods to builder
|
2
|
+
module Builder
|
3
|
+
class XmlMarkup
|
4
|
+
unless method_defined? :indented_text!
|
5
|
+
def indented_text!(text)
|
6
|
+
indented_data!(text) {|data| text! data}
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
unless method_defined? :indented_data!
|
11
|
+
def indented_data!(data, &block)
|
12
|
+
return if data.strip.length == 0
|
13
|
+
|
14
|
+
if @indent > 0
|
15
|
+
data.sub! /\n\s*\Z/, ''
|
16
|
+
data.sub! /\A\s*\n/, ''
|
17
|
+
|
18
|
+
unindent = data.sub(/s+\Z/,'').scan(/^ +/).map(&:length).min || 0
|
19
|
+
|
20
|
+
before = ::Regexp.new('^'.ljust(unindent+1))
|
21
|
+
after = " " * (@level * @indent)
|
22
|
+
data.gsub! before, after
|
23
|
+
end
|
24
|
+
|
25
|
+
if block
|
26
|
+
block.call(data)
|
27
|
+
else
|
28
|
+
self << data
|
29
|
+
end
|
30
|
+
|
31
|
+
_newline unless data =~ /\n\Z/
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
unless method_defined? :traceback!
|
36
|
+
def traceback!(exception=$!, klass='traceback')
|
37
|
+
pre :class=>klass do
|
38
|
+
text! exception.inspect
|
39
|
+
_newline
|
40
|
+
exception.backtrace.each {|frame| text!((' '*@indent)+frame + "\n")}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# monkey patch to ensure that tags are closed
|
48
|
+
test =
|
49
|
+
Builder::XmlMarkup.new.html do |x|
|
50
|
+
x.body do
|
51
|
+
begin
|
52
|
+
x.p do
|
53
|
+
raise Exception.new('boom')
|
54
|
+
end
|
55
|
+
rescue Exception => e
|
56
|
+
x.pre e
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
if test.index('<p>') and !test.index('</p>')
|
62
|
+
module Builder
|
63
|
+
class XmlMarkup
|
64
|
+
def method_missing(sym, *args, &block)
|
65
|
+
text = nil
|
66
|
+
attrs = nil
|
67
|
+
sym = "#{sym}:#{args.shift}" if args.first.kind_of?(Symbol)
|
68
|
+
args.each do |arg|
|
69
|
+
case arg
|
70
|
+
when Hash
|
71
|
+
attrs ||= {}
|
72
|
+
attrs.merge!(arg)
|
73
|
+
else
|
74
|
+
text ||= ''
|
75
|
+
text << arg.to_s
|
76
|
+
end
|
77
|
+
end
|
78
|
+
if block
|
79
|
+
unless text.nil?
|
80
|
+
raise ArgumentError, "XmlMarkup cannot mix a text argument with a block"
|
81
|
+
end
|
82
|
+
_indent
|
83
|
+
_start_tag(sym, attrs)
|
84
|
+
_newline
|
85
|
+
begin ### Added
|
86
|
+
_nested_structures(block)
|
87
|
+
ensure ### Added
|
88
|
+
_indent
|
89
|
+
_end_tag(sym)
|
90
|
+
_newline
|
91
|
+
end ### Added
|
92
|
+
elsif text.nil?
|
93
|
+
_indent
|
94
|
+
_start_tag(sym, attrs, true)
|
95
|
+
_newline
|
96
|
+
else
|
97
|
+
_indent
|
98
|
+
_start_tag(sym, attrs)
|
99
|
+
text! text
|
100
|
+
_end_tag(sym)
|
101
|
+
_newline
|
102
|
+
end
|
103
|
+
@target
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# produce json
|
2
|
+
def $cgi.json
|
3
|
+
return unless $XHR_JSON
|
4
|
+
output = yield
|
5
|
+
rescue Exception => exception
|
6
|
+
Kernel.print "Status: 500 Internal Error\r\n"
|
7
|
+
output = {
|
8
|
+
:exception => exception.inspect,
|
9
|
+
:backtrace => exception.backtrace
|
10
|
+
}
|
11
|
+
ensure
|
12
|
+
if $XHR_JSON
|
13
|
+
Kernel.print "Status: 404 Not Found\r\n" unless output
|
14
|
+
$cgi.out? 'type' => 'application/json', 'Cache-Control' => 'no-cache' do
|
15
|
+
begin
|
16
|
+
JSON.pretty_generate(output)+ "\n"
|
17
|
+
rescue
|
18
|
+
output.to_json + "\n"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# produce json and quit
|
25
|
+
def $cgi.json! &block
|
26
|
+
return unless $XHR_JSON
|
27
|
+
json(&block)
|
28
|
+
Process.exit
|
29
|
+
end
|
30
|
+
|
31
|
+
# produce text
|
32
|
+
def $cgi.text &block
|
33
|
+
return unless $TEXT
|
34
|
+
@output = []
|
35
|
+
def $cgi.puts(line='')
|
36
|
+
@output << line + "\n"
|
37
|
+
end
|
38
|
+
def $cgi.print(line=nil)
|
39
|
+
@output << line
|
40
|
+
end
|
41
|
+
self.instance_eval &block
|
42
|
+
rescue Exception => exception
|
43
|
+
Kernel.print "Status: 500 Internal Error\r\n"
|
44
|
+
@output << "\n" unless @output.empty?
|
45
|
+
@output << exception.inspect + "\n"
|
46
|
+
exception.backtrace.each {|frame| @output << " #{frame}\n"}
|
47
|
+
ensure
|
48
|
+
class << $cgi
|
49
|
+
undef puts
|
50
|
+
undef print
|
51
|
+
end
|
52
|
+
if $TEXT
|
53
|
+
Kernel.print "Status: 404 Not Found\r\n" if @output.empty?
|
54
|
+
$cgi.out? 'type' => 'text/plain', 'Cache-Control' => 'no-cache' do
|
55
|
+
@output.join
|
56
|
+
end
|
57
|
+
@output = nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# produce text and quit
|
62
|
+
def $cgi.text! &block
|
63
|
+
return unless $TEXT
|
64
|
+
json(&block)
|
65
|
+
Process.exit
|
66
|
+
end
|
67
|
+
|
68
|
+
# Conditionally provide output, based on ETAG
|
69
|
+
def $cgi.out?(headers, &block)
|
70
|
+
content = block.call
|
71
|
+
require 'digest/md5'
|
72
|
+
etag = Digest::MD5.hexdigest(content)
|
73
|
+
|
74
|
+
if ENV['HTTP_IF_NONE_MATCH'] == etag.inspect
|
75
|
+
Kernel.print "Status: 304 Not Modified\r\n\r\n"
|
76
|
+
else
|
77
|
+
$cgi.out headers.merge('Etag' => etag.inspect) do
|
78
|
+
content
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# produce html/xhtml
|
84
|
+
def $cgi.html(&block)
|
85
|
+
return if $XHR_JSON or $TEXT
|
86
|
+
x = HtmlMarkup.new
|
87
|
+
if $XHTML
|
88
|
+
$cgi.out? 'type' => 'application/xhtml+xml', 'charset' => 'UTF-8' do
|
89
|
+
x._! "\xEF\xBB\xBF"
|
90
|
+
x.declare! :DOCTYPE, :html
|
91
|
+
x.html :xmlns => 'http://www.w3.org/1999/xhtml', &block
|
92
|
+
end
|
93
|
+
else
|
94
|
+
$cgi.out? 'type' => 'text/html', 'charset' => 'UTF-8' do
|
95
|
+
x._! "\xEF\xBB\xBF"
|
96
|
+
x.declare! :DOCTYPE, :html
|
97
|
+
x.html &block
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# produce html and quit
|
103
|
+
def $cgi.html! &block
|
104
|
+
return if $XHR_JSON or $TEXT
|
105
|
+
html(&block)
|
106
|
+
Process.exit
|
107
|
+
end
|
108
|
+
|
109
|
+
# post specific logic (doesn't produce output)
|
110
|
+
def $cgi.post
|
111
|
+
yield if $HTTP_POST
|
112
|
+
end
|
113
|
+
|
114
|
+
# post specific content (produces output)
|
115
|
+
def $cgi.post! &block
|
116
|
+
html!(&block) if $HTTP_POST
|
117
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# safety first!
|
2
|
+
$SAFE = 1 unless $UNSAFE or $SAFE > 1
|
3
|
+
|
4
|
+
# explicit request types
|
5
|
+
$HTTP_GET = ARGV.delete('--html')
|
6
|
+
$HTTP_POST = ARGV.delete('--post')
|
7
|
+
$XHR_JSON = ARGV.delete('--json')
|
8
|
+
$XHTML = ARGV.delete('--xhtml')
|
9
|
+
$TEXT = ARGV.delete('--text')
|
10
|
+
|
11
|
+
# Only prompt if explicitly asked for
|
12
|
+
ARGV.push '' if ARGV.empty?
|
13
|
+
ARGV.delete('--prompt') or ARGV.delete('--offline')
|
14
|
+
|
15
|
+
# standard objects
|
16
|
+
$cgi = CGI.new
|
17
|
+
|
18
|
+
def $cgi.validate(validations)
|
19
|
+
$SAFE = 1
|
20
|
+
$cgi.params.each do |key, values|
|
21
|
+
next unless validations.include? key
|
22
|
+
values.each do |value|
|
23
|
+
unless value =~ validations[key]
|
24
|
+
raise SecurityError.new("CGI parameter #{key} doesn't match #{validations[key]}")
|
25
|
+
end
|
26
|
+
value.untaint
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
$param = $cgi.params
|
32
|
+
$x = Builder::XmlMarkup.new :indent => 2
|
33
|
+
|
34
|
+
# implied request types
|
35
|
+
$HTTP_GET ||= ($cgi.request_method == 'GET')
|
36
|
+
$HTTP_POST ||= ($cgi.request_method == 'POST')
|
37
|
+
$XHR_JSON ||= ($cgi.accept.to_s =~ /json/)
|
38
|
+
$XHTML ||= ($cgi.accept.to_s =~ /xhtml/)
|
39
|
+
$TEXT ||= ($cgi.accept.to_s =~ /plain/ and $cgi.accept.to_s !~ /html/)
|
40
|
+
|
41
|
+
# get arguments if CGI couldn't find any...
|
42
|
+
$param.merge!(CGI.parse(ARGV.join('&'))) if $param.empty?
|
43
|
+
|
44
|
+
module Wunderbar
|
45
|
+
module Untaint
|
46
|
+
def untaint_if_match regexp
|
47
|
+
self.untaint if regexp.match(self)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# fast path for accessing CGI parameters
|
53
|
+
def $param.method_missing(name)
|
54
|
+
if has_key? name.to_s
|
55
|
+
if self[name.to_s].length == 1
|
56
|
+
self[name.to_s].first.extend(Wunderbar::Untaint)
|
57
|
+
else
|
58
|
+
self[name.to_s].join
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
$env = {}
|
64
|
+
def $env.method_missing(name)
|
65
|
+
delete name.to_s if ENV[name.to_s] != self[name.to_s]
|
66
|
+
if ENV[name.to_s] and not has_key?(name.to_s)
|
67
|
+
self[name.to_s]=ENV[name.to_s].dup.extend(Wunderbar::Untaint)
|
68
|
+
end
|
69
|
+
self[name.to_s]
|
70
|
+
end
|
71
|
+
|
72
|
+
# quick access to request_uri
|
73
|
+
SELF = ENV['REQUEST_URI'].to_s
|
74
|
+
def SELF?
|
75
|
+
if SELF.include? '?'
|
76
|
+
SELF
|
77
|
+
else
|
78
|
+
SELF + "?" # avoids spoiling the cache
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# environment objects
|
83
|
+
$USER = ENV['REMOTE_USER'] || ENV['USER'] ||
|
84
|
+
if RUBY_PLATFORM =~ /darwin/
|
85
|
+
`dscl . -search /Users UniqueID #{Process.uid}`.split.first
|
86
|
+
else
|
87
|
+
`getent passwd #{Process.uid}`.split(':').first
|
88
|
+
end
|
89
|
+
|
90
|
+
$USER = $USER.dup.untaint if $USER =~ /^\w+$/
|
91
|
+
|
92
|
+
ENV['REMOTE_USER'] ||= $USER
|
93
|
+
|
94
|
+
$HOME = ENV['HOME'] || File.expand_path('~' + $USER)
|
95
|
+
$SERVER = ENV['HTTP_HOST'] || `hostname`.chomp
|
96
|
+
|
97
|
+
# more implied request types
|
98
|
+
$XHR_JSON ||= ($env.REQUEST_URI.to_s =~ /\?json$/)
|
99
|
+
$TEXT ||= ($env.REQUEST_URI.to_s =~ /\?text$/)
|
100
|
+
|
101
|
+
# set encoding to UTF-8
|
102
|
+
ENV['LANG'] ||= "en_US.UTF-8"
|
103
|
+
if defined? Encoding
|
104
|
+
Encoding.default_external = Encoding::UTF_8
|
105
|
+
Encoding.default_internal = Encoding::UTF_8
|
106
|
+
else
|
107
|
+
$KCODE = 'U'
|
108
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# smart style, knows that the content is indented text/data
|
2
|
+
def $x.style!(text)
|
3
|
+
text.slice! /^\n/
|
4
|
+
text.slice! /[ ]+\z/
|
5
|
+
$x.style :type => "text/css" do
|
6
|
+
if $XHTML
|
7
|
+
indented_text! text
|
8
|
+
else
|
9
|
+
indented_data! text
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# smart script, knows that the content is indented text/data
|
15
|
+
def $x.script!(text)
|
16
|
+
text.slice! /^\n/
|
17
|
+
text.slice! /[ ]+\z/
|
18
|
+
$x.script :lang => "text/javascript" do
|
19
|
+
if $XHTML
|
20
|
+
indented_text! text
|
21
|
+
else
|
22
|
+
indented_data! text
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# execute a system command, echoing stdin, stdout, and stderr
|
28
|
+
def $x.system!(command, opts={})
|
29
|
+
require 'open3'
|
30
|
+
output_class = opts[:class] || {}
|
31
|
+
stdin = output_class[:stdin] || 'stdin'
|
32
|
+
stdout = output_class[:stdout] || 'stdout'
|
33
|
+
stderr = output_class[:stderr] || 'stderr'
|
34
|
+
|
35
|
+
$x.pre command, :class=>stdin unless opts[:echo] == false
|
36
|
+
|
37
|
+
require 'thread'
|
38
|
+
semaphore = Mutex.new
|
39
|
+
Open3.popen3(command) do |pin, pout, perr|
|
40
|
+
[
|
41
|
+
Thread.new do
|
42
|
+
until pout.eof?
|
43
|
+
out_line = pout.readline.chomp
|
44
|
+
semaphore.synchronize { $x.pre out_line, :class=>stdout }
|
45
|
+
end
|
46
|
+
end,
|
47
|
+
|
48
|
+
Thread.new do
|
49
|
+
until perr.eof?
|
50
|
+
err_line = perr.readline.chomp
|
51
|
+
semaphore.synchronize { $x.pre err_line, :class=>stderr }
|
52
|
+
end
|
53
|
+
end,
|
54
|
+
|
55
|
+
Thread.new do
|
56
|
+
if opts[:stdin].respond_to? :read
|
57
|
+
require 'fileutils'
|
58
|
+
FileUtils.copy_stream opts[:stdin], pin
|
59
|
+
elsif opts[:stdin]
|
60
|
+
pin.write opts[:stdin].to_s
|
61
|
+
end
|
62
|
+
pin.close
|
63
|
+
end
|
64
|
+
].each {|thread| thread.join}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def $x.body? args={}
|
69
|
+
traceback_class = args.delete('traceback_class')
|
70
|
+
traceback_style = args.delete('traceback_style')
|
71
|
+
traceback_style ||= 'background-color:#ff0; margin: 1em 0; padding: 1em; ' +
|
72
|
+
'border: 4px solid red; border-radius: 1em'
|
73
|
+
$x.body(args) do
|
74
|
+
begin
|
75
|
+
yield
|
76
|
+
rescue ::Exception => exception
|
77
|
+
text = exception.inspect
|
78
|
+
exception.backtrace.each {|frame| text += "\n #{frame}"}
|
79
|
+
|
80
|
+
if traceback_class
|
81
|
+
$x.pre text, :class=>traceback_class
|
82
|
+
else
|
83
|
+
$x.pre text, :style=>traceback_style
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Wrapper class that understands HTML
|
90
|
+
class HtmlMarkup
|
91
|
+
VOID = %w(
|
92
|
+
area base br col command embed hr img input keygen
|
93
|
+
link meta param source track wbr
|
94
|
+
)
|
95
|
+
|
96
|
+
def initialize(*args, &block)
|
97
|
+
# as a migration aide, use the global variable, but consider that
|
98
|
+
# to be deprecated.
|
99
|
+
$x ||= Builder::XmlMarkup.new :indent => 2
|
100
|
+
@x = $x
|
101
|
+
end
|
102
|
+
|
103
|
+
def html(*args, &block)
|
104
|
+
@x.html(*args) { instance_exec(@x, &block) }
|
105
|
+
end
|
106
|
+
|
107
|
+
def method_missing(name, *args, &block)
|
108
|
+
if name.to_s =~ /^_(\w+)(!|\?|)$/
|
109
|
+
name, flag = $1, $2
|
110
|
+
else
|
111
|
+
error = NameError.new "undefined local variable or method `#{name}'", name
|
112
|
+
error.set_backtrace caller
|
113
|
+
raise error
|
114
|
+
end
|
115
|
+
|
116
|
+
if flag != '!'
|
117
|
+
if %w(script style).include?(name)
|
118
|
+
if String === args.first and not block
|
119
|
+
text = args.shift
|
120
|
+
if $XHTML
|
121
|
+
block = Proc.new {@x.indented_text! text}
|
122
|
+
else
|
123
|
+
block = Proc.new {@x.indented_data! text}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
args << {} if args.length == 0
|
128
|
+
if Hash === args.last
|
129
|
+
args.last[:lang] ||= 'text/javascript' if name == 'script'
|
130
|
+
args.last[:type] ||= 'text/css' if name == 'style'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# ensure that non-void elements are explicitly closed
|
135
|
+
if args.length == 0 or (args.length == 1 and Hash === args.first)
|
136
|
+
args.unshift '' if not VOID.include?(name) and not block
|
137
|
+
end
|
138
|
+
|
139
|
+
# remove attributes with nil values
|
140
|
+
args.last.delete_if {|key, value| value == nil} if Hash === args.last
|
141
|
+
end
|
142
|
+
|
143
|
+
if flag == '!'
|
144
|
+
# turn off indentation
|
145
|
+
indent, level = @x.instance_eval { [@indent, @level] }
|
146
|
+
begin
|
147
|
+
@x.instance_eval { [@indent=0, @level=0] }
|
148
|
+
@x.text! " "*indent*level
|
149
|
+
@x.tag! name, *args, &block
|
150
|
+
ensure
|
151
|
+
@x.text! "\n"
|
152
|
+
@x.instance_eval { [@indent=indent, @level=level] }
|
153
|
+
end
|
154
|
+
elsif flag == '?'
|
155
|
+
# capture exceptions, produce filtered tracebacks
|
156
|
+
options = (Hash === args.last)? args.last : {}
|
157
|
+
traceback_class = options.delete(:traceback_class)
|
158
|
+
traceback_style = options.delete(:traceback_style)
|
159
|
+
traceback_style ||= 'background-color:#ff0; margin: 1em 0; ' +
|
160
|
+
'padding: 1em; border: 4px solid red; border-radius: 1em'
|
161
|
+
@x.tag!(name, *args) do
|
162
|
+
begin
|
163
|
+
block.call
|
164
|
+
rescue ::Exception => exception
|
165
|
+
text = exception.inspect
|
166
|
+
exception.backtrace.each do |frame|
|
167
|
+
next if frame =~ %r{/cgi-spa/}
|
168
|
+
next if frame =~ %r{/gems/.*/builder/}
|
169
|
+
text += "\n #{frame}"
|
170
|
+
end
|
171
|
+
|
172
|
+
if traceback_class
|
173
|
+
$x.pre text, :class=>traceback_class
|
174
|
+
else
|
175
|
+
$x.pre text, :style=>traceback_style
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
else
|
180
|
+
@x.tag! name, *args, &block
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def _head(*args, &block)
|
185
|
+
@x.tag!('head', *args) do
|
186
|
+
@x.meta :charset => 'utf-8' unless $XHTML
|
187
|
+
block.call if block
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def _(text=nil)
|
192
|
+
@x.indented_text! text if text
|
193
|
+
@x
|
194
|
+
end
|
195
|
+
|
196
|
+
def _!(text=nil)
|
197
|
+
@x.text! text if text
|
198
|
+
@x
|
199
|
+
end
|
200
|
+
|
201
|
+
def declare!(*args)
|
202
|
+
@x.declare!(*args)
|
203
|
+
end
|
204
|
+
|
205
|
+
def target!
|
206
|
+
@x.target!
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# option to create an suexec callable wrapper
|
2
|
+
install = ARGV.find {|arg| arg =~ /--install=(.*)/}
|
3
|
+
if install and ARGV.delete(install)
|
4
|
+
|
5
|
+
# scope out the situation
|
6
|
+
dest = File.expand_path($1)
|
7
|
+
main = File.expand_path(caller.last[/^(.*):\d+(:|$)/,1])
|
8
|
+
|
9
|
+
# if destination is a directory, determine an appropriate file name
|
10
|
+
if File.directory?(dest)
|
11
|
+
if dest =~ /\/cgi-bin\/?$/
|
12
|
+
dest = File.join(dest, File.basename(main))
|
13
|
+
else
|
14
|
+
dest = File.join(dest, File.basename(main).sub(/\.rb$/,'.cgi'))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# prevent accidental overwrite
|
19
|
+
if File.exist?(dest) and not ARGV.delete('-f')
|
20
|
+
STDERR.puts "File #{dest} already exists. (Specify -f to overwrite)"
|
21
|
+
Process.exit
|
22
|
+
end
|
23
|
+
|
24
|
+
# ensure destination directory exists
|
25
|
+
destdir = File.dirname(dest)
|
26
|
+
if not File.exist?(destdir) or not File.directory?(destdir)
|
27
|
+
STDERR.puts "Directory #{destdir} does not exist."
|
28
|
+
Process.exit
|
29
|
+
end
|
30
|
+
|
31
|
+
# output wrapper
|
32
|
+
open(dest,'w') do |file|
|
33
|
+
# she-bang
|
34
|
+
file.puts "#!" + File.join(
|
35
|
+
RbConfig::CONFIG["bindir"],
|
36
|
+
RbConfig::CONFIG["ruby_install_name"] + RbConfig::CONFIG["EXEEXT"]
|
37
|
+
)
|
38
|
+
|
39
|
+
# Change directory
|
40
|
+
file.puts "Dir.chdir #{File.dirname(main).inspect}"
|
41
|
+
|
42
|
+
# Optional data from the script (after __END__)
|
43
|
+
file.puts DATA.read if Object.const_defined? :DATA
|
44
|
+
|
45
|
+
# Load script
|
46
|
+
file.puts "require #{File.basename(main).sub(/\.rb$/,'').inspect}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Mark wrapper as executable
|
50
|
+
File.chmod(0755, dest)
|
51
|
+
|
52
|
+
# Don't execute the script itself at this time
|
53
|
+
Process.exit
|
54
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# run command/block as a background daemon
|
2
|
+
def submit(cmd=nil)
|
3
|
+
fork do
|
4
|
+
# detach from tty
|
5
|
+
Process.setsid
|
6
|
+
fork and exit
|
7
|
+
|
8
|
+
# clear working directory and mask
|
9
|
+
Dir.chdir '/'
|
10
|
+
File.umask 0000
|
11
|
+
|
12
|
+
# close open files
|
13
|
+
STDIN.reopen '/dev/null'
|
14
|
+
STDOUT.reopen '/dev/null', 'a'
|
15
|
+
STDERR.reopen STDOUT
|
16
|
+
|
17
|
+
# clear environment of cgi cruft
|
18
|
+
ENV.keys.to_a.each do |key|
|
19
|
+
ENV.delete(key) if key =~ /HTTP/ or $cgi.respond_to? key.downcase
|
20
|
+
end
|
21
|
+
|
22
|
+
# setup environment
|
23
|
+
ENV['USER'] ||= $USER
|
24
|
+
ENV['HOME'] ||= $HOME
|
25
|
+
|
26
|
+
# run cmd and/or block
|
27
|
+
system cmd if cmd
|
28
|
+
yield if block_given?
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'cgi-spa'
|
4
|
+
|
5
|
+
class HtmlMarkupTest < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
$x = nil # until this hack is removed html-methods.rb
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_html
|
11
|
+
x = HtmlMarkup.new
|
12
|
+
x.html {}
|
13
|
+
assert_equal %{<html>\n</html>\n}, x.target!
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_void_element
|
17
|
+
x = HtmlMarkup.new
|
18
|
+
x.html {_br}
|
19
|
+
assert_match %r{<br/>}, x.target!
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_normal_element
|
23
|
+
x = HtmlMarkup.new
|
24
|
+
x.html {_textarea}
|
25
|
+
assert_match %r{<textarea></textarea>}, x.target!
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_script_lang
|
29
|
+
x = HtmlMarkup.new
|
30
|
+
x.html {_script}
|
31
|
+
assert_match %r[<script lang="text/javascript">], x.target!
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_style_type
|
35
|
+
x = HtmlMarkup.new
|
36
|
+
x.html {_style}
|
37
|
+
assert_match %r[<style type="text/css">], x.target!
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_script_plain
|
41
|
+
x = HtmlMarkup.new
|
42
|
+
x.html {_script! 'alert("foo");'}
|
43
|
+
assert_match %r[<script>alert\("foo"\);</script>], x.target!
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_script_indent
|
47
|
+
x = HtmlMarkup.new
|
48
|
+
x.html {_script "if (i<1) {}"}
|
49
|
+
assert_match %r[^ if], x.target!
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_script_html
|
53
|
+
$XHTML = false
|
54
|
+
x = HtmlMarkup.new
|
55
|
+
x.html {_script "if (i<1) {}"}
|
56
|
+
assert_match %r[<script.*>\s*if \(i<1\) \{\}\s*</script>], x.target!
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_script_xhtml
|
60
|
+
$XHTML = true
|
61
|
+
x = HtmlMarkup.new
|
62
|
+
x.html {_script "if (i<1) {}"}
|
63
|
+
assert_match %r[<script.*>\s*if \(i<1\) \{\}\s*</script>], x.target!
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_disable_indent
|
67
|
+
x = HtmlMarkup.new
|
68
|
+
x.html {_div! {_ "one "; _strong "two"; _ " three"}}
|
69
|
+
assert_match %r[<div>one <strong>two</strong> three</div>], x.target!
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_traceback
|
73
|
+
x = HtmlMarkup.new
|
74
|
+
x.html {_body? {boom}}
|
75
|
+
assert_match %r[<pre.*>#<NameError: .*boom],
|
76
|
+
x.target!
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_traceback_default_style
|
80
|
+
x = HtmlMarkup.new
|
81
|
+
x.html {_body? {boom}}
|
82
|
+
assert_match %r[<pre style="background-color:#ff0.*">], x.target!
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_traceback_style_override
|
86
|
+
x = HtmlMarkup.new
|
87
|
+
x.html {_body?(:traceback_style => 'color:red') {boom}}
|
88
|
+
assert_match %r[<pre style="color:red"], x.target!
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_traceback_class_override
|
92
|
+
x = HtmlMarkup.new
|
93
|
+
x.html {_body?(:traceback_class => 'traceback') {boom}}
|
94
|
+
assert_match %r[<pre class="traceback"], x.target!
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_meta_charset
|
98
|
+
x = HtmlMarkup.new
|
99
|
+
x.html {_head}
|
100
|
+
assert_match %r[<head>\s*<meta charset="utf-8"/>\s*</head>], x.target!
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_indented_text
|
104
|
+
x = HtmlMarkup.new
|
105
|
+
x.html {_div {_ 'text'}}
|
106
|
+
assert_match %r[^ <div>\n text\n </div>], x.target!
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_unindented_text
|
110
|
+
x = HtmlMarkup.new
|
111
|
+
x.html {_div {_! "text\n"}}
|
112
|
+
assert_match %r[^ <div>\ntext\n </div>], x.target!
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_declare
|
116
|
+
x = HtmlMarkup.new
|
117
|
+
x.declare! :DOCTYPE, 'html'
|
118
|
+
assert_equal %{<!DOCTYPE "html">\n}, x.target!
|
119
|
+
end
|
120
|
+
end
|
data/wunderbar.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "wunderbar"
|
5
|
+
s.version = "0.8.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Sam Ruby"]
|
9
|
+
s.date = "2012-02-02"
|
10
|
+
s.description = " Provides a number of globals, helper methods, and monkey patches which\n simplify the development of generation of HTML and other artifacts for\n CGI scripts.\n"
|
11
|
+
s.email = "rubys@intertwingly.net"
|
12
|
+
s.extra_rdoc_files = ["COPYING", "README", "lib/cgi-spa.rb", "lib/wunderbar/builder.rb", "lib/wunderbar/cgi-methods.rb", "lib/wunderbar/environment.rb", "lib/wunderbar/html-methods.rb", "lib/wunderbar/installation.rb", "lib/wunderbar/job-control.rb", "lib/wunderbar/version.rb"]
|
13
|
+
s.files = ["COPYING", "README", "Rakefile", "lib/cgi-spa.rb", "lib/wunderbar/builder.rb", "lib/wunderbar/cgi-methods.rb", "lib/wunderbar/environment.rb", "lib/wunderbar/html-methods.rb", "lib/wunderbar/installation.rb", "lib/wunderbar/job-control.rb", "lib/wunderbar/version.rb", "test/test_html_markup.rb", "Manifest", "wunderbar.gemspec"]
|
14
|
+
s.homepage = "http://github.com/rubys/wunderbar"
|
15
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Wunderbar", "--main", "README"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = "wunderbar"
|
18
|
+
s.rubygems_version = "1.8.15"
|
19
|
+
s.summary = "CGI Builder"
|
20
|
+
s.test_files = ["test/test_html_markup.rb"]
|
21
|
+
|
22
|
+
if s.respond_to? :specification_version then
|
23
|
+
s.specification_version = 3
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
26
|
+
s.add_runtime_dependency(%q<builder>, [">= 0"])
|
27
|
+
s.add_runtime_dependency(%q<json>, [">= 0"])
|
28
|
+
else
|
29
|
+
s.add_dependency(%q<builder>, [">= 0"])
|
30
|
+
s.add_dependency(%q<json>, [">= 0"])
|
31
|
+
end
|
32
|
+
else
|
33
|
+
s.add_dependency(%q<builder>, [">= 0"])
|
34
|
+
s.add_dependency(%q<json>, [">= 0"])
|
35
|
+
end
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wunderbar
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 63
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 8
|
9
|
+
- 0
|
10
|
+
version: 0.8.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Sam Ruby
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-02-02 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: builder
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: json
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
description: " Provides a number of globals, helper methods, and monkey patches which\n simplify the development of generation of HTML and other artifacts for\n CGI scripts.\n"
|
49
|
+
email: rubys@intertwingly.net
|
50
|
+
executables: []
|
51
|
+
|
52
|
+
extensions: []
|
53
|
+
|
54
|
+
extra_rdoc_files:
|
55
|
+
- COPYING
|
56
|
+
- README
|
57
|
+
- lib/cgi-spa.rb
|
58
|
+
- lib/wunderbar/builder.rb
|
59
|
+
- lib/wunderbar/cgi-methods.rb
|
60
|
+
- lib/wunderbar/environment.rb
|
61
|
+
- lib/wunderbar/html-methods.rb
|
62
|
+
- lib/wunderbar/installation.rb
|
63
|
+
- lib/wunderbar/job-control.rb
|
64
|
+
- lib/wunderbar/version.rb
|
65
|
+
files:
|
66
|
+
- COPYING
|
67
|
+
- README
|
68
|
+
- Rakefile
|
69
|
+
- lib/cgi-spa.rb
|
70
|
+
- lib/wunderbar/builder.rb
|
71
|
+
- lib/wunderbar/cgi-methods.rb
|
72
|
+
- lib/wunderbar/environment.rb
|
73
|
+
- lib/wunderbar/html-methods.rb
|
74
|
+
- lib/wunderbar/installation.rb
|
75
|
+
- lib/wunderbar/job-control.rb
|
76
|
+
- lib/wunderbar/version.rb
|
77
|
+
- test/test_html_markup.rb
|
78
|
+
- Manifest
|
79
|
+
- wunderbar.gemspec
|
80
|
+
homepage: http://github.com/rubys/wunderbar
|
81
|
+
licenses: []
|
82
|
+
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options:
|
85
|
+
- --line-numbers
|
86
|
+
- --inline-source
|
87
|
+
- --title
|
88
|
+
- Wunderbar
|
89
|
+
- --main
|
90
|
+
- README
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
hash: 3
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
version: "0"
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
hash: 11
|
108
|
+
segments:
|
109
|
+
- 1
|
110
|
+
- 2
|
111
|
+
version: "1.2"
|
112
|
+
requirements: []
|
113
|
+
|
114
|
+
rubyforge_project: wunderbar
|
115
|
+
rubygems_version: 1.8.15
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: CGI Builder
|
119
|
+
test_files:
|
120
|
+
- test/test_html_markup.rb
|