shopify-cli 1.3.0 → 1.6.0

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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +2 -2
  3. data/.github/CONTRIBUTING.md +9 -1
  4. data/.github/PULL_REQUEST_TEMPLATE.md +10 -1
  5. data/.github/workflows/release.yml +61 -0
  6. data/.github/workflows/triage.yml +22 -0
  7. data/.gitignore +0 -1
  8. data/.rubocop.yml +61 -8
  9. data/.rubocop_todo.yml +11 -0
  10. data/.travis.yml +1 -0
  11. data/CHANGELOG.md +30 -0
  12. data/Gemfile +3 -2
  13. data/Gemfile.lock +39 -37
  14. data/README.md +39 -7
  15. data/RELEASING.md +19 -29
  16. data/Rakefile +2 -0
  17. data/dev.yml +2 -2
  18. data/docs/_config.yml +1 -18
  19. data/docs/app/node/commands/index.md +2 -80
  20. data/docs/app/node/index.md +2 -33
  21. data/docs/app/rails/commands/index.md +2 -78
  22. data/docs/app/rails/index.md +2 -34
  23. data/docs/core/index.md +2 -84
  24. data/docs/getting-started/index.md +2 -25
  25. data/docs/getting-started/install/index.md +1 -118
  26. data/docs/getting-started/migrate/index.md +2 -94
  27. data/docs/getting-started/uninstall/index.md +2 -35
  28. data/docs/getting-started/upgrade/index.md +2 -39
  29. data/docs/help/start-app/index.md +2 -4
  30. data/docs/index.md +2 -24
  31. data/install.sh +1 -1
  32. data/lib/project_types/extension/cli.rb +21 -11
  33. data/lib/project_types/extension/commands/extension_command.rb +2 -2
  34. data/lib/project_types/extension/features/argo.rb +117 -0
  35. data/lib/project_types/extension/forms/create.rb +2 -2
  36. data/lib/project_types/extension/models/specification.rb +35 -0
  37. data/lib/project_types/extension/models/specification_handlers/checkout_post_purchase.rb +19 -0
  38. data/lib/project_types/extension/models/specification_handlers/default.rb +67 -0
  39. data/lib/project_types/extension/models/specifications.rb +77 -0
  40. data/lib/project_types/extension/tasks/configure_features.rb +52 -0
  41. data/lib/project_types/extension/tasks/fetch_specifications.rb +38 -0
  42. data/lib/project_types/node/cli.rb +4 -1
  43. data/lib/project_types/node/commands/connect.rb +15 -0
  44. data/lib/project_types/node/commands/create.rb +10 -4
  45. data/lib/project_types/node/commands/generate.rb +2 -11
  46. data/lib/project_types/node/messages/messages.rb +16 -50
  47. data/lib/project_types/rails/cli.rb +4 -1
  48. data/lib/project_types/rails/commands/connect.rb +15 -0
  49. data/lib/project_types/rails/commands/create.rb +15 -12
  50. data/lib/project_types/rails/forms/create.rb +1 -1
  51. data/lib/project_types/rails/gem.rb +1 -1
  52. data/lib/project_types/rails/messages/messages.rb +8 -5
  53. data/lib/project_types/script/cli.rb +9 -5
  54. data/lib/project_types/script/commands/create.rb +6 -4
  55. data/lib/project_types/script/commands/enable.rb +12 -4
  56. data/lib/project_types/script/commands/push.rb +5 -13
  57. data/lib/project_types/script/config/extension_points.yml +17 -12
  58. data/lib/project_types/script/errors.rb +21 -0
  59. data/lib/project_types/script/forms/create.rb +26 -2
  60. data/lib/project_types/script/graphql/app_script_update_or_create.graphql +10 -1
  61. data/lib/project_types/script/layers/application/build_script.rb +18 -17
  62. data/lib/project_types/script/layers/application/create_script.rb +12 -10
  63. data/lib/project_types/script/layers/application/extension_points.rb +24 -0
  64. data/lib/project_types/script/layers/application/push_script.rb +18 -16
  65. data/lib/project_types/script/layers/domain/errors.rb +7 -0
  66. data/lib/project_types/script/layers/domain/extension_point.rb +62 -7
  67. data/lib/project_types/script/layers/domain/metadata.rb +55 -0
  68. data/lib/project_types/script/layers/domain/push_package.rb +25 -6
  69. data/lib/project_types/script/layers/infrastructure/assemblyscript_project_creator.rb +17 -52
  70. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +42 -11
  71. data/lib/project_types/script/layers/infrastructure/errors.rb +16 -0
  72. data/lib/project_types/script/layers/infrastructure/extension_point_repository.rb +10 -4
  73. data/lib/project_types/script/layers/infrastructure/project_creator.rb +2 -1
  74. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +25 -13
  75. data/lib/project_types/script/layers/infrastructure/rust_project_creator.rb +72 -0
  76. data/lib/project_types/script/layers/infrastructure/rust_task_runner.rb +59 -0
  77. data/lib/project_types/script/layers/infrastructure/script_service.rb +9 -1
  78. data/lib/project_types/script/layers/infrastructure/task_runner.rb +4 -3
  79. data/lib/project_types/script/messages/messages.rb +55 -4
  80. data/lib/project_types/script/script_project.rb +25 -16
  81. data/lib/project_types/script/ui/error_handler.rb +59 -1
  82. data/lib/project_types/theme/cli.rb +40 -0
  83. data/lib/project_types/theme/commands/connect.rb +54 -0
  84. data/lib/project_types/theme/commands/create.rb +48 -0
  85. data/lib/project_types/theme/commands/deploy.rb +38 -0
  86. data/lib/project_types/theme/commands/generate.rb +20 -0
  87. data/lib/project_types/theme/commands/generate/env.rb +79 -0
  88. data/lib/project_types/theme/commands/push.rb +55 -0
  89. data/lib/project_types/theme/commands/serve.rb +31 -0
  90. data/lib/project_types/theme/forms/connect.rb +34 -0
  91. data/lib/project_types/theme/forms/create.rb +22 -0
  92. data/lib/project_types/theme/messages/messages.rb +147 -0
  93. data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +78 -0
  94. data/lib/project_types/theme/themekit.rb +113 -0
  95. data/lib/shopify-cli/admin_api.rb +42 -2
  96. data/lib/shopify-cli/api.rb +34 -33
  97. data/lib/shopify-cli/commands/config.rb +24 -0
  98. data/lib/shopify-cli/commands/connect.rb +32 -15
  99. data/lib/shopify-cli/commands/system.rb +10 -1
  100. data/lib/shopify-cli/context.rb +23 -2
  101. data/lib/shopify-cli/core/entry_point.rb +1 -1
  102. data/lib/shopify-cli/core/monorail.rb +6 -4
  103. data/lib/shopify-cli/feature.rb +0 -2
  104. data/lib/shopify-cli/http_request.rb +27 -0
  105. data/lib/shopify-cli/js_deps.rb +1 -1
  106. data/lib/shopify-cli/messages/messages.rb +31 -7
  107. data/lib/shopify-cli/method_object.rb +104 -0
  108. data/lib/shopify-cli/partners_api.rb +25 -3
  109. data/lib/shopify-cli/process_supervision.rb +1 -1
  110. data/lib/shopify-cli/project.rb +12 -8
  111. data/lib/shopify-cli/project_type.rb +18 -2
  112. data/lib/shopify-cli/resolve_constant.rb +25 -0
  113. data/lib/shopify-cli/result.rb +432 -0
  114. data/lib/shopify-cli/shopifolk.rb +87 -0
  115. data/lib/shopify-cli/task.rb +8 -0
  116. data/lib/shopify-cli/tasks/create_api_client.rb +13 -2
  117. data/lib/shopify-cli/tasks/ensure_env.rb +3 -0
  118. data/lib/shopify-cli/tasks/select_org_and_shop.rb +10 -5
  119. data/lib/shopify-cli/tunnel.rb +8 -2
  120. data/lib/shopify-cli/version.rb +1 -1
  121. data/lib/shopify_cli.rb +5 -1
  122. data/shopify.fish +1 -1
  123. data/shopify.sh +1 -1
  124. data/vendor/deps/cli-kit/REVISION +1 -1
  125. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +2 -2
  126. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +3 -3
  127. data/vendor/deps/cli-ui/REVISION +1 -1
  128. data/vendor/deps/cli-ui/lib/cli/ui.rb +26 -22
  129. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +4 -6
  130. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +3 -3
  131. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +8 -9
  132. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +1 -1
  133. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +1 -0
  134. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +15 -3
  135. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +4 -11
  136. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +3 -5
  137. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +10 -10
  138. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +1 -1
  139. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +56 -0
  140. data/vendor/deps/webrick/.gitignore +9 -0
  141. data/vendor/deps/webrick/Gemfile +3 -0
  142. data/vendor/deps/webrick/LICENSE.txt +22 -0
  143. data/vendor/deps/webrick/README.md +61 -0
  144. data/vendor/deps/webrick/Rakefile +10 -0
  145. data/vendor/deps/webrick/lib/webrick.rb +232 -0
  146. data/vendor/deps/webrick/lib/webrick/accesslog.rb +157 -0
  147. data/vendor/deps/webrick/lib/webrick/cgi.rb +313 -0
  148. data/vendor/deps/webrick/lib/webrick/compat.rb +36 -0
  149. data/vendor/deps/webrick/lib/webrick/config.rb +158 -0
  150. data/vendor/deps/webrick/lib/webrick/cookie.rb +172 -0
  151. data/vendor/deps/webrick/lib/webrick/htmlutils.rb +30 -0
  152. data/vendor/deps/webrick/lib/webrick/httpauth.rb +96 -0
  153. data/vendor/deps/webrick/lib/webrick/httpauth/authenticator.rb +117 -0
  154. data/vendor/deps/webrick/lib/webrick/httpauth/basicauth.rb +116 -0
  155. data/vendor/deps/webrick/lib/webrick/httpauth/digestauth.rb +395 -0
  156. data/vendor/deps/webrick/lib/webrick/httpauth/htdigest.rb +132 -0
  157. data/vendor/deps/webrick/lib/webrick/httpauth/htgroup.rb +97 -0
  158. data/vendor/deps/webrick/lib/webrick/httpauth/htpasswd.rb +158 -0
  159. data/vendor/deps/webrick/lib/webrick/httpauth/userdb.rb +53 -0
  160. data/vendor/deps/webrick/lib/webrick/httpproxy.rb +354 -0
  161. data/vendor/deps/webrick/lib/webrick/httprequest.rb +636 -0
  162. data/vendor/deps/webrick/lib/webrick/httpresponse.rb +564 -0
  163. data/vendor/deps/webrick/lib/webrick/https.rb +152 -0
  164. data/vendor/deps/webrick/lib/webrick/httpserver.rb +294 -0
  165. data/vendor/deps/webrick/lib/webrick/httpservlet.rb +23 -0
  166. data/vendor/deps/webrick/lib/webrick/httpservlet/abstract.rb +152 -0
  167. data/vendor/deps/webrick/lib/webrick/httpservlet/cgi_runner.rb +47 -0
  168. data/vendor/deps/webrick/lib/webrick/httpservlet/cgihandler.rb +126 -0
  169. data/vendor/deps/webrick/lib/webrick/httpservlet/erbhandler.rb +88 -0
  170. data/vendor/deps/webrick/lib/webrick/httpservlet/filehandler.rb +552 -0
  171. data/vendor/deps/webrick/lib/webrick/httpservlet/prochandler.rb +47 -0
  172. data/vendor/deps/webrick/lib/webrick/httpstatus.rb +194 -0
  173. data/vendor/deps/webrick/lib/webrick/httputils.rb +512 -0
  174. data/vendor/deps/webrick/lib/webrick/httpversion.rb +76 -0
  175. data/vendor/deps/webrick/lib/webrick/log.rb +156 -0
  176. data/vendor/deps/webrick/lib/webrick/server.rb +381 -0
  177. data/vendor/deps/webrick/lib/webrick/ssl.rb +215 -0
  178. data/vendor/deps/webrick/lib/webrick/utils.rb +265 -0
  179. data/vendor/deps/webrick/lib/webrick/version.rb +18 -0
  180. data/vendor/deps/webrick/webrick.gemspec +74 -0
  181. metadata +77 -27
  182. data/docs/Gemfile +0 -5
  183. data/docs/Gemfile.lock +0 -258
  184. data/docs/_data/nav.yml +0 -35
  185. data/docs/_includes/footer.html +0 -15
  186. data/docs/_includes/head.html +0 -19
  187. data/docs/_includes/sidebar_nav.html +0 -22
  188. data/docs/_includes/toc.html +0 -112
  189. data/docs/_layouts/default.html +0 -79
  190. data/docs/css/docs.css +0 -157
  191. data/docs/images/header.png +0 -0
  192. data/docs/installing-ruby.md +0 -28
  193. data/lib/project_types/extension/features/argo/admin.rb +0 -20
  194. data/lib/project_types/extension/features/argo/base.rb +0 -129
  195. data/lib/project_types/extension/features/argo/checkout.rb +0 -20
  196. data/lib/project_types/extension/models/type.rb +0 -81
  197. data/lib/project_types/extension/models/types/checkout_post_purchase.rb +0 -23
  198. data/lib/project_types/extension/models/types/product_subscription.rb +0 -24
  199. data/lib/project_types/node/commands/generate/billing.rb +0 -39
  200. data/lib/project_types/node/commands/generate/page.rb +0 -59
  201. data/lib/project_types/node/commands/generate/webhook.rb +0 -37
  202. data/lib/project_types/script/layers/domain/script.rb +0 -18
  203. data/lib/project_types/script/layers/infrastructure/assemblyscript_tsconfig.rb +0 -38
  204. data/lib/project_types/script/layers/infrastructure/script_repository.rb +0 -59
  205. data/lib/project_types/script/templates/ts/as-pect.config.js +0 -27
  206. 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