utopia 1.3.2 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 27fc0800c0d44763978e981de97541dbf2d0d2ef
4
- data.tar.gz: 346b6f43e6e056667c8ab6c30b8b185bfb1cdd58
3
+ metadata.gz: 0fb046791da67c397e853162649620dc2b530d3e
4
+ data.tar.gz: 390a488937297cec03a267206d4a096d4c74a497
5
5
  SHA512:
6
- metadata.gz: e9eedc27a561f991f9d3e637371d51770f31eeeb42d4a46a85f4461a2211b8985b5f86af4f3e5da8115ce45f6b13bfdd12fdc2eafc4b0fa9083dac44d1b6f0af
7
- data.tar.gz: d9feff14bb7a57d3d0df656537751abe5e1fb9b1608f49e870a8a4e2f17650e80e41be6601b8411395b60b33faa9b980f75d3b3862455e9c77850d2a07886a4c
6
+ metadata.gz: 9bd0a3820ebe9422484bb3c1bf048c99b7f7d0e9907eb0cdfccf292ecb6c55e8927cbdeacf87f96b4ad759b7047290f7622639438997d2fa53c96ced3fd24e26
7
+ data.tar.gz: 9cca58be165173f26f72cee8bde2b7b5b1052018c2a1fff7946476228cc09c63eb48e4d51a85da50893e59894b33c504afede627b5cfeb51dd15c466af3b94fa
data/README.md CHANGED
@@ -79,7 +79,7 @@ Then, Nginx is configured like so:
79
79
  Utopia builds on top of Rack with the following middleware:
80
80
 
81
81
  - `Utopia::Static`: Serve static files efficiently.
82
- - `Utopia::Redirector`: Redirect URL patterns and status codes.
82
+ - `Utopia::Redirection`: Redirect URL patterns and status codes.
83
83
  - `Utopia::Localization`: Non-intrusive localization of resources.
84
84
  - `Utopia::Controller`: Dynamic behaviour with recursive execution.
85
85
  - `Utopia::Content`: XML-style template engine with powerful tag behaviours.
@@ -89,9 +89,9 @@ Utopia builds on top of Rack with the following middleware:
89
89
 
90
90
  This middleware serves static files using the `mime-types` library. By default, it works with `Rack::Sendfile` and `Rack::Cache` and supports `ETag` based caching.
91
91
 
92
- ### Redirector
92
+ ### Redirection
93
93
 
94
- A flexible high level URI rewriting system which includes support for string mappings, regular expressions and status codes (e.g. 404 errors).
94
+ A set of flexible URI rewriting middleware which includes support for string mappings, regular expressions and status codes (e.g. 404 errors).
95
95
 
96
96
  ### Localization
97
97
 
@@ -22,10 +22,9 @@ require_relative 'utopia/version'
22
22
 
23
23
  require_relative 'utopia/content'
24
24
  require_relative 'utopia/controller'
25
- require_relative 'utopia/exception_handler'
26
25
  require_relative 'utopia/localization'
27
- require_relative 'utopia/mail_exceptions'
28
- require_relative 'utopia/redirector'
26
+ require_relative 'utopia/exceptions'
27
+ require_relative 'utopia/redirection'
29
28
  require_relative 'utopia/static'
30
29
 
31
30
  require_relative 'utopia/tags/deferred'
