arrow 1.0.7

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