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.
Files changed (198) hide show
  1. data/ChangeLog +1590 -0
  2. data/LICENSE +28 -0
  3. data/README +75 -0
  4. data/Rakefile +366 -0
  5. data/Rakefile.local +63 -0
  6. data/data/arrow/applets/TEMPLATE.rb.tpl +53 -0
  7. data/data/arrow/applets/args.rb +50 -0
  8. data/data/arrow/applets/config.rb +55 -0
  9. data/data/arrow/applets/error.rb +63 -0
  10. data/data/arrow/applets/files.rb +46 -0
  11. data/data/arrow/applets/inspect.rb +46 -0
  12. data/data/arrow/applets/nosuchapplet.rb +31 -0
  13. data/data/arrow/applets/status.rb +92 -0
  14. data/data/arrow/applets/test.rb +133 -0
  15. data/data/arrow/applets/tutorial/counter.rb +96 -0
  16. data/data/arrow/applets/tutorial/dingus.rb +67 -0
  17. data/data/arrow/applets/tutorial/hello.rb +34 -0
  18. data/data/arrow/applets/tutorial/hello2.rb +73 -0
  19. data/data/arrow/applets/tutorial/imgtext.rb +90 -0
  20. data/data/arrow/applets/tutorial/imgtext2.rb +286 -0
  21. data/data/arrow/applets/tutorial/index.rb +36 -0
  22. data/data/arrow/applets/tutorial/logo.rb +98 -0
  23. data/data/arrow/applets/tutorial/memcache.rb +61 -0
  24. data/data/arrow/applets/tutorial/missing.rb +37 -0
  25. data/data/arrow/applets/tutorial/protected.rb +100 -0
  26. data/data/arrow/applets/tutorial/redirector.rb +52 -0
  27. data/data/arrow/applets/tutorial/rndimages.rb +159 -0
  28. data/data/arrow/applets/tutorial/sharenotes.rb +83 -0
  29. data/data/arrow/applets/tutorial/subclassed-hello.rb +32 -0
  30. data/data/arrow/applets/tutorial/superhello.rb +72 -0
  31. data/data/arrow/applets/tutorial/timeclock.rb +78 -0
  32. data/data/arrow/applets/view-applet.rb +123 -0
  33. data/data/arrow/applets/view-template.rb +85 -0
  34. data/data/arrow/applets/wiki.rb +274 -0
  35. data/data/arrow/templates/TEMPLATE.tmpl.tpl +36 -0
  36. data/data/arrow/templates/applet-status.tmpl +153 -0
  37. data/data/arrow/templates/args-display.tmpl +120 -0
  38. data/data/arrow/templates/config/display-table.tmpl +36 -0
  39. data/data/arrow/templates/config/display.tmpl +36 -0
  40. data/data/arrow/templates/counter-deleted.tmpl +33 -0
  41. data/data/arrow/templates/counter.tmpl +59 -0
  42. data/data/arrow/templates/dingus.tmpl +55 -0
  43. data/data/arrow/templates/enumtable.tmpl +8 -0
  44. data/data/arrow/templates/error-display.tmpl +92 -0
  45. data/data/arrow/templates/filemap.tmpl +89 -0
  46. data/data/arrow/templates/hello-world-src.tmpl +34 -0
  47. data/data/arrow/templates/hello-world.tmpl +60 -0
  48. data/data/arrow/templates/imgtext/fontlist.tmpl +46 -0
  49. data/data/arrow/templates/imgtext/form.tmpl +70 -0
  50. data/data/arrow/templates/imgtext/reload-error.tmpl +40 -0
  51. data/data/arrow/templates/imgtext/reload.tmpl +55 -0
  52. data/data/arrow/templates/inspect/display.tmpl +80 -0
  53. data/data/arrow/templates/loginform.tmpl +64 -0
  54. data/data/arrow/templates/logout.tmpl +32 -0
  55. data/data/arrow/templates/memcache/display.tmpl +41 -0
  56. data/data/arrow/templates/navbar.incl +27 -0
  57. data/data/arrow/templates/nosuchapplet.tmpl +32 -0
  58. data/data/arrow/templates/printsource.tmpl +35 -0
  59. data/data/arrow/templates/protected.tmpl +36 -0
  60. data/data/arrow/templates/rndimages.tmpl +38 -0
  61. data/data/arrow/templates/service-response.tmpl +13 -0
  62. data/data/arrow/templates/sharenotes/display.tmpl +38 -0
  63. data/data/arrow/templates/status.tmpl +120 -0
  64. data/data/arrow/templates/templateviewer.tmpl +43 -0
  65. data/data/arrow/templates/test/harness.tmpl +57 -0
  66. data/data/arrow/templates/test/list.tmpl +48 -0
  67. data/data/arrow/templates/test/problem.tmpl +42 -0
  68. data/data/arrow/templates/tutorial/index.tmpl +37 -0
  69. data/data/arrow/templates/tutorial/missingapplet.tmpl +29 -0
  70. data/data/arrow/templates/view-applet-nosuch.tmpl +32 -0
  71. data/data/arrow/templates/view-applet.tmpl +40 -0
  72. data/data/arrow/templates/view-template.tmpl +83 -0
  73. data/data/arrow/templates/wiki/formerror.tmpl +47 -0
  74. data/data/arrow/templates/wiki/markup_help.incl +6 -0
  75. data/data/arrow/templates/wiki/new.tmpl +56 -0
  76. data/data/arrow/templates/wiki/new_system.tmpl +122 -0
  77. data/data/arrow/templates/wiki/sectionlist.tmpl +43 -0
  78. data/data/arrow/templates/wiki/show.tmpl +34 -0
  79. data/docs/manual/layouts/default.page +43 -0
  80. data/docs/manual/lib/api-filter.rb +81 -0
  81. data/docs/manual/lib/editorial-filter.rb +64 -0
  82. data/docs/manual/lib/examples-filter.rb +244 -0
  83. data/docs/manual/lib/links-filter.rb +117 -0
  84. data/lib/apache/fakerequest.rb +448 -0
  85. data/lib/apache/logger.rb +33 -0
  86. data/lib/arrow.rb +51 -0
  87. data/lib/arrow/acceptparam.rb +207 -0
  88. data/lib/arrow/applet.rb +725 -0
  89. data/lib/arrow/appletmixins.rb +218 -0
  90. data/lib/arrow/appletregistry.rb +590 -0
  91. data/lib/arrow/applettestcase.rb +503 -0
  92. data/lib/arrow/broker.rb +255 -0
  93. data/lib/arrow/cache.rb +176 -0
  94. data/lib/arrow/config-loaders/yaml.rb +75 -0
  95. data/lib/arrow/config.rb +615 -0
  96. data/lib/arrow/constants.rb +24 -0
  97. data/lib/arrow/cookie.rb +359 -0
  98. data/lib/arrow/cookieset.rb +108 -0
  99. data/lib/arrow/dispatcher.rb +368 -0
  100. data/lib/arrow/dispatcherloader.rb +50 -0
  101. data/lib/arrow/exceptions.rb +61 -0
  102. data/lib/arrow/fallbackhandler.rb +48 -0
  103. data/lib/arrow/formvalidator.rb +631 -0
  104. data/lib/arrow/htmltokenizer.rb +343 -0
  105. data/lib/arrow/logger.rb +488 -0
  106. data/lib/arrow/logger/apacheoutputter.rb +69 -0
  107. data/lib/arrow/logger/arrayoutputter.rb +63 -0
  108. data/lib/arrow/logger/coloroutputter.rb +111 -0
  109. data/lib/arrow/logger/fileoutputter.rb +96 -0
  110. data/lib/arrow/logger/htmloutputter.rb +54 -0
  111. data/lib/arrow/logger/outputter.rb +123 -0
  112. data/lib/arrow/mixins.rb +425 -0
  113. data/lib/arrow/monkeypatches.rb +94 -0
  114. data/lib/arrow/object.rb +117 -0
  115. data/lib/arrow/path.rb +196 -0
  116. data/lib/arrow/service.rb +447 -0
  117. data/lib/arrow/session.rb +289 -0
  118. data/lib/arrow/session/dbstore.rb +100 -0
  119. data/lib/arrow/session/filelock.rb +160 -0
  120. data/lib/arrow/session/filestore.rb +132 -0
  121. data/lib/arrow/session/id.rb +98 -0
  122. data/lib/arrow/session/lock.rb +253 -0
  123. data/lib/arrow/session/md5id.rb +42 -0
  124. data/lib/arrow/session/nulllock.rb +42 -0
  125. data/lib/arrow/session/posixlock.rb +166 -0
  126. data/lib/arrow/session/sha1id.rb +54 -0
  127. data/lib/arrow/session/store.rb +366 -0
  128. data/lib/arrow/session/usertrackid.rb +52 -0
  129. data/lib/arrow/spechelpers.rb +73 -0
  130. data/lib/arrow/template.rb +713 -0
  131. data/lib/arrow/template/attr.rb +31 -0
  132. data/lib/arrow/template/call.rb +31 -0
  133. data/lib/arrow/template/comment.rb +33 -0
  134. data/lib/arrow/template/container.rb +118 -0
  135. data/lib/arrow/template/else.rb +41 -0
  136. data/lib/arrow/template/elsif.rb +44 -0
  137. data/lib/arrow/template/escape.rb +53 -0
  138. data/lib/arrow/template/export.rb +87 -0
  139. data/lib/arrow/template/for.rb +145 -0
  140. data/lib/arrow/template/if.rb +78 -0
  141. data/lib/arrow/template/import.rb +119 -0
  142. data/lib/arrow/template/include.rb +206 -0
  143. data/lib/arrow/template/iterator.rb +208 -0
  144. data/lib/arrow/template/nodes.rb +734 -0
  145. data/lib/arrow/template/parser.rb +571 -0
  146. data/lib/arrow/template/prettyprint.rb +53 -0
  147. data/lib/arrow/template/render.rb +191 -0
  148. data/lib/arrow/template/selectlist.rb +94 -0
  149. data/lib/arrow/template/set.rb +87 -0
  150. data/lib/arrow/template/timedelta.rb +81 -0
  151. data/lib/arrow/template/unless.rb +78 -0
  152. data/lib/arrow/template/urlencode.rb +51 -0
  153. data/lib/arrow/template/yield.rb +139 -0
  154. data/lib/arrow/templatefactory.rb +125 -0
  155. data/lib/arrow/testcase.rb +567 -0
  156. data/lib/arrow/transaction.rb +608 -0
  157. data/rake/191_compat.rb +26 -0
  158. data/rake/dependencies.rb +76 -0
  159. data/rake/documentation.rb +114 -0
  160. data/rake/helpers.rb +502 -0
  161. data/rake/hg.rb +282 -0
  162. data/rake/manual.rb +787 -0
  163. data/rake/packaging.rb +129 -0
  164. data/rake/publishing.rb +278 -0
  165. data/rake/style.rb +62 -0
  166. data/rake/svn.rb +668 -0
  167. data/rake/testing.rb +187 -0
  168. data/rake/verifytask.rb +64 -0
  169. data/spec/arrow/acceptparam_spec.rb +157 -0
  170. data/spec/arrow/applet_spec.rb +575 -0
  171. data/spec/arrow/appletmixins_spec.rb +409 -0
  172. data/spec/arrow/appletregistry_spec.rb +294 -0
  173. data/spec/arrow/broker_spec.rb +153 -0
  174. data/spec/arrow/config_spec.rb +224 -0
  175. data/spec/arrow/cookieset_spec.rb +164 -0
  176. data/spec/arrow/dispatcher_spec.rb +137 -0
  177. data/spec/arrow/dispatcherloader_spec.rb +65 -0
  178. data/spec/arrow/formvalidator_spec.rb +781 -0
  179. data/spec/arrow/logger_spec.rb +346 -0
  180. data/spec/arrow/mixins_spec.rb +120 -0
  181. data/spec/arrow/service_spec.rb +645 -0
  182. data/spec/arrow/session_spec.rb +121 -0
  183. data/spec/arrow/template/iterator_spec.rb +222 -0
  184. data/spec/arrow/templatefactory_spec.rb +185 -0
  185. data/spec/arrow/transaction_spec.rb +319 -0
  186. data/spec/arrow_spec.rb +37 -0
  187. data/spec/lib/appletmatchers.rb +281 -0
  188. data/spec/lib/constants.rb +77 -0
  189. data/spec/lib/helpers.rb +41 -0
  190. data/spec/lib/matchers.rb +44 -0
  191. data/tests/cookie.tests.rb +310 -0
  192. data/tests/path.tests.rb +157 -0
  193. data/tests/session.tests.rb +111 -0
  194. data/tests/session_id.tests.rb +82 -0
  195. data/tests/session_lock.tests.rb +191 -0
  196. data/tests/session_store.tests.rb +53 -0
  197. data/tests/template.tests.rb +1360 -0
  198. metadata +339 -0
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'arrow/applet'
4
+ require 'arrow/acceptparam'
5
+
6
+ # :nodoc:
7
+ module Arrow
8
+
9
+ # A collection of functions for abstracting authentication and authorization
10
+ # away from Arrow::Applets. Applets which include this module should provide
11
+ # implementations of at least the #get_authenticated_user method, and may
12
+ # provide implementations of other methods to tailor the authentication for
13
+ # their particular applet.
14
+ #
15
+ # == Customization API
16
+ #
17
+ # [[#get_authenticated_user]]
18
+ # Override this method to provide the particulars of your authentication
19
+ # system. The method is given the Arrow::Transaction object that wraps the
20
+ # incoming request, and should return whatever kind of "user" object they
21
+ # wish to use. The only requirement for a user object as far as this mixin
22
+ # is concerned is that it must have a #to_s method, so even a simple username
23
+ # in a String will suffice. If no authorization is possible, return nil, which
24
+ # will cause the #login_action to be invoked.
25
+ # [[#user_is_authorized]]
26
+ # Override this method to provide authorization checks of an authenticated user
27
+ # (the one returned from #get_authenticated_user) against the incoming request.
28
+ # If the user is authorized to run the action, return +true+, else return
29
+ # +false+. Failed authorization will cause the #deny_access_action to be
30
+ # invoked.
31
+ # [[#login_action]]
32
+ # Override this method if you wish to customize the login process. By default,
33
+ # this returns a response that prompts the client using Basic HTTP
34
+ # authentication.
35
+ # [[#logout_action]]
36
+ # Override this method if you wish to customize the logout process. By default,
37
+ # this declines the request, which will tell Apache to try to handle the
38
+ # request itself.
39
+ # [[#deny_access_action]]
40
+ # Override this method if you wish to customize what happens when the client
41
+ # sends a request for a resource they are not authorized to interact with. By
42
+ # default, this method returns a simple HTTP FORBIDDEN response.
43
+ #
44
+ # == VCS Id
45
+ #
46
+ # $Id$
47
+ #
48
+ # == Authors
49
+ #
50
+ # * Michael Granger <ged@FaerieMUD.org>
51
+ #
52
+ # :include: LICENSE
53
+ #
54
+ #--
55
+ #
56
+ # Please see the file LICENSE in the top-level directory for licensing details.
57
+ #
58
+ module AppletAuthentication
59
+
60
+ ### Default AppletAuthentication API: provides login functionality for actions that
61
+ ### require authorization; override this to provide a login form. By default, this
62
+ ### just returns an HTTP UNAUTHORIZED response.
63
+ def login_action( txn, *args )
64
+ self.log.info "Prompting the client for authentication"
65
+ # :TODO: This really needs to set the WWW-Authenticate header...
66
+ txn.status = Apache::HTTP_UNAUTHORIZED
67
+ return "this resource requires authentication"
68
+ end
69
+
70
+
71
+ ### Default AppletAuthentication API: provides login functionality for actions that
72
+ ### require authorization; override this to customize the logout process. By default, this
73
+ ### just returns +nil+, which will decline the request.
74
+ def logout_action( txn, *args )
75
+ self.log.info "No logout action provided, passing the request off to the server"
76
+ return Apache::DECLINED
77
+ end
78
+
79
+
80
+ ### Default AppletAuthentication API: provides a hook for applets which have some
81
+ ### actions which require authorization to run; override this to provide a "Forbidden"
82
+ ### page. By default, this just returns an HTTP FORBIDDEN response.
83
+ def deny_access_action( txn, *args )
84
+ self.log.error "Unauthorized request for %s" % [ txn.uri ]
85
+ txn.status = Apache::FORBIDDEN
86
+ return "access denied"
87
+ end
88
+
89
+
90
+ #########
91
+ protected
92
+ #########
93
+
94
+ ### Check to see that the user is authenticated. If not attempt to
95
+ ### authenticate them via a form. If they are authenticated, or become
96
+ ### authenticated after the form action, call the supplied block with the
97
+ ### authenticated user.
98
+ def with_authentication( txn, *args )
99
+ self.log.debug "wrapping a block in authentication"
100
+
101
+ # If the user doesn't have a session user, go to the login form.
102
+ if user = self.get_authenticated_user( txn )
103
+ return yield( user )
104
+ else
105
+ self.log.warning "Authentication failed from %s for %s" %
106
+ [ txn.remote_host(Apache::REMOTE_NOLOOKUP), txn.the_request ]
107
+ return self.subrun( :login, txn, *args )
108
+ end
109
+ end
110
+
111
+
112
+ ### Wrap a block in authorization. If the given +user+ has all of the
113
+ ### necessary permissions to run the given +applet_chain+ (an Array of
114
+ ### Arrow::AppRegistry::ChainLink structs), call the provided block.
115
+ ### Otherwise run the 'deny_access' action and return the result.
116
+ def with_authorization( txn, *args )
117
+ self.with_authentication( txn ) do |user|
118
+ self.log.debug "Checking permissions of '%s' to execute %s" % [ user, txn.uri ]
119
+
120
+ if self.user_is_authorized( user, txn, *args )
121
+ return yield
122
+ else
123
+ self.log.warning "Access denied to %s for %s" % [ user, txn.the_request ]
124
+ return self.subrun( :deny_access, txn )
125
+ end
126
+ end
127
+ end
128
+
129
+
130
+ ### Default AppletAuthentication API: return a "user" object if the specified +txn+
131
+ ### object provides authentication. Applets wishing to authenticate uses should
132
+ ### provide an overriding implementation of this method. The base implementation
133
+ ### always returns +nil+.
134
+ def get_authenticated_user( txn )
135
+ self.log.notice "No implementation of get_authenticated_user for %s" %
136
+ [ self.class.signature.name ]
137
+ return nil
138
+ end
139
+
140
+
141
+ ### Default AppletAuthentication API: returns true if the specified +user+ is
142
+ ### authorized to run the applet. Applets wishing to authorize users should
143
+ ### provide an overriding implementation of this method. The base implementation
144
+ ### always returns +false+.
145
+ def user_is_authorized( user, txn, *args )
146
+ self.log.notice "No implementation of user_is_authorized for %s" %
147
+ [ self.class.signature.name ]
148
+ return false
149
+ end
150
+
151
+
152
+ end # module AppletAuthentication
153
+
154
+
155
+ ### Add access-control to all actions and then allow them to be removed on a per-action
156
+ ### basis via a directive.
157
+ module AccessControls
158
+ include Arrow::AppletAuthentication
159
+
160
+ # Actions which don't go through access control
161
+ UNAUTHENTICATED_ACTIONS = [
162
+ :deny_access, :login, :logout
163
+ ].freeze
164
+
165
+
166
+ ### Methods to add to including classes
167
+ module ClassMethods
168
+ ### Allow declaration of actions which don't require authentication -- all other
169
+ ### methods are authenticated by default
170
+ def unauthenticated_actions( *actions )
171
+ @unauthenticated_actions.push( *actions )
172
+ return @unauthenticated_actions
173
+ end
174
+ alias :unauthenticated_action :unauthenticated_actions
175
+
176
+ end
177
+
178
+
179
+ ### Inclusion callback
180
+ def self::included( mod )
181
+ Arrow::Logger[ self ].debug "Adding declarative method to %p" % [ mod ]
182
+ mod.instance_variable_set( :@unauthenticated_actions, UNAUTHENTICATED_ACTIONS.dup )
183
+ mod.extend( ClassMethods )
184
+ super
185
+ end
186
+
187
+
188
+ ### Overridden to map the +action+ to the authorization action's method if
189
+ ### +action+ isn't one of the ones that's defined as unauthenticated.
190
+ def find_action_method( txn, action=nil, *args )
191
+ if self.class.unauthenticated_actions.include?( action )
192
+ self.log.debug "Supering to unauthenticated action %p" % [ action ]
193
+ super
194
+ else
195
+ self.log.debug "Action %p wasn't marked as unauthenticated; checking authorization." %
196
+ [ action ]
197
+ with_authorization( txn, action, *args ) do
198
+ super
199
+ end
200
+ end
201
+ end
202
+
203
+
204
+ ### Delegate to applets further on in the chain only if the user is authorized.
205
+ def delegate( txn, chain, *args )
206
+ self.log.debug "Delegating to chain: %p" % [ chain ]
207
+
208
+ with_authorization( txn, chain ) do
209
+ yield( chain )
210
+ end
211
+ end
212
+
213
+ end # AccessControls
214
+
215
+
216
+ end # module Arrow
217
+
218
+
@@ -0,0 +1,590 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'arrow/broker'
4
+ require 'forwardable'
5
+
6
+
7
+
8
+ # The Arrow::AppletRegistry class, a derivative of
9
+ # Arrow::Object. Instances of this class are responsible for loading and
10
+ # maintaining the collection of Arrow::Applets registered with an
11
+ # Arrow::Broker.
12
+ #
13
+ # == VCS Id
14
+ #
15
+ # $Id$
16
+ #
17
+ # == Authors
18
+ #
19
+ # * Michael Granger <ged@FaerieMUD.org>
20
+ #
21
+ # Please see the file LICENSE in the top-level directory for licensing details.
22
+ #
23
+ class Arrow::AppletRegistry < Arrow::Object
24
+ extend Forwardable
25
+ include Enumerable, Arrow::Loggable
26
+
27
+ # Pattern for matching valid components of the uri
28
+ IDENTIFIER = /^\w[-\w]*/
29
+
30
+
31
+ # Link in an applet chain
32
+ ChainLink = Struct.new( 'ArrowAppletChainLink', :applet, :path, :args )
33
+
34
+ ### Registry applet filemap data structure.
35
+ class AppletFile < Arrow::Object
36
+
37
+ DEFAULT_SOURCE_WINDOW_SIZE = 20
38
+
39
+ ### Create a new Arrow::AppletRegistry::AppletFile for the applet at
40
+ ### the given +path+.
41
+ def initialize( path )
42
+ @path = path
43
+ @uris = []
44
+ @appletclasses = nil
45
+ @timestamp = File.mtime( path )
46
+ @exception = nil
47
+ end
48
+
49
+
50
+ ######
51
+ public
52
+ ######
53
+
54
+ # The fully-qualified path to the applet file
55
+ attr_reader :path
56
+
57
+ # An Array of URIs that applets contained in this file are mapped to
58
+ attr_reader :uris
59
+
60
+ # A Time object representing the modification time of the file when it was loaded
61
+ attr_reader :timestamp
62
+
63
+ # The Exception object that was thrown when trying to load this file, if any
64
+ attr_accessor :exception
65
+
66
+
67
+ ### Returns +true+ if this file loaded without error
68
+ def loaded_okay?
69
+ @exception.nil?
70
+ end
71
+
72
+
73
+ ### Returns +true+ if the corresponding file has changed since it was loaded
74
+ def has_changed?
75
+ @timestamp != File.mtime( path )
76
+ end
77
+
78
+
79
+ ### Returns an Array of Arrow::Applet classes loaded from this file, loading
80
+ ### them if they haven't already been loaded.
81
+ def appletclasses
82
+ unless @appletclasses
83
+ self.log.debug "Loading applet classes from #{@path}"
84
+ @appletclasses = Arrow::Applet.load( @path )
85
+ end
86
+
87
+ rescue ::Exception => err
88
+ @exception = err
89
+ frames = self.filtered_backtrace
90
+ self.log.error "%s failed to load: %s" % [ path, err.message ]
91
+ self.log.debug " " + frames.collect {|frame| "[%s]" % frame}.join(" ")
92
+ @appletclasses = []
93
+ ensure
94
+ return @appletclasses
95
+ end
96
+
97
+
98
+ ### Return the lines of the applet exception's backtrace up to the
99
+ ### first frame of the framework. Returns an empty Array if there is
100
+ ### no current exception.
101
+ def filtered_backtrace
102
+ return [] unless @exception
103
+
104
+ filtered = []
105
+ @exception.backtrace.each do |frame|
106
+ break if frame.include?('lib/arrow/')
107
+ filtered.push( frame )
108
+ end
109
+
110
+ return filtered
111
+ end
112
+
113
+
114
+ ### Return the lines from the applet's source as an Array.
115
+ def source_lines
116
+ return File.readlines( @path )
117
+ end
118
+
119
+
120
+ ### Return +window_size+ lines of the source from the applet
121
+ ### surrounding the specified +linenum+ as an Array of Hashes of the
122
+ ### form:
123
+ ### {
124
+ ### :source => <line of source code>,
125
+ ### :linenum => <line number>,
126
+ ### :target => <true if this is the target line>
127
+ ### }
128
+ def source_window( linenum, window_size=DEFAULT_SOURCE_WINDOW_SIZE )
129
+ linenum -= 1
130
+ before_line = linenum - (window_size / 2)
131
+ after_line = linenum + (window_size / 2.0).ceil
132
+
133
+ before_line = 0 if before_line < 0
134
+
135
+ self.log.debug "Reading lines %d-%d from %s for source window on line %d" %
136
+ [ before_line, after_line, @path, linenum + 1 ]
137
+
138
+ rval = []
139
+ lines = self.source_lines[ before_line .. after_line ]
140
+ lines.each_with_index do |line, i|
141
+ rval << {
142
+ :source => line.chomp,
143
+ :linenum => before_line + i + 1,
144
+ :target => (before_line + i) == linenum,
145
+ }
146
+ end
147
+
148
+ return rval
149
+ end
150
+
151
+
152
+ ### Return the line of the exception that occurred while loading the
153
+ ### applet, if any. If there was no exception, this method returns
154
+ ### +nil+.
155
+ def exception_line
156
+ return nil unless @exception
157
+ targetline = nil
158
+ line = nil
159
+
160
+ # ScriptErrors have the target line in the message; everything else
161
+ # is assumed to have it in the first line of the backtrace
162
+ if @exception.is_a?( ScriptError )
163
+ targetline = @exception.message
164
+ else
165
+ targetline = @exception.backtrace.first
166
+ end
167
+
168
+ #
169
+ if targetline =~ /.*:(\d+)(?::.*)?$/
170
+ line = Integer( $1 )
171
+ else
172
+ raise "Couldn't parse exception backtrace '%s' for error line." %
173
+ [ targetline ]
174
+ end
175
+
176
+ return line
177
+ end
178
+
179
+
180
+ ### Return +window_size+ lines surrounding the line of the applet's
181
+ ### loading exception. If there was no loading exception, returns
182
+ ### an empty Array.
183
+ def exception_source_window( window_size=DEFAULT_SOURCE_WINDOW_SIZE )
184
+ return [] unless @exception
185
+ return self.source_window( self.exception_line, window_size )
186
+ end
187
+
188
+ end
189
+
190
+
191
+ #################################################################
192
+ ### C L A S S M E T H O D S
193
+ #################################################################
194
+
195
+ ### Get the 'gem home' from RubyGems and check it for sanity. Returns +nil+ if it
196
+ ### is not an extant, non-world-writable directory.
197
+ def self::get_safe_gemhome
198
+ gemhome = Pathname.new( Gem.user_home ) + 'gems'
199
+ gemhome.untaint
200
+
201
+ if ! gemhome.directory?
202
+ Arrow::Logger[ self ].notice "Gem home '%s' is not a directory; ignoring it" % [ gemhome ]
203
+ return nil
204
+ elsif (gemhome.stat.mode & 0002).nonzero?
205
+ Arrow::Logger[ self ].notice "Gem home '%s' is world-writable; ignoring it" % [ gemhome ]
206
+ return nil
207
+ end
208
+
209
+ Arrow::Logger[ self ].info "Got safe gem home: %p" % [ gemhome ]
210
+ return gemhome
211
+ end
212
+
213
+
214
+
215
+
216
+ #################################################################
217
+ ### I N S T A N C E M E T H O D S
218
+ #################################################################
219
+
220
+ # The stuff the registry needs:
221
+ #
222
+ # * Map of uri to applet object [maps incoming requests to applet/s]
223
+ # * Map of file to uri/s [for deleting entries from map of uris when a file disappears]
224
+
225
+ ### Create a new Arrow::AppletRegistry object.
226
+ def initialize( config )
227
+ @config = config
228
+
229
+ @path = @config.applets.path
230
+ @classmap = nil
231
+ @filemap = {}
232
+ @urispace = {}
233
+ @template_factory = Arrow::TemplateFactory.new( config )
234
+ @load_time = nil
235
+
236
+ self.load_gems
237
+ self.load_applets
238
+
239
+ super()
240
+ end
241
+
242
+
243
+ ### Copy initializer -- reload applets for cloned registries.
244
+ def initialize_copy( other ) # :nodoc:
245
+ @config = other.config.dup
246
+
247
+ @path = @config.applets.path.dup
248
+ @classmap = nil
249
+ @filemap = {}
250
+ @urispace = {}
251
+ @template_factory = Arrow::TemplateFactory.new( config )
252
+ @load_time = nil
253
+
254
+ self.load_gems
255
+ self.load_applets
256
+
257
+ super
258
+ end
259
+
260
+
261
+ ######
262
+ public
263
+ ######
264
+
265
+ # The internal hash of Entry objects, keyed by URI
266
+ attr_reader :urispace
267
+
268
+ # The internal hash of Entry objects keyed by the file they were loaded
269
+ # from
270
+ attr_reader :filemap
271
+
272
+ # The Arrow::Config object which specified the registry's behavior.
273
+ attr_reader :config
274
+
275
+ # The Arrow::TemplateFactory which will be given to any loaded applet
276
+ attr_reader :template_factory
277
+
278
+ # The Time when the registry was last loaded
279
+ attr_accessor :load_time
280
+
281
+ # The path the registry will search when looking for new/updated/deleted applets
282
+ attr_reader :path
283
+
284
+
285
+ # Delegate hash-ish methods to the uri-keyed internal hash
286
+ def_delegators :@urispace, :[], :[]=, :key?, :keys, :length, :each
287
+
288
+
289
+ ### Check the config for any gems to load, load them, and add their template and applet
290
+ ### directories to the appropriate parts of the config.
291
+ def load_gems
292
+ self.log.info "Loading gems."
293
+
294
+ unless @config.respond_to?( :gems )
295
+ self.log.debug "No gems section in the config; skipping gemified applets"
296
+ return
297
+ end
298
+
299
+ self.log.debug " using gem config: %p" % [ config.gems ]
300
+
301
+ # Make sure the 'gem home' is a directory and not world-writable; don't use it
302
+ # otherwise
303
+ gemhome = self.class.get_safe_gemhome
304
+ paths = @config.gems.path.collect {|path| path.untaint }
305
+ self.log.debug " safe gem paths: %p" % [ paths ]
306
+ Gem.use_paths( Apache.server_root, paths )
307
+
308
+ @config.gems.applets.to_h.each do |gemname, reqstring|
309
+ self.log.debug " trying to load %s %s" % [ gemname, reqstring ]
310
+ reqstring = '>= 0' if reqstring.nil? or reqstring.empty?
311
+
312
+ begin
313
+ self.log.info "Activating gem %s (%s)" % [ gemname, reqstring ]
314
+ Gem.activate( gemname.to_s, reqstring )
315
+ self.log.info " gem %s activated." % [ gemname ]
316
+ rescue LoadError => err
317
+ self.log.crit "%s while activating '%s': %s" %
318
+ [ err.class.name, gemname, err.message ]
319
+ err.backtrace.each do |frame|
320
+ self.log.debug " " + frame
321
+ end
322
+ else
323
+ datadir = Pathname.new( Gem.datadir(gemname.to_s) )
324
+ appletdir = datadir + 'applets'
325
+ templatedir = datadir + 'templates'
326
+ self.log.debug "Adding appletdir %p and templatedir %p" %
327
+ [ appletdir, templatedir ]
328
+ @path << appletdir.to_s
329
+ @template_factory.path << templatedir.to_s
330
+ end
331
+ end
332
+
333
+ self.log.info " done loading gems (path is now: %p)." % [ @path ]
334
+ end
335
+ alias_method :reload_gems, :load_gems
336
+
337
+
338
+ ### Loading and reloading the applet registy uses the following strategy:
339
+ ###
340
+ ### 1. Find all files in the config.applets.path matching the
341
+ ### config.applets.pattern.
342
+ ### 2. Remove from the registry any loaded applets which correspond to
343
+ ### applet files which are no longer in that list.
344
+ ### 3. For files which do exist in the path which were already loaded,
345
+ ### reload applets for any whose timestamp has changed since the applets
346
+ ### were loaded.
347
+ ### 4. For new files, load the applets they contain.
348
+
349
+ ### Load any new applets in the registry's path, reload any previously-
350
+ ### loaded applets whose files have changed, and discard any applets whose
351
+ ### files have disappeared.
352
+ def load_applets
353
+ self.log.debug "Loading applet registry"
354
+
355
+ @classmap = self.build_classmap
356
+ filelist = self.find_appletfiles
357
+
358
+ # Remove applet files which correspond to files that are no longer
359
+ # in the list
360
+ self.purge_deleted_applets( @filemap.keys - filelist ) unless
361
+ @filemap.empty?
362
+
363
+ # Now search the applet path for applet files
364
+ filelist.each do |appletfile|
365
+ self.log.debug "Found applet file %p" % appletfile
366
+ self.load_applets_from_file( appletfile )
367
+ self.log.debug "After %s, registry has %d entries" %
368
+ [ appletfile, @urispace.length ]
369
+ end
370
+
371
+ self.load_time = Time.now
372
+ end
373
+ alias_method :reload_applets, :load_applets
374
+
375
+
376
+ ### Find the chain of applets indicated by the given +uri+ and return an
377
+ ### Array of ChainLink structs.
378
+ def find_applet_chain( uri )
379
+ self.log.debug "Searching urispace for appletchain for %p" % [ uri ]
380
+
381
+ uri_parts = uri.sub(%r{^/(?=.)}, '').split(%r{/}).grep( IDENTIFIER )
382
+ appletchain = []
383
+ args = []
384
+
385
+ # If there's an applet installed at the base, prepend it to the
386
+ # appletchain
387
+ if @urispace.key?( "" )
388
+ appletchain << ChainLink.new( @urispace[""], "", uri_parts )
389
+ self.log.debug "Added base applet to chain."
390
+ end
391
+
392
+ # Only allow reference to internal handlers (handlers mapped to
393
+ # directories that start with '_') if allow_internal is set.
394
+ self.log.debug "Split URI into parts: %p" % [uri_parts]
395
+
396
+ # Map uri fragments onto registry entries, stopping at any element
397
+ # which isn't a valid Ruby identifier.
398
+ uri_parts.each_index do |i|
399
+ newuri = uri_parts[0,i+1].join("/")
400
+ # self.log.debug "Testing %s against %p" % [ newuri, @urispace.keys.sort ]
401
+ appletchain << ChainLink.new( @urispace[newuri], newuri, uri_parts[(i+1)..-1] ) if
402
+ @urispace.key?( newuri )
403
+ end
404
+
405
+ return appletchain
406
+ end
407
+
408
+
409
+ ### Check the applets path for new/updated/deleted applets if the poll
410
+ ### interval has passed.
411
+ def check_for_updates
412
+ interval = @config.applets.pollInterval
413
+ if interval.nonzero?
414
+ if Time.now - self.load_time > interval
415
+ self.log.debug "Checking for applet updates: poll interval at %ds" % [ interval ]
416
+ self.reload_applets
417
+ self.reload_gems
418
+ end
419
+ else
420
+ self.log.debug "Dynamic applet reloading turned off, continuing"
421
+ end
422
+ end
423
+
424
+
425
+ ### Remove the applets that were loaded from the given +missing_files+ from
426
+ ### the registry.
427
+ def purge_deleted_applets( *missing_files )
428
+
429
+ # For each filename, find the applets which were loaded from it,
430
+ # map the name of each applet to a uri via the classmap, and delete
431
+ # the entries by uri
432
+ missing_files.flatten.each do |filename|
433
+ self.log.info "Unregistering old applets from %p" % [ filename ]
434
+
435
+ @filemap[ filename ].uris.each do |uri|
436
+ self.log.debug " Removing %p, registered at %p" % [ @urispace[uri], uri ]
437
+ @urispace.delete( uri )
438
+ end
439
+
440
+ @filemap.delete( filename )
441
+ end
442
+ end
443
+
444
+
445
+ ### Load the applet classes from the given +path+ and return them
446
+ ### in an Array. If a block is given, then each loaded class is yielded
447
+ ### to the block in turn, and the return values are used in the Array
448
+ ### instead.
449
+ def load_applets_from_file( path )
450
+
451
+ # Reload mode -- don't do anything unless the file's been updated
452
+ if @filemap.key?( path )
453
+ file = @filemap[ path ]
454
+
455
+ if file.has_changed?
456
+ self.log.info "File %p has changed since loaded. Reloading." % [path]
457
+ self.purge_deleted_applets( path )
458
+ elsif !file.loaded_okay?
459
+ self.log.warning "File %s could not be loaded: %s" %
460
+ [path, file.exception.message]
461
+ file.exception.backtrace.each do |frame|
462
+ self.log.debug " " + frame
463
+ end
464
+ else
465
+ self.log.debug "File %p has not changed." % [path]
466
+ return nil
467
+ end
468
+ end
469
+
470
+ self.log.debug "Attempting to load applet objects from %p" % path
471
+ @filemap[ path ] = AppletFile.new( path )
472
+
473
+ @filemap[ path ].appletclasses.each do |appletclass|
474
+ self.log.debug "Registering applet class %s from %p" % [appletclass.name, path]
475
+ begin
476
+ uris = self.register_applet_class( appletclass )
477
+ @filemap[ path ].uris << uris
478
+ rescue ::Exception => err
479
+ frames = filter_backtrace( err.backtrace )
480
+ self.log.error "%s loaded, but failed to initialize: %s" % [
481
+ appletclass.normalized_name,
482
+ err.message,
483
+ ]
484
+ self.log.debug " " + frames.collect {|frame| "[%s]" % frame }.join(" ")
485
+ @filemap[ path ].exception = err
486
+ end
487
+ end
488
+
489
+ end
490
+
491
+
492
+ ### Register an instance of the given +klass+ with the broker if the
493
+ ### classmap includes it, returning the URIs which were mapped to
494
+ ### instances of the +klass+.
495
+ def register_applet_class( klass )
496
+ uris = []
497
+
498
+ # Trim the Module serving as private namespace from the
499
+ # class name
500
+ appletname = klass.normalized_name
501
+ self.log.debug "Registering %p applet as %p" % [ klass.name, appletname ]
502
+
503
+ # Look for a uri corresponding to the loaded class, and instantiate it
504
+ # if there is one.
505
+ if @classmap.key?( appletname )
506
+ self.log.debug " Found one or more uris for '%s'" % appletname
507
+
508
+
509
+ # Create a new instance of the applet for each uri it's
510
+ # registered under, then wrap that in a RegistryEntry
511
+ # and put it in the entries hash we'll return later.
512
+ @classmap[ appletname ].each do |uri|
513
+ @urispace[ uri ] = klass.new( @config, @template_factory, uri )
514
+ uris << uri
515
+ end
516
+ else
517
+ self.log.debug "No uri for '%s': Not instantiated" % appletname
518
+ end
519
+
520
+ return uris
521
+ end
522
+
523
+
524
+ ### Make and return a Hash which inverts the registry's applet
525
+ ### layout into a map of class name to the URIs onto which instances of
526
+ ### them should be installed.
527
+ def build_classmap
528
+ classmap = Hash.new {|ary,k| ary[k] = []}
529
+
530
+ # Invert the applet layout into Class => [ uris ] so as classes
531
+ # load, we know where to put 'em.
532
+ @config.applets.layout.each do |uri, klassname|
533
+ uri = uri.to_s.sub( %r{^/}, '' )
534
+ self.log.debug "Mapping %p to %p" % [ klassname, uri ]
535
+ classmap[ klassname ] << uri
536
+ end
537
+
538
+ return classmap
539
+ end
540
+
541
+
542
+ ### Find applet files by looking in the applets path of the registry's
543
+ ### configuration for files matching the configured pattern. Return an
544
+ ### Array of fully-qualified applet files. If the optional +excludeList+
545
+ ### is given, exclude any files specified from the return value.
546
+ def find_appletfiles( excludeList=[] )
547
+ files = []
548
+ dirCount = 0
549
+
550
+ # The Arrow::Path object will only give us extant directories...
551
+ @path.each do |path|
552
+
553
+ # Look for files under a directory
554
+ dirCount += 1
555
+ pat = File.join( path, @config.applets.pattern )
556
+ pat.untaint
557
+
558
+ self.log.debug "Looking for applets: %p" % [ pat ]
559
+ files.push( *Dir[ pat ] )
560
+ end
561
+
562
+ self.log.info "Fetched %d applet file paths from %d directories (out of %d)" %
563
+ [ files.nitems, dirCount, @path.dirs.nitems ]
564
+
565
+ files.each {|file| file.untaint }
566
+ return files - excludeList
567
+ end
568
+
569
+
570
+
571
+
572
+ #######
573
+ private
574
+ #######
575
+
576
+ ### Return frames from the given +backtrace+ that didn't come from the
577
+ ### current file.
578
+ def filter_backtrace( backtrace )
579
+ filtered = []
580
+ backtrace.each do |frame|
581
+ break if frame.include?(__FILE__)
582
+ filtered.push( frame )
583
+ end
584
+
585
+ return filtered
586
+ end
587
+
588
+ end # class Arrow::AppletRegistry
589
+
590
+