@@ -0,0 +1,27 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'exceptions/handler'
22
+ require_relative 'exceptions/mailer'
23
+
24
+ module Utopia
25
+ module Exceptions
26
+ end
27
+ end
@@ -0,0 +1,99 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Utopia
22
+ module Exceptions
23
+ # Catches exceptions and performs an internal redirect.
24
+ class Handler
25
+ def initialize(app, location = '/errors/exception')
26
+ @app = app
27
+
28
+ @location = location
29
+ end
30
+
31
+ def freeze
32
+ @location.freeze
33
+
34
+ super
35
+ end
36
+
37
+ # Generate a very simple fatal error response. This function should be unlikely to fail. Additionally, it generates a lowest common denominator response which should be suitable as a response to any kind of request. Ideally, this response is also not good or useful for any kind of higher level browser or API client, as this is not a normal error path but one that represents broken behaviour.
38
+ def fatal_error(env, exception)
39
+ body = StringIO.new
40
+
41
+ write_exception_to_stream(body, env, exception)
42
+ body.rewind
43
+
44
+ return [500, {HTTP::CONTENT_TYPE => "text/plain"}, body]
45
+ end
46
+
47
+ def log_exception(env, exception)
48
+ # An error has occurred, log it:
49
+ output = env['rack.errors'] || $stderr
50
+ write_exception_to_stream(output, env, exception, true)
51
+ end
52
+
53
+ def call(env)
54
+ begin
55
+ return @app.call(env)
56
+ rescue Exception => exception
57
+ log_exception(env, exception)
58
+
59
+ # If the error occurred while accessing the error handler, we finish with a fatal error:
60
+ if env[Rack::PATH_INFO] == @location
61
+ return fatal_error(env, exception)
62
+ else
63
+ begin
64
+ # We do an internal redirection to the error location:
65
+ error_request = env.merge(Rack::PATH_INFO => @location, Rack::REQUEST_METHOD => Rack::GET)
66
+ error_response = @app.call(error_request)
67
+
68
+ return [500, error_response[1], error_response[2]]
69
+ rescue Exception
70
+ # If redirection fails, we also finish with a fatal error:
71
+ return fatal_error(env, exception)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ private def write_exception_to_stream(stream, env, exception, include_backtrace = false)
78
+ buffer = []
79
+
80
+ buffer << "While requesting resource #{env[Rack::PATH_INFO].inspect}, a fatal error occurred:"
81
+
82
+ while exception != nil
83
+ buffer << "\t#{exception.class.name}: #{exception.to_s}"
84
+
85
+ if include_backtrace
86
+ exception.backtrace.each do |line|
87
+ buffer << "\t\t#{line}"
88
+ end
89
+ end
90
+
91
+ exception = exception.cause
92
+ end
93
+
94
+ # We do this in one go so that lines don't get mixed up.
95
+ stream.puts buffer.join("\n")
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,138 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'net/smtp'
22
+ require 'mail'
23
+
24
+ module Utopia
25
+ module Exceptions
26
+ # Catches all exceptions raised from the app it wraps and sends a useful email with the exception, stacktrace, and contents of the environment.
27
+ class Mailer
28
+ # A basic local non-authenticated SMTP server.
29
+ LOCAL_SMTP = [:smtp, {
30
+ :address => "localhost",
31
+ :port => 25,
32
+ :enable_starttls_auto => false
33
+ }]
34
+
35
+ def initialize(app, config = {})
36
+ @app = app
37
+
38
+ @to = config[:to] || "postmaster"
39
+ @from = config.fetch(:from) {(ENV['USER'] || 'rack') + "@localhost"}
40
+ @subject = config[:subject] || '%{exception} [PID %{pid} : %{cwd}]'
41
+ @delivery_method = config.fetch(:delivery_method, LOCAL_SMTP)
42
+
43
+ @dump_environment = config.fetch(:dump_environment, false)
44
+ end
45
+
46
+ def call(env)
47
+ begin
48
+ return @app.call(env)
49
+ rescue => exception
50
+ send_notification exception, env
51
+
52
+ raise
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ REQUEST_KEYS = [:ip, :referrer, :path, :user_agent]
59
+
60
+ def generate_body(exception, env)
61
+ io = StringIO.new
62
+
63
+ # Dump out useful rack environment variables:
64
+ request = Rack::Request.new(env)
65
+
66
+ io.puts "#{request.request_method} #{request.url}"
67
+
68
+ io.puts
69
+
70
+ REQUEST_KEYS.each do |key|
71
+ value = request.send(key)
72
+ io.puts "request.#{key}: #{value.inspect}"
73
+ end
74
+
75
+ request.params.each do |key, value|
76
+ io.puts "request.params.#{key}: #{value.inspect}"
77
+ end
78
+
79
+ io.puts
80
+
81
+ io.puts "#{exception.class.name}: #{exception.to_s}"
82
+
83
+ if exception.respond_to?(:backtrace)
84
+ io.puts exception.backtrace
85
+ else
86
+ io.puts exception.to_s
87
+ end
88
+
89
+ return io.string
90
+ end
91
+
92
+ def generate_mail(exception, env)
93
+ attributes = {
94
+ exception: exception.class.name,
95
+ pid: $$,
96
+ cwd: Dir.getwd,
97
+ }
98
+
99
+ mail = Mail.new(
100
+ :from => @from,
101
+ :to => @to,
102
+ :subject => @subject % attributes
103
+ )
104
+
105
+ mail.text_part = Mail::Part.new
106
+ mail.text_part.body = generate_body(exception, env)
107
+
108
+ if body = extract_body(env) and body.size > 0
109
+ mail.attachments['body.bin'] = body
110
+ end
111
+
112
+ if @dump_environment
113
+ mail.attachments['environment.yaml'] = YAML::dump(env)
114
+ end
115
+
116
+ return mail
117
+ end
118
+
119
+ def send_notification(exception, env)
120
+ mail = generate_mail(exception, env)
121
+
122
+ mail.delivery_method(*@delivery_method) if @delivery_method
123
+
124
+ mail.deliver
125
+ rescue => mail_exception
126
+ $stderr.puts mail_exception.to_s
127
+ $stderr.puts mail_exception.backtrace
128
+ end
129
+
130
+ def extract_body(env)
131
+ if io = env['rack.input']
132
+ io.rewind if io.respond_to?(:rewind)
133
+ io.read
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,176 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'middleware'
22
+
23
+ module Utopia
24
+ module Redirection
25
+ class RequestFailure < StandardError
26
+ def initialize(resource_path, resource_status, error_path, error_status)
27
+ @resource_path = resource_path
28
+ @resource_status = resource_status
29
+
30
+ @error_path = error_path
31
+ @error_status = error_status
32
+
33
+ super "Requested resource #{@resource_path} resulted in a #{@resource_status} error. Requested error handler #{@error_path} resulted in a #{@error_status} error."
34
+ end
35
+ end
36
+
37
+ class Errors
38
+ # Maps an error code to a given string
39
+ def initialize(app, codes = {})
40
+ @codes = codes
41
+
42
+ @app = app
43
+ end
44
+
45
+ def freeze
46
+ @codes.freeze
47
+
48
+ super
49
+ end
50
+
51
+ def call(env)
52
+ response = @app.call(env)
53
+
54
+ if response[0] >= 400 and location = @codes[response[0]]
55
+ error_request = env.merge(Rack::PATH_INFO => location, Rack::REQUEST_METHOD => Rack::GET)
56
+ error_response = @app.call(error_request)
57
+
58
+ if error_response[0] >= 400
59
+ raise RequestFailure.new(env[Rack::PATH_INFO], response[0], location, error_response[0])
60
+ else
61
+ # Feed the error code back with the error document:
62
+ error_response[0] = response[0]
63
+ return error_response
64
+ end
65
+ else
66
+ return response
67
+ end
68
+ end
69
+ end
70
+
71
+ class FileNotFound < Errors
72
+ def initialize(uri = '/errors/file-not-found')
73
+ super(404 => uri)
74
+ end
75
+ end
76
+
77
+ # We cache 301 redirects for 24 hours.
78
+ DEFAULT_MAX_AGE = 3600*24
79
+
80
+ class Redirection
81
+ def initialize(app, status: 307, max_age: DEFAULT_MAX_AGE)
82
+ @app = app
83
+ @status = status
84
+ @max_age = max_age
85
+ end
86
+
87
+ def freeze
88
+ @status.freeze
89
+ @max_age.freeze
90
+
91
+ super
92
+ end
93
+
94
+ attr :status
95
+ attr :max_age
96
+
97
+ def cache_control
98
+ # http://jacquesmattheij.com/301-redirects-a-dangerous-one-way-street
99
+ "max-age=#{self.max_age}"
100
+ end
101
+
102
+ def headers(location)
103
+ {HTTP::LOCATION => location, HTTP::CACHE_CONTROL => self.cache_control}
104
+ end
105
+
106
+ def redirect(location)
107
+ return [self.status, self.headers(location), []]
108
+ end
109
+
110
+ def [] path
111
+ false
112
+ end
113
+
114
+ def call(env)
115
+ path = env[Rack::PATH_INFO]
116
+
117
+ if redirection = self[path]
118
+ return redirection
119
+ end
120
+
121
+ return @app.call(env)
122
+ end
123
+ end
124
+
125
+ class DirectoryIndex < Redirection
126
+ def initialize(app, index = 'index')
127
+ @app = app
128
+ @index = 'index'
129
+
130
+ super(app)
131
+ end
132
+
133
+ def [] path
134
+ if path.end_with?('/')
135
+ return redirect(path + @index)
136
+ end
137
+ end
138
+ end
139
+
140
+ class Rewrite < Redirection
141
+ def initialize(app, patterns, status: 301)
142
+ @patterns = patterns
143
+
144
+ super(app, status: status)
145
+ end
146
+
147
+ def [] path
148
+ if location = @patterns[path]
149
+ return redirect(location)
150
+ end
151
+ end
152
+ end
153
+
154
+ class Moved < Redirection
155
+ def initialize(app, pattern, prefix, status: 301, flatten: false)
156
+ @app = app
157
+
158
+ @pattern = pattern
159
+ @prefix = prefix
160
+ @flatten = flatten
161
+
162
+ super(app, status: status)
163
+ end
164
+
165
+ def [] path
166
+ if path.start_with?(@pattern)
167
+ if @flatten
168
+ return redirect(@prefix)
169
+ else
170
+ return redirect(path.sub(@pattern, @prefix))
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Utopia
22
- VERSION = "1.3.2"
22
+ VERSION = "1.4.0"
23
23
  end
