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