shopify-cli 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (179) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +1 -0
  3. data/.travis.yml +1 -0
  4. data/CHANGELOG.md +9 -0
  5. data/README.md +39 -7
  6. data/Rakefile +2 -0
  7. data/dev.yml +2 -2
  8. data/docs/_config.yml +1 -18
  9. data/docs/app/node/commands/index.md +2 -80
  10. data/docs/app/node/index.md +2 -33
  11. data/docs/app/rails/commands/index.md +2 -78
  12. data/docs/app/rails/index.md +2 -34
  13. data/docs/core/index.md +2 -84
  14. data/docs/getting-started/index.md +2 -25
  15. data/docs/getting-started/install/index.md +1 -118
  16. data/docs/getting-started/migrate/index.md +2 -94
  17. data/docs/getting-started/uninstall/index.md +2 -35
  18. data/docs/getting-started/upgrade/index.md +2 -39
  19. data/docs/help/start-app/index.md +2 -4
  20. data/docs/index.md +2 -24
  21. data/install.sh +1 -1
  22. data/lib/project_types/extension/cli.rb +19 -10
  23. data/lib/project_types/extension/commands/extension_command.rb +2 -2
  24. data/lib/project_types/extension/features/argo.rb +117 -0
  25. data/lib/project_types/extension/forms/create.rb +2 -2
  26. data/lib/project_types/extension/models/specification.rb +35 -0
  27. data/lib/project_types/extension/models/specification_handlers/checkout_post_purchase.rb +19 -0
  28. data/lib/project_types/extension/models/specification_handlers/default.rb +67 -0
  29. data/lib/project_types/extension/models/specifications.rb +77 -0
  30. data/lib/project_types/extension/tasks/configure_features.rb +52 -0
  31. data/lib/project_types/extension/tasks/fetch_specifications.rb +38 -0
  32. data/lib/project_types/node/commands/create.rb +3 -1
  33. data/lib/project_types/node/commands/generate.rb +2 -11
  34. data/lib/project_types/node/messages/messages.rb +9 -44
  35. data/lib/project_types/rails/commands/create.rb +8 -9
  36. data/lib/project_types/rails/forms/create.rb +1 -1
  37. data/lib/project_types/rails/gem.rb +1 -1
  38. data/lib/project_types/rails/messages/messages.rb +1 -1
  39. data/lib/project_types/script/cli.rb +7 -4
  40. data/lib/project_types/script/commands/create.rb +6 -4
  41. data/lib/project_types/script/commands/push.rb +5 -13
  42. data/lib/project_types/script/config/extension_points.yml +9 -5
  43. data/lib/project_types/script/errors.rb +17 -0
  44. data/lib/project_types/script/forms/create.rb +26 -2
  45. data/lib/project_types/script/graphql/app_script_update_or_create.graphql +10 -1
  46. data/lib/project_types/script/layers/application/build_script.rb +9 -4
  47. data/lib/project_types/script/layers/application/create_script.rb +12 -10
  48. data/lib/project_types/script/layers/application/extension_points.rb +24 -0
  49. data/lib/project_types/script/layers/application/push_script.rb +18 -16
  50. data/lib/project_types/script/layers/domain/errors.rb +4 -0
  51. data/lib/project_types/script/layers/domain/extension_point.rb +62 -6
  52. data/lib/project_types/script/layers/domain/metadata.rb +55 -0
  53. data/lib/project_types/script/layers/domain/push_package.rb +25 -6
  54. data/lib/project_types/script/layers/infrastructure/assemblyscript_project_creator.rb +6 -6
  55. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +16 -6
  56. data/lib/project_types/script/layers/infrastructure/extension_point_repository.rb +10 -4
  57. data/lib/project_types/script/layers/infrastructure/project_creator.rb +2 -1
  58. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +25 -13
  59. data/lib/project_types/script/layers/infrastructure/rust_project_creator.rb +72 -0
  60. data/lib/project_types/script/layers/infrastructure/rust_task_runner.rb +59 -0
  61. data/lib/project_types/script/layers/infrastructure/script_service.rb +7 -1
  62. data/lib/project_types/script/layers/infrastructure/task_runner.rb +4 -3
  63. data/lib/project_types/script/messages/messages.rb +39 -8
  64. data/lib/project_types/script/script_project.rb +25 -16
  65. data/lib/project_types/script/ui/error_handler.rb +34 -1
  66. data/lib/project_types/theme/cli.rb +40 -0
  67. data/lib/project_types/theme/commands/connect.rb +54 -0
  68. data/lib/project_types/theme/commands/create.rb +48 -0
  69. data/lib/project_types/theme/commands/deploy.rb +38 -0
  70. data/lib/project_types/theme/commands/generate.rb +20 -0
  71. data/lib/project_types/theme/commands/generate/env.rb +79 -0
  72. data/lib/project_types/theme/commands/push.rb +55 -0
  73. data/lib/project_types/theme/commands/serve.rb +31 -0
  74. data/lib/project_types/theme/forms/connect.rb +34 -0
  75. data/lib/project_types/theme/forms/create.rb +22 -0
  76. data/lib/project_types/theme/messages/messages.rb +147 -0
  77. data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +78 -0
  78. data/lib/project_types/theme/themekit.rb +113 -0
  79. data/lib/shopify-cli/admin_api.rb +42 -2
  80. data/lib/shopify-cli/api.rb +27 -24
  81. data/lib/shopify-cli/commands/system.rb +1 -1
  82. data/lib/shopify-cli/context.rb +23 -2
  83. data/lib/shopify-cli/feature.rb +0 -2
  84. data/lib/shopify-cli/http_request.rb +20 -8
  85. data/lib/shopify-cli/messages/messages.rb +6 -3
  86. data/lib/shopify-cli/method_object.rb +104 -0
  87. data/lib/shopify-cli/partners_api.rb +8 -2
  88. data/lib/shopify-cli/project_type.rb +1 -1
  89. data/lib/shopify-cli/resolve_constant.rb +25 -0
  90. data/lib/shopify-cli/result.rb +432 -0
  91. data/lib/shopify-cli/shopifolk.rb +3 -2
  92. data/lib/shopify-cli/tasks/select_org_and_shop.rb +6 -5
  93. data/lib/shopify-cli/tunnel.rb +7 -1
  94. data/lib/shopify-cli/version.rb +1 -1
  95. data/lib/shopify_cli.rb +4 -1
  96. data/shopify.fish +1 -1
  97. data/shopify.sh +1 -1
  98. data/vendor/deps/cli-kit/REVISION +1 -1
  99. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +2 -2
  100. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +3 -3
  101. data/vendor/deps/cli-ui/REVISION +1 -1
  102. data/vendor/deps/cli-ui/lib/cli/ui.rb +26 -22
  103. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +4 -6
  104. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +3 -3
  105. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +8 -9
  106. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +1 -1
  107. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +1 -0
  108. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +15 -3
  109. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +4 -11
  110. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +3 -5
  111. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +10 -10
  112. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +1 -1
  113. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +56 -0
  114. data/vendor/deps/webrick/.gitignore +9 -0
  115. data/vendor/deps/webrick/Gemfile +3 -0
  116. data/vendor/deps/webrick/LICENSE.txt +22 -0
  117. data/vendor/deps/webrick/README.md +61 -0
  118. data/vendor/deps/webrick/Rakefile +10 -0
  119. data/vendor/deps/webrick/lib/webrick.rb +232 -0
  120. data/vendor/deps/webrick/lib/webrick/accesslog.rb +157 -0
  121. data/vendor/deps/webrick/lib/webrick/cgi.rb +313 -0
  122. data/vendor/deps/webrick/lib/webrick/compat.rb +36 -0
  123. data/vendor/deps/webrick/lib/webrick/config.rb +158 -0
  124. data/vendor/deps/webrick/lib/webrick/cookie.rb +172 -0
  125. data/vendor/deps/webrick/lib/webrick/htmlutils.rb +30 -0
  126. data/vendor/deps/webrick/lib/webrick/httpauth.rb +96 -0
  127. data/vendor/deps/webrick/lib/webrick/httpauth/authenticator.rb +117 -0
  128. data/vendor/deps/webrick/lib/webrick/httpauth/basicauth.rb +116 -0
  129. data/vendor/deps/webrick/lib/webrick/httpauth/digestauth.rb +395 -0
  130. data/vendor/deps/webrick/lib/webrick/httpauth/htdigest.rb +132 -0
  131. data/vendor/deps/webrick/lib/webrick/httpauth/htgroup.rb +97 -0
  132. data/vendor/deps/webrick/lib/webrick/httpauth/htpasswd.rb +158 -0
  133. data/vendor/deps/webrick/lib/webrick/httpauth/userdb.rb +53 -0
  134. data/vendor/deps/webrick/lib/webrick/httpproxy.rb +354 -0
  135. data/vendor/deps/webrick/lib/webrick/httprequest.rb +636 -0
  136. data/vendor/deps/webrick/lib/webrick/httpresponse.rb +564 -0
  137. data/vendor/deps/webrick/lib/webrick/https.rb +152 -0
  138. data/vendor/deps/webrick/lib/webrick/httpserver.rb +294 -0
  139. data/vendor/deps/webrick/lib/webrick/httpservlet.rb +23 -0
  140. data/vendor/deps/webrick/lib/webrick/httpservlet/abstract.rb +152 -0
  141. data/vendor/deps/webrick/lib/webrick/httpservlet/cgi_runner.rb +47 -0
  142. data/vendor/deps/webrick/lib/webrick/httpservlet/cgihandler.rb +126 -0
  143. data/vendor/deps/webrick/lib/webrick/httpservlet/erbhandler.rb +88 -0
  144. data/vendor/deps/webrick/lib/webrick/httpservlet/filehandler.rb +552 -0
  145. data/vendor/deps/webrick/lib/webrick/httpservlet/prochandler.rb +47 -0
  146. data/vendor/deps/webrick/lib/webrick/httpstatus.rb +194 -0
  147. data/vendor/deps/webrick/lib/webrick/httputils.rb +512 -0
  148. data/vendor/deps/webrick/lib/webrick/httpversion.rb +76 -0
  149. data/vendor/deps/webrick/lib/webrick/log.rb +156 -0
  150. data/vendor/deps/webrick/lib/webrick/server.rb +381 -0
  151. data/vendor/deps/webrick/lib/webrick/ssl.rb +215 -0
  152. data/vendor/deps/webrick/lib/webrick/utils.rb +265 -0
  153. data/vendor/deps/webrick/lib/webrick/version.rb +18 -0
  154. data/vendor/deps/webrick/webrick.gemspec +74 -0
  155. metadata +70 -26
  156. data/docs/Gemfile +0 -5
  157. data/docs/Gemfile.lock +0 -258
  158. data/docs/_data/nav.yml +0 -35
  159. data/docs/_includes/footer.html +0 -15
  160. data/docs/_includes/head.html +0 -19
  161. data/docs/_includes/sidebar_nav.html +0 -22
  162. data/docs/_includes/toc.html +0 -112
  163. data/docs/_layouts/default.html +0 -79
  164. data/docs/css/docs.css +0 -157
  165. data/docs/images/header.png +0 -0
  166. data/docs/installing-ruby.md +0 -28
  167. data/lib/project_types/extension/features/argo/admin.rb +0 -20
  168. data/lib/project_types/extension/features/argo/base.rb +0 -129
  169. data/lib/project_types/extension/features/argo/checkout.rb +0 -20
  170. data/lib/project_types/extension/models/type.rb +0 -81
  171. data/lib/project_types/extension/models/types/checkout_post_purchase.rb +0 -23
  172. data/lib/project_types/extension/models/types/product_subscription.rb +0 -24
  173. data/lib/project_types/node/commands/generate/billing.rb +0 -39
  174. data/lib/project_types/node/commands/generate/page.rb +0 -59
  175. data/lib/project_types/node/commands/generate/webhook.rb +0 -37
  176. data/lib/project_types/script/layers/domain/script.rb +0 -18
  177. data/lib/project_types/script/layers/infrastructure/script_repository.rb +0 -47
  178. data/lib/project_types/script/templates/ts/as-pect.config.js +0 -27
  179. data/lib/project_types/script/templates/ts/as-pect.d.ts +0 -1
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # httpservlet.rb -- HTTPServlet Module
4
+ #
5
+ # Author: IPR -- Internet Programming with Ruby -- writers
6
+ # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU Yuuzou
7
+ # Copyright (c) 2002 Internet Programming with Ruby writers. All rights
8
+ # reserved.
9
+ #
10
+ # $IPR: abstract.rb,v 1.24 2003/07/11 11:16:46 gotoyuzo Exp $
11
+
12
+ require_relative '../htmlutils'
13
+ require_relative '../httputils'
14
+ require_relative '../httpstatus'
15
+
16
+ module WEBrick
17
+ module HTTPServlet
18
+ class HTTPServletError < StandardError; end
19
+
20
+ ##
21
+ # AbstractServlet allows HTTP server modules to be reused across multiple
22
+ # servers and allows encapsulation of functionality.
23
+ #
24
+ # By default a servlet will respond to GET, HEAD (through an alias to GET)
25
+ # and OPTIONS requests.
26
+ #
27
+ # By default a new servlet is initialized for every request. A servlet
28
+ # instance can be reused by overriding ::get_instance in the
29
+ # AbstractServlet subclass.
30
+ #
31
+ # == A Simple Servlet
32
+ #
33
+ # class Simple < WEBrick::HTTPServlet::AbstractServlet
34
+ # def do_GET request, response
35
+ # status, content_type, body = do_stuff_with request
36
+ #
37
+ # response.status = status
38
+ # response['Content-Type'] = content_type
39
+ # response.body = body
40
+ # end
41
+ #
42
+ # def do_stuff_with request
43
+ # return 200, 'text/plain', 'you got a page'
44
+ # end
45
+ # end
46
+ #
47
+ # This servlet can be mounted on a server at a given path:
48
+ #
49
+ # server.mount '/simple', Simple
50
+ #
51
+ # == Servlet Configuration
52
+ #
53
+ # Servlets can be configured via initialize. The first argument is the
54
+ # HTTP server the servlet is being initialized for.
55
+ #
56
+ # class Configurable < Simple
57
+ # def initialize server, color, size
58
+ # super server
59
+ # @color = color
60
+ # @size = size
61
+ # end
62
+ #
63
+ # def do_stuff_with request
64
+ # content = "<p " \
65
+ # %q{style="color: #{@color}; font-size: #{@size}"} \
66
+ # ">Hello, World!"
67
+ #
68
+ # return 200, "text/html", content
69
+ # end
70
+ # end
71
+ #
72
+ # This servlet must be provided two arguments at mount time:
73
+ #
74
+ # server.mount '/configurable', Configurable, 'red', '2em'
75
+
76
+ class AbstractServlet
77
+
78
+ ##
79
+ # Factory for servlet instances that will handle a request from +server+
80
+ # using +options+ from the mount point. By default a new servlet
81
+ # instance is created for every call.
82
+
83
+ def self.get_instance(server, *options)
84
+ self.new(server, *options)
85
+ end
86
+
87
+ ##
88
+ # Initializes a new servlet for +server+ using +options+ which are
89
+ # stored as-is in +@options+. +@logger+ is also provided.
90
+
91
+ def initialize(server, *options)
92
+ @server = @config = server
93
+ @logger = @server[:Logger]
94
+ @options = options
95
+ end
96
+
97
+ ##
98
+ # Dispatches to a +do_+ method based on +req+ if such a method is
99
+ # available. (+do_GET+ for a GET request). Raises a MethodNotAllowed
100
+ # exception if the method is not implemented.
101
+
102
+ def service(req, res)
103
+ method_name = "do_" + req.request_method.gsub(/-/, "_")
104
+ if respond_to?(method_name)
105
+ __send__(method_name, req, res)
106
+ else
107
+ raise HTTPStatus::MethodNotAllowed,
108
+ "unsupported method `#{req.request_method}'."
109
+ end
110
+ end
111
+
112
+ ##
113
+ # Raises a NotFound exception
114
+
115
+ def do_GET(req, res)
116
+ raise HTTPStatus::NotFound, "not found."
117
+ end
118
+
119
+ ##
120
+ # Dispatches to do_GET
121
+
122
+ def do_HEAD(req, res)
123
+ do_GET(req, res)
124
+ end
125
+
126
+ ##
127
+ # Returns the allowed HTTP request methods
128
+
129
+ def do_OPTIONS(req, res)
130
+ m = self.methods.grep(/\Ado_([A-Z]+)\z/) {$1}
131
+ m.sort!
132
+ res["allow"] = m.join(",")
133
+ end
134
+
135
+ private
136
+
137
+ ##
138
+ # Redirects to a path ending in /
139
+
140
+ def redirect_to_directory_uri(req, res)
141
+ if req.path[-1] != ?/
142
+ location = WEBrick::HTTPUtils.escape_path(req.path + "/")
143
+ if req.query_string && req.query_string.bytesize > 0
144
+ location << "?" << req.query_string
145
+ end
146
+ res.set_redirect(HTTPStatus::MovedPermanently, location)
147
+ end
148
+ end
149
+ end
150
+
151
+ end
152
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # cgi_runner.rb -- CGI launcher.
4
+ #
5
+ # Author: IPR -- Internet Programming with Ruby -- writers
6
+ # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU
7
+ # Copyright (c) 2002 Internet Programming with Ruby writers. All rights
8
+ # reserved.
9
+ #
10
+ # $IPR: cgi_runner.rb,v 1.9 2002/09/25 11:33:15 gotoyuzo Exp $
11
+
12
+ def sysread(io, size)
13
+ buf = +""
14
+ while size > 0
15
+ tmp = io.sysread(size)
16
+ buf << tmp
17
+ size -= tmp.bytesize
18
+ end
19
+ return buf
20
+ end
21
+
22
+ STDIN.binmode
23
+
24
+ len = sysread(STDIN, 8).to_i
25
+ out = sysread(STDIN, len)
26
+ STDOUT.reopen(File.open(out, "w"))
27
+
28
+ len = sysread(STDIN, 8).to_i
29
+ err = sysread(STDIN, len)
30
+ STDERR.reopen(File.open(err, "w"))
31
+
32
+ len = sysread(STDIN, 8).to_i
33
+ dump = sysread(STDIN, len)
34
+ hash = Marshal.restore(dump)
35
+ ENV.keys.each{|name| ENV.delete(name) }
36
+ hash.each{|k, v| ENV[k] = v if v }
37
+
38
+ dir = File::dirname(ENV["SCRIPT_FILENAME"])
39
+ Dir::chdir dir
40
+
41
+ if ARGV[0]
42
+ argv = ARGV.dup
43
+ argv << ENV["SCRIPT_FILENAME"]
44
+ exec(*argv)
45
+ # NOTREACHED
46
+ end
47
+ exec ENV["SCRIPT_FILENAME"]
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # cgihandler.rb -- CGIHandler Class
4
+ #
5
+ # Author: IPR -- Internet Programming with Ruby -- writers
6
+ # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
7
+ # Copyright (c) 2002 Internet Programming with Ruby writers. All rights
8
+ # reserved.
9
+ #
10
+ # $IPR: cgihandler.rb,v 1.27 2003/03/21 19:56:01 gotoyuzo Exp $
11
+
12
+ require 'rbconfig'
13
+ require 'tempfile'
14
+ require_relative '../config'
15
+ require_relative 'abstract'
16
+
17
+ module WEBrick
18
+ module HTTPServlet
19
+
20
+ ##
21
+ # Servlet for handling CGI scripts
22
+ #
23
+ # Example:
24
+ #
25
+ # server.mount('/cgi/my_script', WEBrick::HTTPServlet::CGIHandler,
26
+ # '/path/to/my_script')
27
+
28
+ class CGIHandler < AbstractServlet
29
+ Ruby = RbConfig.ruby # :nodoc:
30
+ CGIRunner = "\"#{Ruby}\" \"#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb\"" # :nodoc:
31
+ CGIRunnerArray = [Ruby, "#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb".freeze].freeze # :nodoc:
32
+
33
+ ##
34
+ # Creates a new CGI script servlet for the script at +name+
35
+
36
+ def initialize(server, name)
37
+ super(server, name)
38
+ @script_filename = name
39
+ @tempdir = server[:TempDir]
40
+ interpreter = server[:CGIInterpreter]
41
+ if interpreter.is_a?(Array)
42
+ @cgicmd = CGIRunnerArray + interpreter
43
+ else
44
+ @cgicmd = "#{CGIRunner} #{interpreter}"
45
+ end
46
+ end
47
+
48
+ # :stopdoc:
49
+
50
+ def do_GET(req, res)
51
+ cgi_in = IO::popen(@cgicmd, "wb")
52
+ cgi_out = Tempfile.new("webrick.cgiout.", @tempdir, mode: IO::BINARY)
53
+ cgi_out.set_encoding("ASCII-8BIT")
54
+ cgi_err = Tempfile.new("webrick.cgierr.", @tempdir, mode: IO::BINARY)
55
+ cgi_err.set_encoding("ASCII-8BIT")
56
+ begin
57
+ cgi_in.sync = true
58
+ meta = req.meta_vars
59
+ meta["SCRIPT_FILENAME"] = @script_filename
60
+ meta["PATH"] = @config[:CGIPathEnv]
61
+ meta.delete("HTTP_PROXY")
62
+ if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM
63
+ meta["SystemRoot"] = ENV["SystemRoot"]
64
+ end
65
+ dump = Marshal.dump(meta)
66
+
67
+ cgi_in.write("%8d" % cgi_out.path.bytesize)
68
+ cgi_in.write(cgi_out.path)
69
+ cgi_in.write("%8d" % cgi_err.path.bytesize)
70
+ cgi_in.write(cgi_err.path)
71
+ cgi_in.write("%8d" % dump.bytesize)
72
+ cgi_in.write(dump)
73
+
74
+ req.body { |chunk| cgi_in.write(chunk) }
75
+ ensure
76
+ cgi_in.close
77
+ status = $?.exitstatus
78
+ sleep 0.1 if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM
79
+ data = cgi_out.read
80
+ cgi_out.close(true)
81
+ if errmsg = cgi_err.read
82
+ if errmsg.bytesize > 0
83
+ @logger.error("CGIHandler: #{@script_filename}:\n" + errmsg)
84
+ end
85
+ end
86
+ cgi_err.close(true)
87
+ end
88
+
89
+ if status != 0
90
+ @logger.error("CGIHandler: #{@script_filename} exit with #{status}")
91
+ end
92
+
93
+ data = "" unless data
94
+ raw_header, body = data.split(/^[\xd\xa]+/, 2)
95
+ raise HTTPStatus::InternalServerError,
96
+ "Premature end of script headers: #{@script_filename}" if body.nil?
97
+
98
+ begin
99
+ header = HTTPUtils::parse_header(raw_header)
100
+ if /^(\d+)/ =~ header['status'][0]
101
+ res.status = $1.to_i
102
+ header.delete('status')
103
+ end
104
+ if header.has_key?('location')
105
+ # RFC 3875 6.2.3, 6.2.4
106
+ res.status = 302 unless (300...400) === res.status
107
+ end
108
+ if header.has_key?('set-cookie')
109
+ header['set-cookie'].each{|k|
110
+ res.cookies << Cookie.parse_set_cookie(k)
111
+ }
112
+ header.delete('set-cookie')
113
+ end
114
+ header.each{|key, val| res[key] = val.join(", ") }
115
+ rescue => ex
116
+ raise HTTPStatus::InternalServerError, ex.message
117
+ end
118
+ res.body = body
119
+ end
120
+ alias do_POST do_GET
121
+
122
+ # :startdoc:
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # erbhandler.rb -- ERBHandler Class
4
+ #
5
+ # Author: IPR -- Internet Programming with Ruby -- writers
6
+ # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
7
+ # Copyright (c) 2002 Internet Programming with Ruby writers. All rights
8
+ # reserved.
9
+ #
10
+ # $IPR: erbhandler.rb,v 1.25 2003/02/24 19:25:31 gotoyuzo Exp $
11
+
12
+ require_relative 'abstract'
13
+
14
+ require 'erb'
15
+
16
+ module WEBrick
17
+ module HTTPServlet
18
+
19
+ ##
20
+ # ERBHandler evaluates an ERB file and returns the result. This handler
21
+ # is automatically used if there are .rhtml files in a directory served by
22
+ # the FileHandler.
23
+ #
24
+ # ERBHandler supports GET and POST methods.
25
+ #
26
+ # The ERB file is evaluated with the local variables +servlet_request+ and
27
+ # +servlet_response+ which are a WEBrick::HTTPRequest and
28
+ # WEBrick::HTTPResponse respectively.
29
+ #
30
+ # Example .rhtml file:
31
+ #
32
+ # Request to <%= servlet_request.request_uri %>
33
+ #
34
+ # Query params <%= servlet_request.query.inspect %>
35
+
36
+ class ERBHandler < AbstractServlet
37
+
38
+ ##
39
+ # Creates a new ERBHandler on +server+ that will evaluate and serve the
40
+ # ERB file +name+
41
+
42
+ def initialize(server, name)
43
+ super(server, name)
44
+ @script_filename = name
45
+ end
46
+
47
+ ##
48
+ # Handles GET requests
49
+
50
+ def do_GET(req, res)
51
+ unless defined?(ERB)
52
+ @logger.warn "#{self.class}: ERB not defined."
53
+ raise HTTPStatus::Forbidden, "ERBHandler cannot work."
54
+ end
55
+ begin
56
+ data = File.open(@script_filename, &:read)
57
+ res.body = evaluate(ERB.new(data), req, res)
58
+ res['content-type'] ||=
59
+ HTTPUtils::mime_type(@script_filename, @config[:MimeTypes])
60
+ rescue StandardError
61
+ raise
62
+ rescue Exception => ex
63
+ @logger.error(ex)
64
+ raise HTTPStatus::InternalServerError, ex.message
65
+ end
66
+ end
67
+
68
+ ##
69
+ # Handles POST requests
70
+
71
+ alias do_POST do_GET
72
+
73
+ private
74
+
75
+ ##
76
+ # Evaluates +erb+ providing +servlet_request+ and +servlet_response+ as
77
+ # local variables.
78
+
79
+ def evaluate(erb, servlet_request, servlet_response)
80
+ Module.new.module_eval{
81
+ servlet_request.meta_vars
82
+ servlet_request.query
83
+ erb.result(binding)
84
+ }
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,552 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # filehandler.rb -- FileHandler Module
4
+ #
5
+ # Author: IPR -- Internet Programming with Ruby -- writers
6
+ # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
7
+ # Copyright (c) 2003 Internet Programming with Ruby writers. All rights
8
+ # reserved.
9
+ #
10
+ # $IPR: filehandler.rb,v 1.44 2003/06/07 01:34:51 gotoyuzo Exp $
11
+
12
+ require 'time'
13
+
14
+ require_relative '../htmlutils'
15
+ require_relative '../httputils'
16
+ require_relative '../httpstatus'
17
+
18
+ module WEBrick
19
+ module HTTPServlet
20
+
21
+ ##
22
+ # Servlet for serving a single file. You probably want to use the
23
+ # FileHandler servlet instead as it handles directories and fancy indexes.
24
+ #
25
+ # Example:
26
+ #
27
+ # server.mount('/my_page.txt', WEBrick::HTTPServlet::DefaultFileHandler,
28
+ # '/path/to/my_page.txt')
29
+ #
30
+ # This servlet handles If-Modified-Since and Range requests.
31
+
32
+ class DefaultFileHandler < AbstractServlet
33
+
34
+ ##
35
+ # Creates a DefaultFileHandler instance for the file at +local_path+.
36
+
37
+ def initialize(server, local_path)
38
+ super(server, local_path)
39
+ @local_path = local_path
40
+ end
41
+
42
+ # :stopdoc:
43
+
44
+ def do_GET(req, res)
45
+ st = File::stat(@local_path)
46
+ mtime = st.mtime
47
+ res['etag'] = sprintf("%x-%x-%x", st.ino, st.size, st.mtime.to_i)
48
+
49
+ if not_modified?(req, res, mtime, res['etag'])
50
+ res.body = ''
51
+ raise HTTPStatus::NotModified
52
+ elsif req['range']
53
+ make_partial_content(req, res, @local_path, st.size)
54
+ raise HTTPStatus::PartialContent
55
+ else
56
+ mtype = HTTPUtils::mime_type(@local_path, @config[:MimeTypes])
57
+ res['content-type'] = mtype
58
+ res['content-length'] = st.size.to_s
59
+ res['last-modified'] = mtime.httpdate
60
+ res.body = File.open(@local_path, "rb")
61
+ end
62
+ end
63
+
64
+ def not_modified?(req, res, mtime, etag)
65
+ if ir = req['if-range']
66
+ begin
67
+ if Time.httpdate(ir) >= mtime
68
+ return true
69
+ end
70
+ rescue
71
+ if HTTPUtils::split_header_value(ir).member?(res['etag'])
72
+ return true
73
+ end
74
+ end
75
+ end
76
+
77
+ if (ims = req['if-modified-since']) && Time.parse(ims) >= mtime
78
+ return true
79
+ end
80
+
81
+ if (inm = req['if-none-match']) &&
82
+ HTTPUtils::split_header_value(inm).member?(res['etag'])
83
+ return true
84
+ end
85
+
86
+ return false
87
+ end
88
+
89
+ # returns a lambda for webrick/httpresponse.rb send_body_proc
90
+ def multipart_body(body, parts, boundary, mtype, filesize)
91
+ lambda do |socket|
92
+ begin
93
+ begin
94
+ first = parts.shift
95
+ last = parts.shift
96
+ socket.write(
97
+ "--#{boundary}#{CRLF}" \
98
+ "Content-Type: #{mtype}#{CRLF}" \
99
+ "Content-Range: bytes #{first}-#{last}/#{filesize}#{CRLF}" \
100
+ "#{CRLF}"
101
+ )
102
+
103
+ begin
104
+ IO.copy_stream(body, socket, last - first + 1, first)
105
+ rescue NotImplementedError
106
+ body.seek(first, IO::SEEK_SET)
107
+ IO.copy_stream(body, socket, last - first + 1)
108
+ end
109
+ socket.write(CRLF)
110
+ end while parts[0]
111
+ socket.write("--#{boundary}--#{CRLF}")
112
+ ensure
113
+ body.close
114
+ end
115
+ end
116
+ end
117
+
118
+ def make_partial_content(req, res, filename, filesize)
119
+ mtype = HTTPUtils::mime_type(filename, @config[:MimeTypes])
120
+ unless ranges = HTTPUtils::parse_range_header(req['range'])
121
+ raise HTTPStatus::BadRequest,
122
+ "Unrecognized range-spec: \"#{req['range']}\""
123
+ end
124
+ File.open(filename, "rb"){|io|
125
+ if ranges.size > 1
126
+ time = Time.now
127
+ boundary = "#{time.sec}_#{time.usec}_#{Process::pid}"
128
+ parts = []
129
+ ranges.each {|range|
130
+ prange = prepare_range(range, filesize)
131
+ next if prange[0] < 0
132
+ parts.concat(prange)
133
+ }
134
+ raise HTTPStatus::RequestRangeNotSatisfiable if parts.empty?
135
+ res["content-type"] = "multipart/byteranges; boundary=#{boundary}"
136
+ if req.http_version < '1.1'
137
+ res['connection'] = 'close'
138
+ else
139
+ res.chunked = true
140
+ end
141
+ res.body = multipart_body(io.dup, parts, boundary, mtype, filesize)
142
+ elsif range = ranges[0]
143
+ first, last = prepare_range(range, filesize)
144
+ raise HTTPStatus::RequestRangeNotSatisfiable if first < 0
145
+ res['content-type'] = mtype
146
+ res['content-range'] = "bytes #{first}-#{last}/#{filesize}"
147
+ res['content-length'] = (last - first + 1).to_s
148
+ res.body = io.dup
149
+ else
150
+ raise HTTPStatus::BadRequest
151
+ end
152
+ }
153
+ end
154
+
155
+ def prepare_range(range, filesize)
156
+ first = range.first < 0 ? filesize + range.first : range.first
157
+ return -1, -1 if first < 0 || first >= filesize
158
+ last = range.last < 0 ? filesize + range.last : range.last
159
+ last = filesize - 1 if last >= filesize
160
+ return first, last
161
+ end
162
+
163
+ # :startdoc:
164
+ end
165
+
166
+ ##
167
+ # Serves a directory including fancy indexing and a variety of other
168
+ # options.
169
+ #
170
+ # Example:
171
+ #
172
+ # server.mount('/assets', WEBrick::HTTPServlet::FileHandler,
173
+ # '/path/to/assets')
174
+
175
+ class FileHandler < AbstractServlet
176
+ HandlerTable = Hash.new # :nodoc:
177
+
178
+ ##
179
+ # Allow custom handling of requests for files with +suffix+ by class
180
+ # +handler+
181
+
182
+ def self.add_handler(suffix, handler)
183
+ HandlerTable[suffix] = handler
184
+ end
185
+
186
+ ##
187
+ # Remove custom handling of requests for files with +suffix+
188
+
189
+ def self.remove_handler(suffix)
190
+ HandlerTable.delete(suffix)
191
+ end
192
+
193
+ ##
194
+ # Creates a FileHandler servlet on +server+ that serves files starting
195
+ # at directory +root+
196
+ #
197
+ # +options+ may be a Hash containing keys from
198
+ # WEBrick::Config::FileHandler or +true+ or +false+.
199
+ #
200
+ # If +options+ is true or false then +:FancyIndexing+ is enabled or
201
+ # disabled respectively.
202
+
203
+ def initialize(server, root, options={}, default=Config::FileHandler)
204
+ @config = server.config
205
+ @logger = @config[:Logger]
206
+ @root = File.expand_path(root)
207
+ if options == true || options == false
208
+ options = { :FancyIndexing => options }
209
+ end
210
+ @options = default.dup.update(options)
211
+ end
212
+
213
+ # :stopdoc:
214
+
215
+ def set_filesystem_encoding(str)
216
+ enc = Encoding.find('filesystem')
217
+ if enc == Encoding::US_ASCII
218
+ str.b
219
+ else
220
+ str.dup.force_encoding(enc)
221
+ end
222
+ end
223
+
224
+ def service(req, res)
225
+ # if this class is mounted on "/" and /~username is requested.
226
+ # we're going to override path information before invoking service.
227
+ if defined?(Etc) && @options[:UserDir] && req.script_name.empty?
228
+ if %r|^(/~([^/]+))| =~ req.path_info
229
+ script_name, user = $1, $2
230
+ path_info = $'
231
+ begin
232
+ passwd = Etc::getpwnam(user)
233
+ @root = File::join(passwd.dir, @options[:UserDir])
234
+ req.script_name = script_name
235
+ req.path_info = path_info
236
+ rescue
237
+ @logger.debug "#{self.class}#do_GET: getpwnam(#{user}) failed"
238
+ end
239
+ end
240
+ end
241
+ prevent_directory_traversal(req, res)
242
+ super(req, res)
243
+ end
244
+
245
+ def do_GET(req, res)
246
+ unless exec_handler(req, res)
247
+ set_dir_list(req, res)
248
+ end
249
+ end
250
+
251
+ def do_POST(req, res)
252
+ unless exec_handler(req, res)
253
+ raise HTTPStatus::NotFound, "`#{req.path}' not found."
254
+ end
255
+ end
256
+
257
+ def do_OPTIONS(req, res)
258
+ unless exec_handler(req, res)
259
+ super(req, res)
260
+ end
261
+ end
262
+
263
+ # ToDo
264
+ # RFC2518: HTTP Extensions for Distributed Authoring -- WEBDAV
265
+ #
266
+ # PROPFIND PROPPATCH MKCOL DELETE PUT COPY MOVE
267
+ # LOCK UNLOCK
268
+
269
+ # RFC3253: Versioning Extensions to WebDAV
270
+ # (Web Distributed Authoring and Versioning)
271
+ #
272
+ # VERSION-CONTROL REPORT CHECKOUT CHECK_IN UNCHECKOUT
273
+ # MKWORKSPACE UPDATE LABEL MERGE ACTIVITY
274
+
275
+ private
276
+
277
+ def trailing_pathsep?(path)
278
+ # check for trailing path separator:
279
+ # File.dirname("/aaaa/bbbb/") #=> "/aaaa")
280
+ # File.dirname("/aaaa/bbbb/x") #=> "/aaaa/bbbb")
281
+ # File.dirname("/aaaa/bbbb") #=> "/aaaa")
282
+ # File.dirname("/aaaa/bbbbx") #=> "/aaaa")
283
+ return File.dirname(path) != File.dirname(path+"x")
284
+ end
285
+
286
+ def prevent_directory_traversal(req, res)
287
+ # Preventing directory traversal on Windows platforms;
288
+ # Backslashes (0x5c) in path_info are not interpreted as special
289
+ # character in URI notation. So the value of path_info should be
290
+ # normalize before accessing to the filesystem.
291
+
292
+ # dirty hack for filesystem encoding; in nature, File.expand_path
293
+ # should not be used for path normalization. [Bug #3345]
294
+ path = req.path_info.dup.force_encoding(Encoding.find("filesystem"))
295
+ if trailing_pathsep?(req.path_info)
296
+ # File.expand_path removes the trailing path separator.
297
+ # Adding a character is a workaround to save it.
298
+ # File.expand_path("/aaa/") #=> "/aaa"
299
+ # File.expand_path("/aaa/" + "x") #=> "/aaa/x"
300
+ expanded = File.expand_path(path + "x")
301
+ expanded.chop! # remove trailing "x"
302
+ else
303
+ expanded = File.expand_path(path)
304
+ end
305
+ expanded.force_encoding(req.path_info.encoding)
306
+ req.path_info = expanded
307
+ end
308
+
309
+ def exec_handler(req, res)
310
+ raise HTTPStatus::NotFound, "`#{req.path}' not found." unless @root
311
+ if set_filename(req, res)
312
+ handler = get_handler(req, res)
313
+ call_callback(:HandlerCallback, req, res)
314
+ h = handler.get_instance(@config, res.filename)
315
+ h.service(req, res)
316
+ return true
317
+ end
318
+ call_callback(:HandlerCallback, req, res)
319
+ return false
320
+ end
321
+
322
+ def get_handler(req, res)
323
+ suffix1 = (/\.(\w+)\z/ =~ res.filename) && $1.downcase
324
+ if /\.(\w+)\.([\w\-]+)\z/ =~ res.filename
325
+ if @options[:AcceptableLanguages].include?($2.downcase)
326
+ suffix2 = $1.downcase
327
+ end
328
+ end
329
+ handler_table = @options[:HandlerTable]
330
+ return handler_table[suffix1] || handler_table[suffix2] ||
331
+ HandlerTable[suffix1] || HandlerTable[suffix2] ||
332
+ DefaultFileHandler
333
+ end
334
+
335
+ def set_filename(req, res)
336
+ res.filename = @root
337
+ path_info = req.path_info.scan(%r|/[^/]*|)
338
+
339
+ path_info.unshift("") # dummy for checking @root dir
340
+ while base = path_info.first
341
+ base = set_filesystem_encoding(base)
342
+ break if base == "/"
343
+ break unless File.directory?(File.expand_path(res.filename + base))
344
+ shift_path_info(req, res, path_info)
345
+ call_callback(:DirectoryCallback, req, res)
346
+ end
347
+
348
+ if base = path_info.first
349
+ base = set_filesystem_encoding(base)
350
+ if base == "/"
351
+ if file = search_index_file(req, res)
352
+ shift_path_info(req, res, path_info, file)
353
+ call_callback(:FileCallback, req, res)
354
+ return true
355
+ end
356
+ shift_path_info(req, res, path_info)
357
+ elsif file = search_file(req, res, base)
358
+ shift_path_info(req, res, path_info, file)
359
+ call_callback(:FileCallback, req, res)
360
+ return true
361
+ else
362
+ raise HTTPStatus::NotFound, "`#{req.path}' not found."
363
+ end
364
+ end
365
+
366
+ return false
367
+ end
368
+
369
+ def check_filename(req, res, name)
370
+ if nondisclosure_name?(name) || windows_ambiguous_name?(name)
371
+ @logger.warn("the request refers nondisclosure name `#{name}'.")
372
+ raise HTTPStatus::NotFound, "`#{req.path}' not found."
373
+ end
374
+ end
375
+
376
+ def shift_path_info(req, res, path_info, base=nil)
377
+ tmp = path_info.shift
378
+ base = base || set_filesystem_encoding(tmp)
379
+ req.path_info = path_info.join
380
+ req.script_name << base
381
+ res.filename = File.expand_path(res.filename + base)
382
+ check_filename(req, res, File.basename(res.filename))
383
+ end
384
+
385
+ def search_index_file(req, res)
386
+ @config[:DirectoryIndex].each{|index|
387
+ if file = search_file(req, res, "/"+index)
388
+ return file
389
+ end
390
+ }
391
+ return nil
392
+ end
393
+
394
+ def search_file(req, res, basename)
395
+ langs = @options[:AcceptableLanguages]
396
+ path = res.filename + basename
397
+ if File.file?(path)
398
+ return basename
399
+ elsif langs.size > 0
400
+ req.accept_language.each{|lang|
401
+ path_with_lang = path + ".#{lang}"
402
+ if langs.member?(lang) && File.file?(path_with_lang)
403
+ return basename + ".#{lang}"
404
+ end
405
+ }
406
+ (langs - req.accept_language).each{|lang|
407
+ path_with_lang = path + ".#{lang}"
408
+ if File.file?(path_with_lang)
409
+ return basename + ".#{lang}"
410
+ end
411
+ }
412
+ end
413
+ return nil
414
+ end
415
+
416
+ def call_callback(callback_name, req, res)
417
+ if cb = @options[callback_name]
418
+ cb.call(req, res)
419
+ end
420
+ end
421
+
422
+ def windows_ambiguous_name?(name)
423
+ return true if /[. ]+\z/ =~ name
424
+ return true if /::\$DATA\z/ =~ name
425
+ return false
426
+ end
427
+
428
+ def nondisclosure_name?(name)
429
+ @options[:NondisclosureName].each{|pattern|
430
+ if File.fnmatch(pattern, name, File::FNM_CASEFOLD)
431
+ return true
432
+ end
433
+ }
434
+ return false
435
+ end
436
+
437
+ def set_dir_list(req, res)
438
+ redirect_to_directory_uri(req, res)
439
+ unless @options[:FancyIndexing]
440
+ raise HTTPStatus::Forbidden, "no access permission to `#{req.path}'"
441
+ end
442
+ local_path = res.filename
443
+ list = Dir::entries(local_path).collect{|name|
444
+ next if name == "." || name == ".."
445
+ next if nondisclosure_name?(name)
446
+ next if windows_ambiguous_name?(name)
447
+ st = (File::stat(File.join(local_path, name)) rescue nil)
448
+ if st.nil?
449
+ [ name, nil, -1 ]
450
+ elsif st.directory?
451
+ [ name + "/", st.mtime, -1 ]
452
+ else
453
+ [ name, st.mtime, st.size ]
454
+ end
455
+ }
456
+ list.compact!
457
+
458
+ query = req.query
459
+
460
+ d0 = nil
461
+ idx = nil
462
+ %w[N M S].each_with_index do |q, i|
463
+ if d = query.delete(q)
464
+ idx ||= i
465
+ d0 ||= d
466
+ end
467
+ end
468
+ d0 ||= "A"
469
+ idx ||= 0
470
+ d1 = (d0 == "A") ? "D" : "A"
471
+
472
+ if d0 == "A"
473
+ list.sort!{|a,b| a[idx] <=> b[idx] }
474
+ else
475
+ list.sort!{|a,b| b[idx] <=> a[idx] }
476
+ end
477
+
478
+ namewidth = query["NameWidth"]
479
+ if namewidth == "*"
480
+ namewidth = nil
481
+ elsif !namewidth or (namewidth = namewidth.to_i) < 2
482
+ namewidth = 25
483
+ end
484
+ query = query.inject('') {|s, (k, v)| s << '&' << HTMLUtils::escape("#{k}=#{v}")}.dup
485
+
486
+ type = +"text/html"
487
+ case enc = Encoding.find('filesystem')
488
+ when Encoding::US_ASCII, Encoding::ASCII_8BIT
489
+ else
490
+ type << "; charset=\"#{enc.name}\""
491
+ end
492
+ res['content-type'] = type
493
+
494
+ title = "Index of #{HTMLUtils::escape(req.path)}"
495
+ res.body = +<<-_end_of_html_
496
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
497
+ <HTML>
498
+ <HEAD>
499
+ <TITLE>#{title}</TITLE>
500
+ <style type="text/css">
501
+ <!--
502
+ .name, .mtime { text-align: left; }
503
+ .size { text-align: right; }
504
+ td { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
505
+ table { border-collapse: collapse; }
506
+ tr th { border-bottom: 2px groove; }
507
+ //-->
508
+ </style>
509
+ </HEAD>
510
+ <BODY>
511
+ <H1>#{title}</H1>
512
+ _end_of_html_
513
+
514
+ res.body << "<TABLE width=\"100%\"><THEAD><TR>\n"
515
+ res.body << "<TH class=\"name\"><A HREF=\"?N=#{d1}#{query}\">Name</A></TH>"
516
+ res.body << "<TH class=\"mtime\"><A HREF=\"?M=#{d1}#{query}\">Last modified</A></TH>"
517
+ res.body << "<TH class=\"size\"><A HREF=\"?S=#{d1}#{query}\">Size</A></TH>\n"
518
+ res.body << "</TR></THEAD>\n"
519
+ res.body << "<TBODY>\n"
520
+
521
+ query.sub!(/\A&/, '?')
522
+ list.unshift [ "..", File::mtime(local_path+"/.."), -1 ]
523
+ list.each{ |name, time, size|
524
+ if name == ".."
525
+ dname = "Parent Directory"
526
+ elsif namewidth and name.size > namewidth
527
+ dname = name[0...(namewidth - 2)] << '..'
528
+ else
529
+ dname = name
530
+ end
531
+ s = +"<TR><TD class=\"name\"><A HREF=\"#{HTTPUtils::escape(name)}#{query if name.end_with?('/')}\">#{HTMLUtils::escape(dname)}</A></TD>"
532
+ s << "<TD class=\"mtime\">" << (time ? time.strftime("%Y/%m/%d %H:%M") : "") << "</TD>"
533
+ s << "<TD class=\"size\">" << (size >= 0 ? size.to_s : "-") << "</TD></TR>\n"
534
+ res.body << s
535
+ }
536
+ res.body << "</TBODY></TABLE>"
537
+ res.body << "<HR>"
538
+
539
+ res.body << <<-_end_of_html_
540
+ <ADDRESS>
541
+ #{HTMLUtils::escape(@config[:ServerSoftware])}<BR>
542
+ at #{req.host}:#{req.port}
543
+ </ADDRESS>
544
+ </BODY>
545
+ </HTML>
546
+ _end_of_html_
547
+ end
548
+
549
+ # :startdoc:
550
+ end
551
+ end
552
+ end