@@ -14,10 +14,11 @@ require 'utopia'
14
14
  require 'rack/cache'
15
15
 
16
16
  if RACK_ENV == :production
17
- use Utopia::ExceptionHandler, "/errors/exception"
18
- use Utopia::MailExceptions
17
+ use Utopia::Exceptions::Handler
18
+ use Utopia::Exceptions::Mailer
19
19
  elsif RACK_ENV == :development
20
20
  use Rack::ShowExceptions
21
+ use Utopia::Static, root: 'public'
21
22
  end
22
23
 
23
24
  use Rack::Sendfile
@@ -31,16 +32,13 @@ end
31
32
 
32
33
  use Rack::ContentLength
33
34
 
34
- use Utopia::Redirector,
35
- patterns: [
36
- Utopia::Redirector::DIRECTORY_INDEX
37
- ],
38
- strings: {
39
- '/' => '/welcome/index',
40
- },
41
- errors: {
42
- 404 => "/errors/file-not-found"
43
- }
35
+ use Utopia::Redirection::Rewrite,
36
+ '/' => '/welcome/index'
37
+
38
+ use Utopia::Redirection::DirectoryIndex
39
+
40
+ use Utopia::Redirection::Errors,
41
+ 404 => '/errors/file-not-found'
44
42
 
