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