utopia 1.3.2 → 1.4.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.
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)