45
43
  use Utopia::Localization,
46
44
  :default_locale => 'en',
@@ -52,6 +50,12 @@ use Utopia::Controller,
52
50
 
53
51
  use Utopia::Static
54
52
 
53
+ if RACK_ENV != :production
54
+ # Serve static files from public/ when not running in a production environment:
55
+ use Utopia::Static, root: 'public'
56
+ end
57
+
58
+ # Serve dynamic content
55
59
  use Utopia::Content,
56
60
  cache_templates: (RACK_ENV == :production),
57
61
  tags: {
@@ -0,0 +1 @@
1
+ <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 420 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><rect x="0" y="0" width="420" height="56" style="fill:#f79433;"/><rect x="0" y="56" width="420" height="24" style="fill:#4e8dd8;"/><g><path d="M75.145,70.819c2.37,-3.097 4.173,-6.921 5.111,-11.365c0.91,-4.318 1.498,-9.261 1.498,-14.692l0,-44.762l-62.754,0l0,44.762c0,2.628 0.244,5.333 0.407,8.035c0.168,2.782 0.674,5.515 1.345,8.118c0.68,2.644 1.739,5.173 3.067,7.517c1.363,2.405 3.263,4.526 5.609,6.303c2.319,1.755 5.245,3.163 8.677,4.172c1.617,0.478 3.416,1.093 5.354,1.093l13.856,0c3.071,0 5.797,-1.058 8.131,-2.001c4.042,-1.631 7.305,-4.049 9.699,-7.18Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M151.481,18.701l0,-18.701l-62.754,-0.022l0,18.723l22.246,0l0.02,61.299l17.93,0l-0.02,-61.299l22.578,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M229.926,39.999c0,-22.051 -16.979,-39.992 -37.852,-39.992c-20.872,0 -37.851,17.942 -37.851,39.992c0,22.054 16.979,39.994 37.851,39.994c20.873,0 37.852,-17.94 37.852,-39.994Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M269.238,50.909c9.717,0 17.181,-2.066 22.183,-6.395c5.087,-4.399 7.667,-10.942 7.667,-19.575c0,-3.257 -0.393,-5.962 -1.167,-8.476c-0.778,-2.528 -1.883,-4.934 -3.281,-6.814c-1.401,-1.882 -3.098,-3.458 -5.045,-4.703c-1.895,-1.21 -4.003,-2.198 -6.264,-2.943c-2.239,-0.737 -4.64,-1.263 -7.139,-1.56c-2.464,-0.292 -5.016,-0.443 -7.587,-0.443l-29.468,0l0,80l17.93,0l0,-29.091l12.171,0Z" style="fill:#fff;fill-rule:nonzero;"/><rect x="304.879" y="0" width="17.93" height="80" style="fill:#fff;"/><path d="M362.589,0l-29.477,80l75.888,0l-31.247,-80l-15.164,0Z" style="fill:#fff;fill-rule:nonzero;"/></g></g></svg>
@@ -1 +1 @@
1
- This directory is required by Apache/Phusion Passenger.
1
+ This directory is required by Apache/Phusion Passenger and contains static assets that are typically served using sendfile.
@@ -20,33 +20,25 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  # THE SOFTWARE.
22
22
 
23
- require 'rack/mock'
24
- require 'rack/test'
23
+ require_relative '../rack_helper'
24
+ require 'utopia/exceptions'
25
25
 
26
- require 'utopia/controller'
27
- require 'utopia/exception_handler'
28
-
29
- module Utopia::ExceptionHandlerSpec
30
- describe Utopia::ExceptionHandler do
31
- include Rack::Test::Methods
32
-
33
- let(:app) {Rack::Builder.parse_file(File.expand_path('exception_handler_spec.ru', __dir__)).first}
26
+ RSpec.describe Utopia::Exceptions::Handler do
27
+ include_context "rack app", File.expand_path("handler_spec.ru", __dir__)
28
+
29
+ it "should successfully call the controller method" do
30
+ # This request will raise an exception, and then redirect to the /exception url which will fail again, and cause a fatal error.
31
+ get "/blow?fatal=true"
34
32
 
35
- it "should successfully call the controller method" do
36
- # This request will raise an exception, and then redirect to the /exception url which will fail again, and cause a fatal error.
37
-
38
- get "/blow?fatal=true"
39
-
40
- expect(last_response.status).to be == 500
41
- expect(last_response.headers['Content-Type']).to be == 'text/plain'
42
- expect(last_response.body).to be_include 'fatal error'
43
- end
33
+ expect(last_response.status).to be == 500
34
+ expect(last_response.headers['Content-Type']).to be == 'text/plain'
35
+ expect(last_response.body).to be_include 'fatal error'
36
+ end
37
+
38
+ it "should fail with a 500 error" do
39
+ get "/blow"
44
40
 
45
- it "should fail with a 500 error" do
46
- get "/blow"
47
-
48
- expect(last_response.status).to be == 500
49
- expect(last_response.body).to be_include 'Error Will Robertson'
50
- end
41
+ expect(last_response.status).to be == 500
42
+ expect(last_response.body).to be_include 'Error Will Robertson'
51
43
  end
52
44
  end
@@ -0,0 +1,6 @@
1
+
2
+ use Utopia::Exceptions::Handler, '/exception'
3
+
4
+ use Utopia::Controller, root: File.expand_path('handler_spec', __dir__)
5
+
6
+ run lambda {|env| [404, {}, []]}
@@ -19,10 +19,18 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require_relative 'rack_helper'
22
- require 'utopia/redirector'
22
+ require 'utopia/redirection'
23
23
 
24
- RSpec.describe Utopia::Redirector do
25
- include_context "rack app", "redirector_spec.ru"
24
+ RSpec.describe Utopia::Redirection do
25
+ include_context "rack app", "redirection_spec.ru"
26
+
27
+ it "should redirect directory to index" do
28
+ get "/welcome/"
29
+
30
+ expect(last_response.status).to be == 307
31
+ expect(last_response.headers['Location']).to be == '/welcome/index'
32
+ expect(last_response.headers['Cache-Control']).to include("max-age=86400")
33
+ end
26
34
 
27
35
  it "should be permanently moved" do
28
36
  get "/a"
@@ -36,7 +44,7 @@ RSpec.describe Utopia::Redirector do
36
44
  get "/"
37
45
 
38
46
  expect(last_response.status).to be == 301
39
- expect(last_response.headers['Location']).to be == '/c'
47
+ expect(last_response.headers['Location']).to be == '/welcome/index'
40
48
  expect(last_response.headers['Cache-Control']).to include("max-age=86400")
41
49
  end
42
50
 
@@ -48,6 +56,20 @@ RSpec.describe Utopia::Redirector do
48
56
  end
49
57
 
50
58
  it "should blow up if internal error redirect also fails" do
51
- expect{get "/teapot"}.to raise_error Utopia::FailedRequestError
59
+ expect{get "/teapot"}.to raise_error Utopia::Redirection::RequestFailure
60
+ end
61
+
62
+ it "should redirect deep url to top" do
63
+ get "/hierarchy/a/b/c/d/e"
64
+
65
+ expect(last_response.status).to be == 301
66
+ expect(last_response.headers['Location']).to be == '/hierarchy'
67
+ end
68
+
69
+ it "should get a weird status" do
70
+ get "/weird"
71
+
72
+ expect(last_response.status).to be == 333
73
+ expect(last_response.headers['Location']).to be == '/status'
52
74
  end
53
75
  end
@@ -0,0 +1,25 @@
1
+
2
+ use Utopia::Redirection::Rewrite, "/" => "/welcome/index"
3
+
4
+ use Utopia::Redirection::DirectoryIndex
5
+
6
+ use Utopia::Redirection::Errors,
7
+ 404 => '/error',
8
+ 418 => '/teapot'
9
+
10
+ use Utopia::Redirection::Moved, "/a", "/b"
11
+ use Utopia::Redirection::Moved, "/hierarchy/", "/hierarchy", flatten: true
12
+ use Utopia::Redirection::Moved, "/weird", "/status", status: 333
13
+
14
+ def error_handler(env)
15
+ request = Rack::Request.new(env)
16
+ if request.path_info == "/error"
17
+ [200, {}, ["File not found :("]]
18
+ elsif request.path_info == "/teapot"
19
+ [418, {}, ["I'm a teapot!"]]
20
+ else
21
+ [404, {}, []]
22
+ end
23
+ end
24
+
25
+ run self.method(:error_handler)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: utopia
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-22 00:00:00.000000000 Z
11
+ date: 2016-02-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: trenni
@@ -199,7 +199,9 @@ files:
199
199
  - lib/utopia/controller/respond.rb
200
200
  - lib/utopia/controller/rewrite.rb
201
201
  - lib/utopia/controller/variables.rb
202
- - lib/utopia/exception_handler.rb
202
+ - lib/utopia/exceptions.rb
203
+ - lib/utopia/exceptions/handler.rb
204
+ - lib/utopia/exceptions/mailer.rb
203
205
  - lib/utopia/extensions/array.rb
204
206
  - lib/utopia/extensions/date.rb
205
207
  - lib/utopia/extensions/rack.rb
@@ -210,7 +212,7 @@ files:
210
212
  - lib/utopia/middleware.rb
211
213
  - lib/utopia/path.rb
212
214
  - lib/utopia/path/matcher.rb
213
- - lib/utopia/redirector.rb
215
+ - lib/utopia/redirection.rb
214
216
  - lib/utopia/session.rb
215
217
  - lib/utopia/session/encrypted_cookie.rb
216
218
  - lib/utopia/session/lazy_hash.rb
@@ -234,16 +236,14 @@ files:
234
236
  - setup/site/lib/readme.txt
235
237
  - setup/site/pages/_heading.xnode
236
238
  - setup/site/pages/_page.xnode
237
- - setup/site/pages/_static/icon.png
238
- - setup/site/pages/_static/site.css
239
- - setup/site/pages/_static/utopia-background.svg
240
- - setup/site/pages/_static/utopia.svg
241
- - setup/site/pages/errors/controller.rb
242
239
  - setup/site/pages/errors/exception.xnode
243
240
  - setup/site/pages/errors/file-not-found.xnode
244
241
  - setup/site/pages/links.yaml
245
242
  - setup/site/pages/welcome/index.xnode
246
- - setup/site/public/_static
243
+ - setup/site/public/_static/icon.png
244
+ - setup/site/public/_static/site.css
245
+ - setup/site/public/_static/utopia-background.svg
246
+ - setup/site/public/_static/utopia.svg
247
247
  - setup/site/public/readme.txt
248
248
  - setup/site/tmp/readme.txt
249
249
  - spec/utopia/content/link_spec.rb
@@ -291,9 +291,9 @@ files:
291
291
  - spec/utopia/controller/rewrite_spec.rb
292
292
  - spec/utopia/controller/sequence_spec.rb
293
293
  - spec/utopia/controller/variables_spec.rb
294
- - spec/utopia/exception_handler_spec.rb
295
- - spec/utopia/exception_handler_spec.ru
296
- - spec/utopia/exception_handler_spec/controller.rb
294
+ - spec/utopia/exceptions/handler_spec.rb
295
+ - spec/utopia/exceptions/handler_spec.ru
296
+ - spec/utopia/exceptions/handler_spec/controller.rb
297
297
  - spec/utopia/extensions_spec.rb
298
298
  - spec/utopia/http/status_spec.rb
299
299
  - spec/utopia/locale_spec.rb
@@ -316,8 +316,8 @@ files:
316
316
  - spec/utopia/path_spec.rb
317
317
  - spec/utopia/rack_helper.rb
318
318
  - spec/utopia/rack_spec.rb
319
- - spec/utopia/redirector_spec.rb
320
- - spec/utopia/redirector_spec.ru
319
+ - spec/utopia/redirection_spec.rb
320
+ - spec/utopia/redirection_spec.ru
321
321
  - spec/utopia/session_spec.rb
322
322
  - spec/utopia/session_spec.ru
323
323
  - spec/utopia/static_spec.rb
@@ -392,9 +392,9 @@ test_files:
392
392
  - spec/utopia/controller/rewrite_spec.rb
393
393
  - spec/utopia/controller/sequence_spec.rb
394
394
  - spec/utopia/controller/variables_spec.rb
395
- - spec/utopia/exception_handler_spec.rb
396
- - spec/utopia/exception_handler_spec.ru
397
- - spec/utopia/exception_handler_spec/controller.rb
395
+ - spec/utopia/exceptions/handler_spec.rb
396
+ - spec/utopia/exceptions/handler_spec.ru
397
+ - spec/utopia/exceptions/handler_spec/controller.rb
398
398
  - spec/utopia/extensions_spec.rb
399
399
  - spec/utopia/http/status_spec.rb
400
400
  - spec/utopia/locale_spec.rb
@@ -417,8 +417,8 @@ test_files:
417
417
  - spec/utopia/path_spec.rb
418
418
  - spec/utopia/rack_helper.rb
419
419
  - spec/utopia/rack_spec.rb
420
- - spec/utopia/redirector_spec.rb
421
- - spec/utopia/redirector_spec.ru
420
+ - spec/utopia/redirection_spec.rb
421
+ - spec/utopia/redirection_spec.ru
422
422
  - spec/utopia/session_spec.rb
423
423
  - spec/utopia/session_spec.ru
424
424
  - spec/utopia/static_spec.rb
@@ -1,104 +0,0 @@
1
- # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
20
-
21
- require_relative 'middleware'
22
-
23
- require 'trenni/strings'
24
-
25
- module Utopia
26
- class ExceptionHandler
27
- def initialize(app, location)
28
- @app = app
29
-
30
- @location = location
31
-
32
- self.freeze
33
- end
34
-
35
- def freeze
36
- @location.freeze
37
-
38
- super
39
- end
40
-
41
- private def write_exception_to_stream(stream, env, exception, include_backtrace = false)
42
- buffer = []
43
-
44
- buffer << "While requesting resource #{env[Rack::PATH_INFO].inspect}, a fatal error occurred:"
45
-
46
- while exception != nil
47
- buffer << "\t#{exception.class.name}: #{exception.to_s}"
48
-
49
- if include_backtrace
50
- exception.backtrace.each do |line|
51
- buffer << "\t\t#{line}"
52
- end
53
- end
54
-
55
- exception = exception.cause
56
- end
57
-
58
- # We do this in one go so that lines don't get mixed up.
59
- stream.puts buffer.join("\n")
60
- end
61
-
62
- # Generate a very simple fatal error response. This function should be unlikely to fail. Additionally, it generates a lowest common denominator response which should be suitable as a response to any kind of request. Ideally, this response is also not good or useful for any kind of higher level browser or API client, as this is not a normal error path but one that represents broken behaviour.
63
- def fatal_error(env, exception)
64
- body = StringIO.new
65
-
66
- write_exception_to_stream(body, env, exception)
67
- body.rewind
68
-
69
- return [500, {HTTP::CONTENT_TYPE => "text/plain"}, body]
70
- end
71
-
72
- def log_exception(env, exception)
73
- # An error has occurred, log it:
74
- output = env['rack.errors'] || $stderr
75
- write_exception_to_stream(output, env, exception, true)
76
- end
77
-
78
- def redirect(env, exception)
79
- response = @app.call(env.merge(Rack::PATH_INFO => @location, Rack::REQUEST_METHOD => Rack::GET))
80
-
81
- return [500, response[1], response[2]]
82
- end
83
-
84
- def call(env)
85
- begin
86
- return @app.call(env)
87
- rescue Exception => exception
88
- log_exception(env, exception)
89
-
90
- # If the error occurred while accessing the error handler, we finish with a fatal error:
91
- if env[Rack::PATH_INFO] == @location
92
- return fatal_error(env, exception)
93
- else
94
- # If redirection fails, we also finish with a fatal error:
95
- begin
96
- return redirect(env, exception)
97
- rescue
98
- return fatal_error(env, exception)
99
- end
100
- end
101
- end
102
- end
103
- end
104
- end
@@ -1,164 +0,0 @@
1
- # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
20
-
21
- require_relative 'middleware'
22
-
23
- module Utopia
24
- class FailedRequestError < StandardError
25
- def initialize(resource_path, resource_status, error_path, error_status)
26
- @resource_path = resource_path
27
- @resource_status = resource_status
28
-
29
- @error_path = error_path
30
- @error_status = error_status
31
- end
32
-
33
- def to_s
34
- "Requested resource #{@resource_path} resulted in a #{@resource_status} error. Requested error handler #{@error_path} resulted in a #{@error_status} error."
35
- end
36
- end
37
-
38
- class Redirector
39
- # This redirects directories to the directory + 'index'
40
- DIRECTORY_INDEX = [/^(.*)\/$/, lambda{|prefix| [307, {HTTP::LOCATION => "#{prefix}index"}, []]}].freeze
41
-
42
- # Redirects a whole source tree to a destination tree, given by the roots.
43
- def self.moved(source_root, destination_root, max_age = 3600*24)
44
- return [/^#{Regexp.escape(source_root)}(.*)$/, lambda{|match| destination_root + match[1]}]
45
- end
46
-
47
- def self.starts_with(source_root, destination_uri)
48
- return [/^#{Regexp.escape(source_root)}/, destination_uri]
49
- end
50
-
51
- private
52
-
53
- def normalize_strings(strings)
54
- normalized = {}
55
-
56
- strings.each_pair do |key, value|
57
- if Array === key
58
- key.each { |s| normalized[s] = value }
59
- else
60
- normalized[key] = value
61
- end
62
- end
63
-
64
- return normalized
65
- end
66
-
67
- def normalize_patterns(patterns)
68
- normalized = []
69
-
70
- patterns.each do |pattern|
71
- *keys, uri = *pattern
72
-
73
- keys.each do |key|
74
- normalized.push([key, uri])
75
- end
76
- end
77
-
78
- return normalized
79
- end
80
-
81
- public
82
-
83
- def initialize(app, **options)
84
- @app = app
85
-
86
- @strings = options[:strings] || {}
87
- @patterns = options[:patterns] || []
88
-
89
- @patterns.collect! do |rule|
90
- if Symbol === rule[0]
91
- self.class.send(*rule)
92
- else
93
- rule
94
- end
95
- end
96
-
97
- @strings = normalize_strings(@strings)
98
- @patterns = normalize_patterns(@patterns)
99
-
100
- @errors = options[:errors]
101
-
102
- self.freeze
103
- end
104
-
105
- def freeze
106
- @strings.freeze
107
- @patterns.freeze
108
- @errors.freeze
109
-
110
- super
111
- end
112
-
113
- def cache_control(max_age)
114
- # http://jacquesmattheij.com/301-redirects-a-dangerous-one-way-street
115
- "max-age=#{max_age}"
116
- end
117
-
118
- # We cache 301 redirects for 24 hours.
119
- DEFAULT_MAX_AGE = 3600*24
120
-
121
- def redirect(uri, match_data, status: 301, max_age: DEFAULT_MAX_AGE)
122
- cache_control = self.cache_control(max_age)
123
-
124
- if uri.respond_to? :call
125
- uri = uri.call(match_data)
126
- end
127
-
128
- return [status, {HTTP::LOCATION => uri.to_s, HTTP::CACHE_CONTROL => cache_control}, []]
129
- end
130
-
131
- def call(env)
132
- base_path = env[Rack::PATH_INFO]
133
-
134
- if uri = @strings[base_path]
135
- return redirect(@strings[base_path], base_path)
136
- end
137
-
138
- @patterns.each do |pattern, uri|
139
- if match_data = pattern.match(base_path)
140
- result = redirect(uri, match_data)
141
-
142
- return result if result != nil
143
- end
144
- end
145
-
146
- response = @app.call(env)
147
-
148
- if @errors && response[0] >= 400 && uri = @errors[response[0]]
149
- error_request = env.merge(Rack::PATH_INFO => uri, Rack::REQUEST_METHOD => Rack::GET)
150
- error_response = @app.call(error_request)
151
-
152
- if error_response[0] >= 400
153
- raise FailedRequestError.new(env[Rack::PATH_INFO], response[0], uri, error_response[0])
154
- else
155
- # Feed the error code back with the error document
156
- error_response[0] = response[0]
157
- return error_response
158
- end
159
- else
160
- return response
161
- end
162
- end
163
- end
164
- end
@@ -1 +0,0 @@
1
- ../../../../materials/utopia.svg
@@ -1,9 +0,0 @@
1
-
2
- prepend Respond
3
-
4
- respond.with_json
5
- respond.otherwise_passthrough
6
-
7
- on 'file-not-found' do
8
- fail! 404, {message: 'File not found'}
9
- end
@@ -1 +0,0 @@
1
- ../pages/_static
@@ -1,6 +0,0 @@
1
-
2
- use Utopia::ExceptionHandler, '/exception'
3
-
4
- use Utopia::Controller, root: File.expand_path('exception_handler_spec', __dir__)
5
-
6
- run lambda {|env| [404, {}, []]}
@@ -1,26 +0,0 @@
1
-
2
- use Utopia::Redirector,
3
- patterns: [
4
- Utopia::Redirector::DIRECTORY_INDEX,
5
- [:moved, "/a", "/b"],
6
- ],
7
- strings: {
8
- '/' => '/c',
9
- },
10
- errors: {
11
- 404 => "/error",
12
- 418 => "/teapot",
13
- }
14
-
15
- def error_handler(env)
16
- request = Rack::Request.new(env)
17
- if request.path_info == "/error"
18
- [200, {}, ["File not found :("]]
19
- elsif request.path_info == "/teapot"
20
- [418, {}, ["I'm a teapot!"]]
21
- else
22
- [404, {}, []]
23
- end
24
- end
25
-
26
- run self.method(:error_handler)