arrow 1.0.7
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/ChangeLog +1590 -0
- data/LICENSE +28 -0
- data/README +75 -0
- data/Rakefile +366 -0
- data/Rakefile.local +63 -0
- data/data/arrow/applets/TEMPLATE.rb.tpl +53 -0
- data/data/arrow/applets/args.rb +50 -0
- data/data/arrow/applets/config.rb +55 -0
- data/data/arrow/applets/error.rb +63 -0
- data/data/arrow/applets/files.rb +46 -0
- data/data/arrow/applets/inspect.rb +46 -0
- data/data/arrow/applets/nosuchapplet.rb +31 -0
- data/data/arrow/applets/status.rb +92 -0
- data/data/arrow/applets/test.rb +133 -0
- data/data/arrow/applets/tutorial/counter.rb +96 -0
- data/data/arrow/applets/tutorial/dingus.rb +67 -0
- data/data/arrow/applets/tutorial/hello.rb +34 -0
- data/data/arrow/applets/tutorial/hello2.rb +73 -0
- data/data/arrow/applets/tutorial/imgtext.rb +90 -0
- data/data/arrow/applets/tutorial/imgtext2.rb +286 -0
- data/data/arrow/applets/tutorial/index.rb +36 -0
- data/data/arrow/applets/tutorial/logo.rb +98 -0
- data/data/arrow/applets/tutorial/memcache.rb +61 -0
- data/data/arrow/applets/tutorial/missing.rb +37 -0
- data/data/arrow/applets/tutorial/protected.rb +100 -0
- data/data/arrow/applets/tutorial/redirector.rb +52 -0
- data/data/arrow/applets/tutorial/rndimages.rb +159 -0
- data/data/arrow/applets/tutorial/sharenotes.rb +83 -0
- data/data/arrow/applets/tutorial/subclassed-hello.rb +32 -0
- data/data/arrow/applets/tutorial/superhello.rb +72 -0
- data/data/arrow/applets/tutorial/timeclock.rb +78 -0
- data/data/arrow/applets/view-applet.rb +123 -0
- data/data/arrow/applets/view-template.rb +85 -0
- data/data/arrow/applets/wiki.rb +274 -0
- data/data/arrow/templates/TEMPLATE.tmpl.tpl +36 -0
- data/data/arrow/templates/applet-status.tmpl +153 -0
- data/data/arrow/templates/args-display.tmpl +120 -0
- data/data/arrow/templates/config/display-table.tmpl +36 -0
- data/data/arrow/templates/config/display.tmpl +36 -0
- data/data/arrow/templates/counter-deleted.tmpl +33 -0
- data/data/arrow/templates/counter.tmpl +59 -0
- data/data/arrow/templates/dingus.tmpl +55 -0
- data/data/arrow/templates/enumtable.tmpl +8 -0
- data/data/arrow/templates/error-display.tmpl +92 -0
- data/data/arrow/templates/filemap.tmpl +89 -0
- data/data/arrow/templates/hello-world-src.tmpl +34 -0
- data/data/arrow/templates/hello-world.tmpl +60 -0
- data/data/arrow/templates/imgtext/fontlist.tmpl +46 -0
- data/data/arrow/templates/imgtext/form.tmpl +70 -0
- data/data/arrow/templates/imgtext/reload-error.tmpl +40 -0
- data/data/arrow/templates/imgtext/reload.tmpl +55 -0
- data/data/arrow/templates/inspect/display.tmpl +80 -0
- data/data/arrow/templates/loginform.tmpl +64 -0
- data/data/arrow/templates/logout.tmpl +32 -0
- data/data/arrow/templates/memcache/display.tmpl +41 -0
- data/data/arrow/templates/navbar.incl +27 -0
- data/data/arrow/templates/nosuchapplet.tmpl +32 -0
- data/data/arrow/templates/printsource.tmpl +35 -0
- data/data/arrow/templates/protected.tmpl +36 -0
- data/data/arrow/templates/rndimages.tmpl +38 -0
- data/data/arrow/templates/service-response.tmpl +13 -0
- data/data/arrow/templates/sharenotes/display.tmpl +38 -0
- data/data/arrow/templates/status.tmpl +120 -0
- data/data/arrow/templates/templateviewer.tmpl +43 -0
- data/data/arrow/templates/test/harness.tmpl +57 -0
- data/data/arrow/templates/test/list.tmpl +48 -0
- data/data/arrow/templates/test/problem.tmpl +42 -0
- data/data/arrow/templates/tutorial/index.tmpl +37 -0
- data/data/arrow/templates/tutorial/missingapplet.tmpl +29 -0
- data/data/arrow/templates/view-applet-nosuch.tmpl +32 -0
- data/data/arrow/templates/view-applet.tmpl +40 -0
- data/data/arrow/templates/view-template.tmpl +83 -0
- data/data/arrow/templates/wiki/formerror.tmpl +47 -0
- data/data/arrow/templates/wiki/markup_help.incl +6 -0
- data/data/arrow/templates/wiki/new.tmpl +56 -0
- data/data/arrow/templates/wiki/new_system.tmpl +122 -0
- data/data/arrow/templates/wiki/sectionlist.tmpl +43 -0
- data/data/arrow/templates/wiki/show.tmpl +34 -0
- data/docs/manual/layouts/default.page +43 -0
- data/docs/manual/lib/api-filter.rb +81 -0
- data/docs/manual/lib/editorial-filter.rb +64 -0
- data/docs/manual/lib/examples-filter.rb +244 -0
- data/docs/manual/lib/links-filter.rb +117 -0
- data/lib/apache/fakerequest.rb +448 -0
- data/lib/apache/logger.rb +33 -0
- data/lib/arrow.rb +51 -0
- data/lib/arrow/acceptparam.rb +207 -0
- data/lib/arrow/applet.rb +725 -0
- data/lib/arrow/appletmixins.rb +218 -0
- data/lib/arrow/appletregistry.rb +590 -0
- data/lib/arrow/applettestcase.rb +503 -0
- data/lib/arrow/broker.rb +255 -0
- data/lib/arrow/cache.rb +176 -0
- data/lib/arrow/config-loaders/yaml.rb +75 -0
- data/lib/arrow/config.rb +615 -0
- data/lib/arrow/constants.rb +24 -0
- data/lib/arrow/cookie.rb +359 -0
- data/lib/arrow/cookieset.rb +108 -0
- data/lib/arrow/dispatcher.rb +368 -0
- data/lib/arrow/dispatcherloader.rb +50 -0
- data/lib/arrow/exceptions.rb +61 -0
- data/lib/arrow/fallbackhandler.rb +48 -0
- data/lib/arrow/formvalidator.rb +631 -0
- data/lib/arrow/htmltokenizer.rb +343 -0
- data/lib/arrow/logger.rb +488 -0
- data/lib/arrow/logger/apacheoutputter.rb +69 -0
- data/lib/arrow/logger/arrayoutputter.rb +63 -0
- data/lib/arrow/logger/coloroutputter.rb +111 -0
- data/lib/arrow/logger/fileoutputter.rb +96 -0
- data/lib/arrow/logger/htmloutputter.rb +54 -0
- data/lib/arrow/logger/outputter.rb +123 -0
- data/lib/arrow/mixins.rb +425 -0
- data/lib/arrow/monkeypatches.rb +94 -0
- data/lib/arrow/object.rb +117 -0
- data/lib/arrow/path.rb +196 -0
- data/lib/arrow/service.rb +447 -0
- data/lib/arrow/session.rb +289 -0
- data/lib/arrow/session/dbstore.rb +100 -0
- data/lib/arrow/session/filelock.rb +160 -0
- data/lib/arrow/session/filestore.rb +132 -0
- data/lib/arrow/session/id.rb +98 -0
- data/lib/arrow/session/lock.rb +253 -0
- data/lib/arrow/session/md5id.rb +42 -0
- data/lib/arrow/session/nulllock.rb +42 -0
- data/lib/arrow/session/posixlock.rb +166 -0
- data/lib/arrow/session/sha1id.rb +54 -0
- data/lib/arrow/session/store.rb +366 -0
- data/lib/arrow/session/usertrackid.rb +52 -0
- data/lib/arrow/spechelpers.rb +73 -0
- data/lib/arrow/template.rb +713 -0
- data/lib/arrow/template/attr.rb +31 -0
- data/lib/arrow/template/call.rb +31 -0
- data/lib/arrow/template/comment.rb +33 -0
- data/lib/arrow/template/container.rb +118 -0
- data/lib/arrow/template/else.rb +41 -0
- data/lib/arrow/template/elsif.rb +44 -0
- data/lib/arrow/template/escape.rb +53 -0
- data/lib/arrow/template/export.rb +87 -0
- data/lib/arrow/template/for.rb +145 -0
- data/lib/arrow/template/if.rb +78 -0
- data/lib/arrow/template/import.rb +119 -0
- data/lib/arrow/template/include.rb +206 -0
- data/lib/arrow/template/iterator.rb +208 -0
- data/lib/arrow/template/nodes.rb +734 -0
- data/lib/arrow/template/parser.rb +571 -0
- data/lib/arrow/template/prettyprint.rb +53 -0
- data/lib/arrow/template/render.rb +191 -0
- data/lib/arrow/template/selectlist.rb +94 -0
- data/lib/arrow/template/set.rb +87 -0
- data/lib/arrow/template/timedelta.rb +81 -0
- data/lib/arrow/template/unless.rb +78 -0
- data/lib/arrow/template/urlencode.rb +51 -0
- data/lib/arrow/template/yield.rb +139 -0
- data/lib/arrow/templatefactory.rb +125 -0
- data/lib/arrow/testcase.rb +567 -0
- data/lib/arrow/transaction.rb +608 -0
- data/rake/191_compat.rb +26 -0
- data/rake/dependencies.rb +76 -0
- data/rake/documentation.rb +114 -0
- data/rake/helpers.rb +502 -0
- data/rake/hg.rb +282 -0
- data/rake/manual.rb +787 -0
- data/rake/packaging.rb +129 -0
- data/rake/publishing.rb +278 -0
- data/rake/style.rb +62 -0
- data/rake/svn.rb +668 -0
- data/rake/testing.rb +187 -0
- data/rake/verifytask.rb +64 -0
- data/spec/arrow/acceptparam_spec.rb +157 -0
- data/spec/arrow/applet_spec.rb +575 -0
- data/spec/arrow/appletmixins_spec.rb +409 -0
- data/spec/arrow/appletregistry_spec.rb +294 -0
- data/spec/arrow/broker_spec.rb +153 -0
- data/spec/arrow/config_spec.rb +224 -0
- data/spec/arrow/cookieset_spec.rb +164 -0
- data/spec/arrow/dispatcher_spec.rb +137 -0
- data/spec/arrow/dispatcherloader_spec.rb +65 -0
- data/spec/arrow/formvalidator_spec.rb +781 -0
- data/spec/arrow/logger_spec.rb +346 -0
- data/spec/arrow/mixins_spec.rb +120 -0
- data/spec/arrow/service_spec.rb +645 -0
- data/spec/arrow/session_spec.rb +121 -0
- data/spec/arrow/template/iterator_spec.rb +222 -0
- data/spec/arrow/templatefactory_spec.rb +185 -0
- data/spec/arrow/transaction_spec.rb +319 -0
- data/spec/arrow_spec.rb +37 -0
- data/spec/lib/appletmatchers.rb +281 -0
- data/spec/lib/constants.rb +77 -0
- data/spec/lib/helpers.rb +41 -0
- data/spec/lib/matchers.rb +44 -0
- data/tests/cookie.tests.rb +310 -0
- data/tests/path.tests.rb +157 -0
- data/tests/session.tests.rb +111 -0
- data/tests/session_id.tests.rb +82 -0
- data/tests/session_lock.tests.rb +191 -0
- data/tests/session_store.tests.rb +53 -0
- data/tests/template.tests.rb +1360 -0
- metadata +339 -0
data/lib/arrow/object.rb
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'arrow/path'
|
|
4
|
+
require 'arrow/exceptions'
|
|
5
|
+
require 'arrow/mixins'
|
|
6
|
+
require 'arrow/logger'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# This class is the abstract base class for all Arrow objects. Most of the
|
|
10
|
+
# Arrow classes inherit from this.
|
|
11
|
+
#
|
|
12
|
+
# == To Do
|
|
13
|
+
#
|
|
14
|
+
# All of this stuff should really be factored out into mixins.
|
|
15
|
+
#
|
|
16
|
+
# == Authors
|
|
17
|
+
#
|
|
18
|
+
# * Michael Granger <ged@FaerieMUD.org>
|
|
19
|
+
#
|
|
20
|
+
# Please see the file LICENSE in the top-level directory for licensing details.
|
|
21
|
+
#
|
|
22
|
+
class Arrow::Object < ::Object
|
|
23
|
+
include Arrow::Loggable
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Create a method that warns of deprecation for an instance method. If
|
|
27
|
+
### <tt>newSym</tt> is specified, the method is being renamed, and this
|
|
28
|
+
### method acts like an <tt>alias_method</tt> that logs a warning; if
|
|
29
|
+
### not, it is being removed, and the target method will be aliased to
|
|
30
|
+
### an internal method and wrapped in a warning method with the original
|
|
31
|
+
### name.
|
|
32
|
+
def self::deprecate_method( oldSym, newSym=oldSym )
|
|
33
|
+
warningMessage = ''
|
|
34
|
+
|
|
35
|
+
# If the method is being removed, alias it away somewhere and build
|
|
36
|
+
# an appropriate warning message. Otherwise, just build a warning
|
|
37
|
+
# message.
|
|
38
|
+
if oldSym == newSym
|
|
39
|
+
newSym = ("__deprecated_" + oldSym.to_s + "__").to_sym
|
|
40
|
+
warningMessage = "%s#%s is deprecated" %
|
|
41
|
+
[ self.name, oldSym.to_s ]
|
|
42
|
+
alias_method newSym, oldSym
|
|
43
|
+
else
|
|
44
|
+
warningMessage = "%s#%s is deprecated; use %s#%s instead" %
|
|
45
|
+
[ self.name, oldSym.to_s, self.name, newSym.to_s ]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Build the method that logs a warning and then calls the true
|
|
49
|
+
# method.
|
|
50
|
+
class_eval %Q{
|
|
51
|
+
def #{oldSym.to_s}( *args, &block )
|
|
52
|
+
self.log.notice "warning: %s: #{warningMessage}" % [ caller(1) ]
|
|
53
|
+
send( #{newSym.inspect}, *args, &block )
|
|
54
|
+
rescue => err
|
|
55
|
+
# Mangle exceptions to point someplace useful
|
|
56
|
+
Kernel.raise err, err.message, err.backtrace[2..-1]
|
|
57
|
+
end
|
|
58
|
+
}
|
|
59
|
+
rescue Exception => err
|
|
60
|
+
# Mangle exceptions to point someplace useful
|
|
61
|
+
frames = err.backtrace
|
|
62
|
+
frames.shift while frames.first =~ /#{__FILE__}/
|
|
63
|
+
Kernel.raise err, err.message, frames
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### Like Object.deprecate_method, but for class methods.
|
|
68
|
+
def self::deprecate_class_method( oldSym, newSym=oldSym )
|
|
69
|
+
warningMessage = ''
|
|
70
|
+
|
|
71
|
+
# If the method is being removed, alias it away somewhere and build
|
|
72
|
+
# an appropriate warning message. Otherwise, just build a warning
|
|
73
|
+
# message.
|
|
74
|
+
if oldSym == newSym
|
|
75
|
+
newSym = ("__deprecated_" + oldSym.to_s + "__").to_sym
|
|
76
|
+
warningMessage = "%s::%s is deprecated" %
|
|
77
|
+
[ self.name, oldSym.to_s ]
|
|
78
|
+
alias_class_method newSym, oldSym
|
|
79
|
+
else
|
|
80
|
+
warningMessage = "%s::%s is deprecated; use %s::%s instead" %
|
|
81
|
+
[ self.name, oldSym.to_s, self.name, newSym.to_s ]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Build the method that logs a warning and then calls the true
|
|
85
|
+
# method.
|
|
86
|
+
class_eval %Q{
|
|
87
|
+
def self::#{oldSym.to_s}( *args, &block )
|
|
88
|
+
Arrow::Logger.notice "warning: %s: #{warningMessage}" % [ caller(1) ]
|
|
89
|
+
send( #{newSym.inspect}, *args, &block )
|
|
90
|
+
rescue => err
|
|
91
|
+
# Mangle exceptions to point someplace useful
|
|
92
|
+
Kernel.raise err, err.message, err.backtrace[2..-1]
|
|
93
|
+
end
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
### Store the name of the file from which the inheriting +klass+ is
|
|
99
|
+
### being loaded.
|
|
100
|
+
def self::inherited( klass )
|
|
101
|
+
unless klass.instance_variables.include?( "@sourcefile" )
|
|
102
|
+
sourcefile = caller(1).find {|frame|
|
|
103
|
+
/inherited/ !~ frame
|
|
104
|
+
}.sub( /^([^:]+):.*/, "\\1" )
|
|
105
|
+
klass.instance_variable_set( "@sourcefile", sourcefile )
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
unless klass.respond_to?( :sourcefile )
|
|
109
|
+
class << klass
|
|
110
|
+
attr_reader :sourcefile
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
end # class Arrow::Object
|
|
117
|
+
|
data/lib/arrow/path.rb
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
require 'forwardable'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
|
|
7
|
+
require 'arrow'
|
|
8
|
+
require 'arrow/monkeypatches'
|
|
9
|
+
require 'arrow/constants'
|
|
10
|
+
require 'arrow/mixins'
|
|
11
|
+
require 'arrow/exceptions'
|
|
12
|
+
|
|
13
|
+
# The Arrow::Path class, which represents a collection of paths to
|
|
14
|
+
# search for various resources. Instances of this class are used to
|
|
15
|
+
# search for templates, applets, and other resources loaded by the
|
|
16
|
+
# server from a configured list of directories.
|
|
17
|
+
#
|
|
18
|
+
# == Synopsis
|
|
19
|
+
#
|
|
20
|
+
# require 'arrow/path'
|
|
21
|
+
#
|
|
22
|
+
# # Constructed from a String with PATH_SEPARATOR characters:
|
|
23
|
+
# template_path = Arrow::Path.new( ".:/www/templates:/usr/local/www/templates" )
|
|
24
|
+
#
|
|
25
|
+
# # ...or from an Array of Strings
|
|
26
|
+
# template_path = Arrow::Path.new([ '.', '/www/templates', '/usr/local/www/templates' ])
|
|
27
|
+
#
|
|
28
|
+
# # Return only those paths that exist, are directories, are readable
|
|
29
|
+
# # by the current user, and are not world-writable. This will use a
|
|
30
|
+
# # cached value if it has been built within
|
|
31
|
+
# # Arrow::Path::DEFAULT_CACHE_LIFESPAN seconds of the last fetch.
|
|
32
|
+
# paths = template_path.valid_dirs
|
|
33
|
+
#
|
|
34
|
+
# # Fetch without caching
|
|
35
|
+
# template_path.find_valid_dirs
|
|
36
|
+
#
|
|
37
|
+
# # ...or turn caching off and fetch
|
|
38
|
+
# template_path.cache_lifespan = 0
|
|
39
|
+
# paths = template_path.valid_dirs
|
|
40
|
+
#
|
|
41
|
+
# == Authors
|
|
42
|
+
#
|
|
43
|
+
# * Michael Granger <ged@FaerieMUD.org>
|
|
44
|
+
#
|
|
45
|
+
# Please see the file LICENSE in the top-level directory for licensing details.
|
|
46
|
+
#
|
|
47
|
+
class Arrow::Path
|
|
48
|
+
include Enumerable,
|
|
49
|
+
Arrow::Loggable,
|
|
50
|
+
Arrow::Constants
|
|
51
|
+
|
|
52
|
+
extend Forwardable
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# The character to split path Strings on, and join on when
|
|
56
|
+
# converting back to a String.
|
|
57
|
+
SEPARATOR = File::PATH_SEPARATOR
|
|
58
|
+
|
|
59
|
+
# How many seconds to cache directory stat information, in seconds.
|
|
60
|
+
DEFAULT_CACHE_LIFESPAN = 1.5
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
#############################################################
|
|
64
|
+
### C L A S S M E T H O D S
|
|
65
|
+
#############################################################
|
|
66
|
+
|
|
67
|
+
### Return the YAML type for this class
|
|
68
|
+
def self::to_yaml_type
|
|
69
|
+
"!%s/arrowPath" % [ Arrow::Constants::YAML_DOMAIN ]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
#############################################################
|
|
74
|
+
### I N S T A N C E M E T H O D S
|
|
75
|
+
#############################################################
|
|
76
|
+
|
|
77
|
+
### Create a new Arrow::Path object for the specified +path+, which can
|
|
78
|
+
### be either a String containing directory names separated by
|
|
79
|
+
### File::PATH_SEPARATOR, an Array of directory names, or an object
|
|
80
|
+
### which returns such an Array when #to_a is called on it. If
|
|
81
|
+
### +cache_lifespan+ is non-zero, the Array of valid directories will be
|
|
82
|
+
### cached for +cache_lifespan+ seconds to save calls to stat().
|
|
83
|
+
def initialize( path=[], cache_lifespan=DEFAULT_CACHE_LIFESPAN )
|
|
84
|
+
@dirs = case path
|
|
85
|
+
when Array
|
|
86
|
+
path.flatten
|
|
87
|
+
when String
|
|
88
|
+
path.split(SEPARATOR)
|
|
89
|
+
else
|
|
90
|
+
path.to_a.flatten
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
@dirs.collect! {|dir| dir.untaint.to_s }
|
|
94
|
+
|
|
95
|
+
@valid_dirs = []
|
|
96
|
+
@cache_lifespan = cache_lifespan
|
|
97
|
+
@last_stat = Time.at(0)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
######
|
|
102
|
+
public
|
|
103
|
+
######
|
|
104
|
+
|
|
105
|
+
# The raw list of directories contained in the path, including invalid
|
|
106
|
+
# (non-existent or unreadable) ones.
|
|
107
|
+
attr_accessor :dirs
|
|
108
|
+
|
|
109
|
+
# How long (in seconds) to cache the list of good
|
|
110
|
+
# directories. Setting this to 0 turns off caching.
|
|
111
|
+
attr_accessor :cache_lifespan
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
### Fetch the list of valid directories, using a cached value if the
|
|
115
|
+
### path has caching enabled (which is the default). Otherwise, it
|
|
116
|
+
### fetches the valid list via #find_valid_dirs and caches the result
|
|
117
|
+
### for #cache_lifespan seconds. If caching is disabled, this is
|
|
118
|
+
### equivalent to just calling #find_valid_dirs.
|
|
119
|
+
def valid_dirs
|
|
120
|
+
if ( @cache_lifespan.nonzero? &&
|
|
121
|
+
((Time.now - @last_stat) < @cache_lifespan) )
|
|
122
|
+
self.log.debug "Returning cached dirs."
|
|
123
|
+
return @valid_dirs
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@valid_dirs = self.find_valid_dirs
|
|
127
|
+
@last_stat = Time.now
|
|
128
|
+
|
|
129
|
+
return @valid_dirs
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
### Fetch the list of paths in the search path, vetted to only contain
|
|
134
|
+
### those that are not tainted, exist, are directories, are readable
|
|
135
|
+
### by the current user, and are not world-writable.
|
|
136
|
+
def find_valid_dirs
|
|
137
|
+
return @dirs.find_all do |dir|
|
|
138
|
+
if dir.tainted?
|
|
139
|
+
self.log.info "Discarding tainted directory entry %p" % [ dir ]
|
|
140
|
+
next
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
path = Pathname.new( dir )
|
|
144
|
+
|
|
145
|
+
if ! path.exist?
|
|
146
|
+
self.log.debug "Discarding non-existant path: %s" % [ path ]
|
|
147
|
+
next false
|
|
148
|
+
elsif ! path.directory?
|
|
149
|
+
self.log.debug "Discarding non-directory: %s" % [ path ]
|
|
150
|
+
next false
|
|
151
|
+
elsif ! path.readable?
|
|
152
|
+
self.log.debug "Discarding unreadable directory: %s" % [ path ]
|
|
153
|
+
next false
|
|
154
|
+
elsif( (path.stat.mode & 0002).nonzero? )
|
|
155
|
+
self.log.debug "Discarding world-writable directory: %s" % [ path ]
|
|
156
|
+
next false
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
true
|
|
160
|
+
end.map {|pn| pn.to_s }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Generate Array-ish methods that delegate to self.dirs
|
|
165
|
+
def_delegators :@dirs,
|
|
166
|
+
*(Array.instance_methods(false) -
|
|
167
|
+
Enumerable.instance_methods(false) -
|
|
168
|
+
[:to_yaml, :inspect, :to_s])
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
### Enumerable interface method. Iterate over the list of valid dirs
|
|
172
|
+
### in this path, calling the specified block for each.
|
|
173
|
+
def each( &block )
|
|
174
|
+
self.valid_dirs.each( &block )
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
### Return the path as a <tt>SEPARATOR</tt>-separated String.
|
|
179
|
+
def to_s
|
|
180
|
+
return self.valid_dirs.join( SEPARATOR )
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
### Return the path as YAML text
|
|
185
|
+
def to_yaml( opts={} )
|
|
186
|
+
require 'yaml'
|
|
187
|
+
YAML.quick_emit( self.object_id, opts ) {|out|
|
|
188
|
+
out.seq( self.class.to_yaml_type ){|seq|
|
|
189
|
+
seq.add( self.dirs )
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
end # class Arrow::Path
|
|
195
|
+
|
|
196
|
+
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/ruby
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
require 'arrow/applet'
|
|
7
|
+
require 'arrow/acceptparam'
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# This file contains the Arrow::Service class, a derivative of
|
|
11
|
+
# Arrow::Applet that provides some conveniences for creating REST-style
|
|
12
|
+
# service applets.
|
|
13
|
+
#
|
|
14
|
+
# It provides:
|
|
15
|
+
# * automatic content-type negotiation
|
|
16
|
+
# * automatic API description-generation for service actions
|
|
17
|
+
# * new action dispatch mechanism that takes the HTTP request method into account
|
|
18
|
+
# * convenience functions for returning a non-OK HTTP status
|
|
19
|
+
#
|
|
20
|
+
# == Authors
|
|
21
|
+
#
|
|
22
|
+
# * Michael Granger <ged@FaerieMUD.org>
|
|
23
|
+
#
|
|
24
|
+
# Please see the file LICENSE in the top-level directory for licensing details.
|
|
25
|
+
#
|
|
26
|
+
class Arrow::Service < Arrow::Applet
|
|
27
|
+
include Arrow::Loggable,
|
|
28
|
+
Arrow::HTMLUtilities,
|
|
29
|
+
Arrow::Constants
|
|
30
|
+
|
|
31
|
+
# Subversion revision
|
|
32
|
+
SVNRev = %q$Rev$
|
|
33
|
+
|
|
34
|
+
# VCS Id
|
|
35
|
+
SvnId = %q$Id$
|
|
36
|
+
|
|
37
|
+
# Map of HTTP methods to their Ruby equivalents as tuples of the form:
|
|
38
|
+
# [ :method_without_args, :method_with_args ]
|
|
39
|
+
METHOD_MAPPING = {
|
|
40
|
+
'OPTIONS' => [ :options, :options ],
|
|
41
|
+
'GET' => [ :fetch_all, :fetch ],
|
|
42
|
+
'HEAD' => [ :fetch_all, :fetch ],
|
|
43
|
+
'POST' => [ :create, :create ],
|
|
44
|
+
'PUT' => [ :update_all, :update ],
|
|
45
|
+
'DELETE' => [ :delete_all, :delete ],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Map of Ruby methods to their HTTP equivalents from either the single or collection URIs
|
|
49
|
+
HTTP_METHOD_MAPPING = {
|
|
50
|
+
:single => {
|
|
51
|
+
:options => 'OPTIONS',
|
|
52
|
+
:fetch => 'GET',
|
|
53
|
+
:create => 'POST',
|
|
54
|
+
:update => 'PUT',
|
|
55
|
+
:delete => 'DELETE',
|
|
56
|
+
},
|
|
57
|
+
:collection => {
|
|
58
|
+
:options => 'OPTIONS',
|
|
59
|
+
:fetch_all => 'GET',
|
|
60
|
+
:create => 'POST',
|
|
61
|
+
:update_all => 'PUT',
|
|
62
|
+
:delete_all => 'DELETE',
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# A registry of HTTP status codes that don't allow an entity body in the response.
|
|
67
|
+
BODILESS_HTTP_RESPONSE_CODES = [
|
|
68
|
+
Apache::HTTP_CONTINUE,
|
|
69
|
+
Apache::HTTP_SWITCHING_PROTOCOLS,
|
|
70
|
+
Apache::HTTP_PROCESSING,
|
|
71
|
+
Apache::HTTP_NO_CONTENT,
|
|
72
|
+
Apache::HTTP_RESET_CONTENT,
|
|
73
|
+
Apache::HTTP_NOT_MODIFIED,
|
|
74
|
+
Apache::HTTP_USE_PROXY,
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# The list of content-types and the corresponding message to send to transform
|
|
78
|
+
# a Ruby object to that content type, in order of preference. See #negotiate_content.
|
|
79
|
+
SERIALIZERS = [
|
|
80
|
+
['application/json', :to_json],
|
|
81
|
+
['text/x-yaml', :to_yaml],
|
|
82
|
+
['application/xml+rubyobject', :to_xml],
|
|
83
|
+
[RUBY_MARSHALLED_MIMETYPE, :dump],
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
# The list of content-types and the corresponding method on the service to use to
|
|
87
|
+
# transform it into something useful.
|
|
88
|
+
DESERIALIZERS = {
|
|
89
|
+
'application/json' => :deserialize_json_body,
|
|
90
|
+
'text/x-yaml' => :deserialize_yaml_body,
|
|
91
|
+
'application/x-www-form-urlencoded' => :deserialize_form_body,
|
|
92
|
+
'multipart/form-data' => :deserialize_form_body,
|
|
93
|
+
RUBY_MARSHALLED_MIMETYPE => :deserialize_marshalled_body,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# The content-type that's used for HTTP content negotiation if none
|
|
98
|
+
# is set on the transaction
|
|
99
|
+
DEFAULT_CONTENT_TYPE = RUBY_OBJECT_MIMETYPE
|
|
100
|
+
|
|
101
|
+
# The key for POSTed/PUT JSON entity bodies that will be unwrapped as a simple string value.
|
|
102
|
+
# This is necessary because JSON doesn't have a simple value type of its own, whereas all
|
|
103
|
+
# the other serialization types do.
|
|
104
|
+
SPECIAL_JSON_KEY = 'single_value'
|
|
105
|
+
|
|
106
|
+
# Struct for containing thrown HTTP status responses
|
|
107
|
+
StatusResponse = Struct.new( "ArrowServiceStatusResponse", :status, :message )
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
######
|
|
111
|
+
public
|
|
112
|
+
######
|
|
113
|
+
|
|
114
|
+
### OPTIONS /
|
|
115
|
+
### Return a service document containing links to all
|
|
116
|
+
### :TODO: Integrate HTTP Access Control preflighted requests?
|
|
117
|
+
### (https://developer.mozilla.org/en/HTTP_access_control)
|
|
118
|
+
def options( txn, *args )
|
|
119
|
+
allowed_methods = self.allowed_methods( args )
|
|
120
|
+
txn.headers_out['Allow'] = allowed_methods.join(', ')
|
|
121
|
+
txn.content_type = RUBY_OBJECT_MIMETYPE
|
|
122
|
+
|
|
123
|
+
return allowed_methods
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
#########
|
|
128
|
+
protected
|
|
129
|
+
#########
|
|
130
|
+
|
|
131
|
+
### Map the request in the given +txn+ to an action and return its name as a Symbol.
|
|
132
|
+
def get_action_name( txn, id=nil, *args )
|
|
133
|
+
http_method = txn.request_method
|
|
134
|
+
self.log.debug "Looking up service action for %s %s (%p)" %
|
|
135
|
+
[ http_method, txn.uri, args ]
|
|
136
|
+
|
|
137
|
+
tuple = METHOD_MAPPING[ txn.request_method ] or return :not_allowed
|
|
138
|
+
self.log.debug "Method mapping for %s is %p" % [ txn.request_method, tuple ]
|
|
139
|
+
|
|
140
|
+
if args.empty?
|
|
141
|
+
self.log.debug " URI refers to top-level resource"
|
|
142
|
+
msym = tuple[ id ? 1 : 0 ]
|
|
143
|
+
self.log.debug " picked the %p method (%s ID argument)" %
|
|
144
|
+
[ msym, id ? 'has an' : 'no' ]
|
|
145
|
+
|
|
146
|
+
else
|
|
147
|
+
self.log.debug " URI refers to a sub-resource (args = %p)" % [ args ]
|
|
148
|
+
ops = args.collect {|arg| arg[/^([a-z]\w+)$/, 1].untaint }
|
|
149
|
+
|
|
150
|
+
mname = "%s_%s" % [ tuple[1], ops.compact.join('_') ]
|
|
151
|
+
msym = mname.to_sym
|
|
152
|
+
self.log.debug " picked the %p method (args = %p)" % [ msym, args ]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
return msym, id, *args
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
### Given a +txn+, an +action+ name, and any other remaining URI path +args+ from
|
|
160
|
+
### the request, return a Method object that will handle the request (or at least something
|
|
161
|
+
### #call-able with #arity).
|
|
162
|
+
def find_action_method( txn, action, *args )
|
|
163
|
+
return self.method( action ) if self.respond_to?( action )
|
|
164
|
+
|
|
165
|
+
# Otherwise, return an appropriate error response
|
|
166
|
+
self.log.error "request for unimplemented %p action for %s" % [ action, txn.uri ]
|
|
167
|
+
return self.method( :not_allowed )
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
### Overridden to provide content-negotiation and error-handling.
|
|
172
|
+
def call_action_method( txn, action, id=nil, *args )
|
|
173
|
+
self.log.debug "calling %p( id: %p, args: %p ) for service request" %
|
|
174
|
+
[ action, id, args ]
|
|
175
|
+
content = nil
|
|
176
|
+
|
|
177
|
+
# Run the action. If it executes normally, 'content' will contain the
|
|
178
|
+
# object that should make up the response entity body. If :finish is
|
|
179
|
+
# thrown early, e.g. via #finish_with, content will be nil and
|
|
180
|
+
# http_status_response should contain a StatusResponse struct
|
|
181
|
+
http_status_response = catch( :finish ) do
|
|
182
|
+
if id
|
|
183
|
+
id = self.validate_id( id )
|
|
184
|
+
content = action.call( txn, id )
|
|
185
|
+
else
|
|
186
|
+
content = action.call( txn )
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
self.log.debug " service finished successfully"
|
|
190
|
+
nil # rvalue for catch
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Handle finishing with a status first
|
|
194
|
+
if content
|
|
195
|
+
txn.status ||= Apache::HTTP_OK
|
|
196
|
+
return self.negotiate_content( txn, content )
|
|
197
|
+
elsif http_status_response
|
|
198
|
+
status_code = http_status_response[:status].to_i
|
|
199
|
+
msg = http_status_response[:message]
|
|
200
|
+
return self.prepare_status_response( txn, status_code, msg )
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
return nil
|
|
204
|
+
rescue => err
|
|
205
|
+
raise if err.class.name =~ /^Spec::/
|
|
206
|
+
|
|
207
|
+
msg = "%s: %s %s" % [ err.class.name, err.message, err.backtrace.first ]
|
|
208
|
+
self.log.error( msg )
|
|
209
|
+
return self.prepare_status_response( txn, Apache::SERVER_ERROR, msg )
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
### Return a METHOD_NOT_ALLOWED response
|
|
214
|
+
def not_allowed( txn, *args )
|
|
215
|
+
txn.err_headers_out['Allow'] = self.build_allow_header( args )
|
|
216
|
+
finish_with( Apache::METHOD_NOT_ALLOWED, "%s is not allowed" % [txn.request_method] )
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
### Return a valid 'Allow' header for the receiver for the given +path_components+ (relative to
|
|
221
|
+
### its mountpoint)
|
|
222
|
+
def build_allow_header( path_components )
|
|
223
|
+
return self.allowed_methods( path_components ).join(', ')
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
### Return an Array of valid HTTP methods for the given +path_components+
|
|
228
|
+
def allowed_methods( path_components )
|
|
229
|
+
type = path_components.empty? ? :collection : :single
|
|
230
|
+
allowed = HTTP_METHOD_MAPPING[ type ].keys.
|
|
231
|
+
find_all {|msym| self.respond_to?(msym) }.
|
|
232
|
+
inject([]) {|ary,msym| ary << HTTP_METHOD_MAPPING[type][msym]; ary }
|
|
233
|
+
|
|
234
|
+
allowed += ['HEAD'] if allowed.include?( 'GET' )
|
|
235
|
+
return allowed.uniq.sort
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
### Validates the given string as a non-negative integer, either
|
|
239
|
+
### returning it after untainting it or aborting with BAD_REQUEST. Override this
|
|
240
|
+
### in your service if your resource IDs aren't integers.
|
|
241
|
+
def validate_id( id )
|
|
242
|
+
self.log.debug "validating ID %p" % [ id ]
|
|
243
|
+
finish_with Apache::BAD_REQUEST, "missing ID" if id.nil?
|
|
244
|
+
finish_with Apache::BAD_REQUEST, "malformed or invalid ID: #{id}" unless
|
|
245
|
+
id =~ /^\d+$/
|
|
246
|
+
|
|
247
|
+
id.untaint
|
|
248
|
+
return Integer( id )
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
### Format the given +content+ according to the content-negotiation
|
|
253
|
+
### headers of the request in the given +txn+.
|
|
254
|
+
def negotiate_content( txn, content )
|
|
255
|
+
current_type = txn.content_type
|
|
256
|
+
|
|
257
|
+
# If the content is already in a form the client understands, just return it
|
|
258
|
+
# TODO: q-value upgrades?
|
|
259
|
+
if current_type && txn.accepts?( current_type )
|
|
260
|
+
self.log.debug " '%s' content already in acceptable form for '%s'" %
|
|
261
|
+
[ current_type, txn.normalized_accept_string ]
|
|
262
|
+
return content
|
|
263
|
+
else
|
|
264
|
+
self.log.info "Negotiating a response which matches '%s' from a %p entity body" %
|
|
265
|
+
[ txn.normalized_accept_string, current_type || content.class ]
|
|
266
|
+
|
|
267
|
+
# See if SERIALIZERS has an available transform that the request
|
|
268
|
+
# accepts and the content supports.
|
|
269
|
+
SERIALIZERS.each do |type, msg|
|
|
270
|
+
if txn.explicitly_accepts?( type ) && content.respond_to?( msg )
|
|
271
|
+
self.log.debug " using %p to serialize the content to %p" % [ msg, type ]
|
|
272
|
+
serialized = content.send( msg )
|
|
273
|
+
txn.content_type = type
|
|
274
|
+
return serialized
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
self.log.debug " no matching serializers, trying a hypertext response"
|
|
278
|
+
|
|
279
|
+
# If the client can accept HTML, try to make an HTML response from whatever we have.
|
|
280
|
+
if txn.accepts_html?
|
|
281
|
+
self.log.debug " client accepts HTML"
|
|
282
|
+
return prepare_hypertext_response( txn, content )
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
return prepare_status_response( txn, Apache::NOT_ACCEPTABLE, "" )
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
### Set up the response in the specified +txn+ based on the specified +status_code+
|
|
291
|
+
### and +message+.
|
|
292
|
+
def prepare_status_response( txn, status_code, message )
|
|
293
|
+
self.log.info "Non-OK response: %d (%s)" % [ status_code, message ]
|
|
294
|
+
|
|
295
|
+
txn.status = status_code
|
|
296
|
+
|
|
297
|
+
# Some status codes allow explanatory text to be returned; some forbid it.
|
|
298
|
+
unless BODILESS_HTTP_RESPONSE_CODES.include?( status_code )
|
|
299
|
+
txn.content_type = 'text/plain'
|
|
300
|
+
return message.to_s
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# For bodiless responses, just tell the dispatcher that we've handled
|
|
304
|
+
# everything.
|
|
305
|
+
return true
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
### Convert the specified +content+ to HTML and return it wrapped in a minimal
|
|
310
|
+
### (X)HTML document. The +content+ will be transformed into an HTML fragment via
|
|
311
|
+
### its #html_inspect method (if it has one), or via
|
|
312
|
+
### Arrow::HtmlInspectableObject#make_html_for_object
|
|
313
|
+
def prepare_hypertext_response( txn, content )
|
|
314
|
+
self.log.debug "Preparing a hypertext response out of %p" %
|
|
315
|
+
[ txn.content_type || content.class ]
|
|
316
|
+
|
|
317
|
+
body = self.make_hypertext_from_content( content )
|
|
318
|
+
|
|
319
|
+
# Generate an HTML response
|
|
320
|
+
tmpl = self.load_template( :service )
|
|
321
|
+
tmpl.body = body
|
|
322
|
+
tmpl.txn = txn
|
|
323
|
+
tmpl.applet = self
|
|
324
|
+
|
|
325
|
+
txn.content_type = HTML_MIMETYPE
|
|
326
|
+
# txn.content_encoding = 'utf8'
|
|
327
|
+
|
|
328
|
+
return tmpl
|
|
329
|
+
end
|
|
330
|
+
template :service => 'service-response.tmpl'
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
### Make HTML from the given +content+, either via its #html_inspect method, or via
|
|
334
|
+
### Arrow::HTMLUtilities.make_html_for_object if it doesn't respond to #html_inspect.
|
|
335
|
+
def make_hypertext_from_content( content )
|
|
336
|
+
if content.respond_to?( :html_inspect )
|
|
337
|
+
self.log.debug " making hypertext from %p using %p" %
|
|
338
|
+
[ content, content.method(:html_inspect) ]
|
|
339
|
+
body = content.html_inspect
|
|
340
|
+
elsif content.respond_to?( :fetch ) && content.respond_to?( :collect )
|
|
341
|
+
self.log.debug " recursively hypertexting a collection"
|
|
342
|
+
body = content.collect {|o| self.make_hypertext_from_content(o) }.join("\n")
|
|
343
|
+
else
|
|
344
|
+
self.log.debug " using the generic HTML inspector"
|
|
345
|
+
body = make_html_for_object( content )
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
return body
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
### Read the request body from the specified transaction, deserialize it if
|
|
353
|
+
### necessary, and return one or more Ruby objects. If there isn't a deserializer
|
|
354
|
+
### in DESERIALIZERS that matches the request's `Content-type`, the request
|
|
355
|
+
### is aborted with an "Unsupported Media Type" (415) response.
|
|
356
|
+
def deserialize_request_body( txn )
|
|
357
|
+
content_type = txn.headers_in['content-type'].sub( /;.*/, '' ).strip
|
|
358
|
+
self.log.debug "Trying to deserialize a %p request body." % [ content_type ]
|
|
359
|
+
|
|
360
|
+
mname = DESERIALIZERS[ content_type ]
|
|
361
|
+
|
|
362
|
+
if mname && self.respond_to?( mname )
|
|
363
|
+
self.log.debug " calling deserializer: #%s" % [ mname ]
|
|
364
|
+
return self.send( mname, txn )
|
|
365
|
+
else
|
|
366
|
+
self.log.error " no support for %p requests: %s" % [
|
|
367
|
+
content_type,
|
|
368
|
+
mname ? "no implementation of the #{mname} method" : "unknown content-type"
|
|
369
|
+
]
|
|
370
|
+
finish_with( Apache::HTTP_UNSUPPORTED_MEDIA_TYPE,
|
|
371
|
+
"don't know how to handle %p requests" % [content_type, txn.request_method] )
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
### Deserialize the given transaction's request body from an HTML form.
|
|
377
|
+
def deserialize_form_body( txn )
|
|
378
|
+
return txn.all_params
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
### Deserialize the given transaction's request body as JSON and return it.
|
|
383
|
+
def deserialize_json_body( txn )
|
|
384
|
+
rval = JSON.load( txn )
|
|
385
|
+
if rval.is_a?( Hash ) && rval.keys == [ SPECIAL_JSON_KEY ]
|
|
386
|
+
return rval[ SPECIAL_JSON_KEY ]
|
|
387
|
+
else
|
|
388
|
+
return rval
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
### Deserialize the given transaction's request body as YAML and return it.
|
|
394
|
+
def deserialize_yaml_body( txn )
|
|
395
|
+
return YAML.load( txn )
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
### Deserialize the given transaction's request body as a marshalled Ruby
|
|
400
|
+
### object and return it.
|
|
401
|
+
def deserialize_marshalled_body( txn )
|
|
402
|
+
return Marshal.load( txn )
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
#######
|
|
407
|
+
private
|
|
408
|
+
#######
|
|
409
|
+
|
|
410
|
+
### Abort the current execution and return a response with the specified
|
|
411
|
+
### http_status code immediately. The specified +message+ will be logged,
|
|
412
|
+
### and will be included in any message that is returned as part of the
|
|
413
|
+
### response.
|
|
414
|
+
def finish_with( http_status, message, otherstuff={} )
|
|
415
|
+
http_response = otherstuff.merge( :status => http_status, :message => message )
|
|
416
|
+
throw :finish, http_response
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
### Deep untaint an object structure and return it.
|
|
421
|
+
def untaint_values( obj )
|
|
422
|
+
self.log.debug "Untainting a result %s" % [ obj.class.name ]
|
|
423
|
+
return obj unless obj.tainted?
|
|
424
|
+
newobj = nil
|
|
425
|
+
|
|
426
|
+
case obj
|
|
427
|
+
when Hash
|
|
428
|
+
newobj = {}
|
|
429
|
+
obj.each do |key,val|
|
|
430
|
+
newobj[ key ] = untaint_values( val )
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
when Array
|
|
434
|
+
# Arrow::Logger[ self ].debug "Untainting array %p" % val
|
|
435
|
+
newobj = obj.collect {|v| v.dup.untaint}
|
|
436
|
+
|
|
437
|
+
else
|
|
438
|
+
# Arrow::Logger[ self ].debug "Untainting %p" % val
|
|
439
|
+
newobj = obj.dup
|
|
440
|
+
newobj.untaint
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
return newobj
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
end # class Arrow::Service
|
|
447
|
+
|