utopia 1.7.1 → 1.8.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 +4 -4
- data/.travis.yml +2 -3
- data/README.md +142 -11
- data/benchmarks/string_vs_symbol.rb +12 -0
- data/lib/utopia/command.rb +16 -13
- data/lib/utopia/content.rb +1 -5
- data/lib/utopia/content/node.rb +9 -4
- data/lib/utopia/{extensions/rack.rb → content/response.rb} +33 -30
- data/lib/utopia/content/tag.rb +14 -17
- data/lib/utopia/content/transaction.rb +19 -17
- data/lib/utopia/controller.rb +29 -8
- data/lib/utopia/controller/actions.rb +148 -0
- data/lib/utopia/controller/base.rb +9 -49
- data/lib/utopia/controller/respond.rb +1 -1
- data/lib/utopia/controller/rewrite.rb +9 -1
- data/lib/utopia/controller/variables.rb +1 -0
- data/lib/utopia/localization.rb +4 -1
- data/lib/utopia/middleware.rb +0 -2
- data/lib/utopia/path.rb +9 -0
- data/lib/utopia/path/matcher.rb +0 -1
- data/lib/utopia/redirection.rb +3 -2
- data/lib/utopia/session.rb +119 -2
- data/lib/utopia/session/lazy_hash.rb +1 -3
- data/lib/utopia/setup.rb +73 -0
- data/lib/utopia/static.rb +9 -2
- data/lib/utopia/version.rb +1 -1
- data/setup/examples/wiki/controller.rb +41 -0
- data/setup/examples/wiki/edit.xnode +15 -0
- data/setup/examples/wiki/index.xnode +10 -0
- data/setup/examples/wiki/welcome/content.md +3 -0
- data/setup/server/config/environment.yaml +1 -0
- data/setup/server/git/hooks/post-receive +4 -5
- data/setup/site/Gemfile +5 -0
- data/setup/site/config.ru +2 -1
- data/setup/site/config/environment.rb +5 -17
- data/setup/site/pages/_page.xnode +4 -2
- data/setup/site/pages/links.yaml +1 -1
- data/setup/site/pages/welcome/index.xnode +33 -15
- data/setup/site/public/_static/site.css +72 -4
- data/setup/site/tasks/utopia.rake +8 -0
- data/spec/utopia/{rack_spec.rb → content/response_spec.rb} +12 -19
- data/spec/utopia/content_spec.rb +2 -3
- data/spec/utopia/controller/{action_spec.rb → actions_spec.rb} +18 -32
- data/spec/utopia/controller/middleware_spec.rb +10 -10
- data/spec/utopia/controller/middleware_spec/controller/controller.rb +3 -3
- data/spec/utopia/controller/middleware_spec/controller/nested/controller.rb +1 -1
- data/spec/utopia/controller/middleware_spec/redirect/controller.rb +1 -1
- data/spec/utopia/controller/respond_spec.rb +3 -2
- data/spec/utopia/controller/respond_spec/api/controller.rb +2 -2
- data/spec/utopia/controller/respond_spec/errors/controller.rb +1 -1
- data/spec/utopia/controller/rewrite_spec.rb +1 -1
- data/spec/utopia/controller/sequence_spec.rb +12 -16
- data/spec/utopia/exceptions/handler_spec/controller.rb +2 -2
- data/spec/utopia/performance_spec/config.ru +1 -0
- data/spec/utopia/session_spec.rb +34 -1
- data/spec/utopia/session_spec.ru +3 -3
- data/spec/utopia/setup_spec.rb +2 -2
- data/utopia.gemspec +2 -2
- metadata +18 -12
- data/lib/utopia/controller/action.rb +0 -116
- data/lib/utopia/session/encrypted_cookie.rb +0 -118
@@ -166,7 +166,7 @@ module Utopia
|
|
166
166
|
end
|
167
167
|
|
168
168
|
# Rewrite the path before processing the request if possible.
|
169
|
-
def
|
169
|
+
def process!(request, path)
|
170
170
|
if response = super
|
171
171
|
response = self.class.response_for(self, request, path, response)
|
172
172
|
|
@@ -23,6 +23,12 @@ require_relative '../path/matcher'
|
|
23
23
|
|
24
24
|
module Utopia
|
25
25
|
class Controller
|
26
|
+
# This controller layer rewrites the path before executing controller actions. When the rule matches, the supplied block is executed.
|
27
|
+
# @example
|
28
|
+
# prepend Rewrite
|
29
|
+
# rewrite.extract_prefix id: Integer do
|
30
|
+
# @user = User.find(@id)
|
31
|
+
# end
|
26
32
|
module Rewrite
|
27
33
|
def self.prepended(base)
|
28
34
|
base.extend(ClassMethods)
|
@@ -86,6 +92,8 @@ module Utopia
|
|
86
92
|
@rules = []
|
87
93
|
end
|
88
94
|
|
95
|
+
attr :rules
|
96
|
+
|
89
97
|
def extract_prefix(**arguments, &block)
|
90
98
|
@rules << ExtractPrefixRule.new(arguments, block)
|
91
99
|
end
|
@@ -116,7 +124,7 @@ module Utopia
|
|
116
124
|
end
|
117
125
|
|
118
126
|
# Rewrite the path before processing the request if possible.
|
119
|
-
def
|
127
|
+
def process!(request, path)
|
120
128
|
catch_response do
|
121
129
|
self.class.rewrite_request(self, request, path)
|
122
130
|
end || super
|
data/lib/utopia/localization.rb
CHANGED
@@ -68,7 +68,8 @@ module Utopia
|
|
68
68
|
|
69
69
|
# Locales here are represented as an array of strings, e.g. ['en', 'ja', 'cn', 'de'].
|
70
70
|
unless @default_locales = options[:default_locales]
|
71
|
-
|
71
|
+
# We append nil, i.e. no localization.
|
72
|
+
@default_locales = @all_locales.names + [nil]
|
72
73
|
end
|
73
74
|
|
74
75
|
if @default_locale = options[:default_locale]
|
@@ -123,6 +124,8 @@ module Utopia
|
|
123
124
|
end
|
124
125
|
end
|
125
126
|
|
127
|
+
HTTP_HOST = 'HTTP_HOST'.freeze
|
128
|
+
|
126
129
|
def host_preferred_locales(env)
|
127
130
|
http_host = env[Rack::HTTP_HOST]
|
128
131
|
|
data/lib/utopia/middleware.rb
CHANGED
data/lib/utopia/path.rb
CHANGED
@@ -133,6 +133,11 @@ module Utopia
|
|
133
133
|
end
|
134
134
|
end
|
135
135
|
|
136
|
+
# This constructor takes a string and generates a relative path as efficiently as possible. This is a direct entry point for all controller invocations so it's designed to suit the requirements of that function.
|
137
|
+
def self.from_string(string)
|
138
|
+
self.new(unescape(string).split(SEPARATOR, -1))
|
139
|
+
end
|
140
|
+
|
136
141
|
def self.create(path)
|
137
142
|
case path
|
138
143
|
when Path
|
@@ -178,6 +183,10 @@ module Utopia
|
|
178
183
|
end
|
179
184
|
end
|
180
185
|
|
186
|
+
def to_relative!
|
187
|
+
@components.shift if relative?
|
188
|
+
end
|
189
|
+
|
181
190
|
def to_str
|
182
191
|
if @components == ['']
|
183
192
|
SEPARATOR
|
data/lib/utopia/path/matcher.rb
CHANGED
@@ -63,7 +63,6 @@ module Utopia
|
|
63
63
|
end
|
64
64
|
|
65
65
|
# This is a path prefix matching algorithm. The pattern is an array of String, Symbol, Regexp, or nil. The components is an array of String.
|
66
|
-
# As long as the components match the patterns,
|
67
66
|
def match(path)
|
68
67
|
components = path.to_a
|
69
68
|
|
data/lib/utopia/redirection.rb
CHANGED
@@ -21,6 +21,7 @@
|
|
21
21
|
require_relative 'middleware'
|
22
22
|
|
23
23
|
module Utopia
|
24
|
+
# A middleware which assists with redirecting from one path to another.
|
24
25
|
module Redirection
|
25
26
|
class RequestFailure < StandardError
|
26
27
|
def initialize(resource_path, resource_status, error_path, error_status)
|
@@ -123,9 +124,9 @@ module Utopia
|
|
123
124
|
end
|
124
125
|
|
125
126
|
class DirectoryIndex < Redirection
|
126
|
-
def initialize(app, index
|
127
|
+
def initialize(app, index: 'index')
|
127
128
|
@app = app
|
128
|
-
@index =
|
129
|
+
@index = index
|
129
130
|
|
130
131
|
super(app)
|
131
132
|
end
|
data/lib/utopia/session.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright,
|
1
|
+
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the "Software"), to deal
|
@@ -18,8 +18,125 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
+
require 'openssl'
|
22
|
+
require 'digest/sha2'
|
23
|
+
|
24
|
+
require_relative 'session/lazy_hash'
|
25
|
+
|
21
26
|
module Utopia
|
22
|
-
|
27
|
+
# Stores all session data client side using a private symmetric encrpytion key.
|
28
|
+
class Session
|
23
29
|
RACK_SESSION = "rack.session".freeze
|
30
|
+
CIPHER_ALGORITHM = "aes-256-cbc"
|
31
|
+
KEY_LENGTH = 32
|
32
|
+
|
33
|
+
def initialize(app, secret:, **options)
|
34
|
+
@app = app
|
35
|
+
@cookie_name = options.delete(:cookie_name) || (RACK_SESSION + ".encrypted")
|
36
|
+
|
37
|
+
salt = OpenSSL::Random.random_bytes(16)
|
38
|
+
@key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret, salt, 1, KEY_LENGTH)
|
39
|
+
|
40
|
+
@options = {
|
41
|
+
:domain => nil,
|
42
|
+
:path => "/",
|
43
|
+
:expires_after => nil
|
44
|
+
}.merge(options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(env)
|
48
|
+
session_hash = prepare_session(env)
|
49
|
+
|
50
|
+
status, headers, body = @app.call(env)
|
51
|
+
|
52
|
+
if session_hash.changed?
|
53
|
+
commit(session_hash.values, headers)
|
54
|
+
end
|
55
|
+
|
56
|
+
return [status, headers, body]
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def prepare_session(env)
|
62
|
+
env[RACK_SESSION] = LazyHash.new do
|
63
|
+
self.load_session_values(env)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Constructs a valid session for the given request. These fields must match as per the checks performed in `valid_session?`:
|
68
|
+
def build_initial_session(request)
|
69
|
+
{
|
70
|
+
request_ip: request.ip,
|
71
|
+
request_user_agent: request.user_agent,
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Load session from user supplied cookie. If the data is invalid or otherwise fails validation, `build_iniital_session` is invoked.
|
76
|
+
# @return hash of values.
|
77
|
+
def load_session_values(env)
|
78
|
+
request = Rack::Request.new(env)
|
79
|
+
|
80
|
+
# Decrypt the data from the user if possible:
|
81
|
+
if data = request.cookies[@cookie_name]
|
82
|
+
if values = decrypt(data) and valid_session?(request, values)
|
83
|
+
return values
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# If we couldn't create a session
|
88
|
+
return build_initial_session(request)
|
89
|
+
end
|
90
|
+
|
91
|
+
def valid_session?(request, values)
|
92
|
+
if values[:request_ip] != request.ip
|
93
|
+
return false
|
94
|
+
end
|
95
|
+
|
96
|
+
if values[:request_user_agent] != request.user_agent
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
|
100
|
+
return true
|
101
|
+
end
|
102
|
+
|
103
|
+
def commit(values, headers)
|
104
|
+
data = encrypt(values)
|
105
|
+
|
106
|
+
cookie = {:value => data}
|
107
|
+
|
108
|
+
cookie[:expires] = Time.now + @options[:expires_after] unless @options[:expires_after].nil?
|
109
|
+
|
110
|
+
Rack::Utils.set_cookie_header!(headers, @cookie_name, cookie.merge(@options))
|
111
|
+
end
|
112
|
+
|
113
|
+
def encrypt(hash)
|
114
|
+
c = OpenSSL::Cipher.new(CIPHER_ALGORITHM)
|
115
|
+
c.encrypt
|
116
|
+
|
117
|
+
# your pass is what is used to encrypt/decrypt
|
118
|
+
c.key = @key
|
119
|
+
c.iv = iv = c.random_iv
|
120
|
+
|
121
|
+
e = c.update(Marshal.dump(hash))
|
122
|
+
e << c.final
|
123
|
+
|
124
|
+
return [iv, e].pack("m16m*")
|
125
|
+
end
|
126
|
+
|
127
|
+
def decrypt(data)
|
128
|
+
iv, e = data.unpack("m16m*")
|
129
|
+
|
130
|
+
c = OpenSSL::Cipher.new(CIPHER_ALGORITHM)
|
131
|
+
c.decrypt
|
132
|
+
|
133
|
+
c.key = @key
|
134
|
+
c.iv = iv
|
135
|
+
|
136
|
+
d = c.update(e)
|
137
|
+
d << c.final
|
138
|
+
|
139
|
+
return Marshal.load(d)
|
140
|
+
end
|
24
141
|
end
|
25
142
|
end
|
@@ -18,10 +18,8 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
require_relative '../session'
|
22
|
-
|
23
21
|
module Utopia
|
24
|
-
|
22
|
+
class Session
|
25
23
|
# A simple hash table which fetches it's values only when required.
|
26
24
|
class LazyHash
|
27
25
|
def initialize(&block)
|
data/lib/utopia/setup.rb
ADDED
@@ -0,0 +1,73 @@
|
|
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
|
+
class Bootstrap
|
23
|
+
def initialize(config_root)
|
24
|
+
@config_root = config_root
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup
|
28
|
+
setup_encoding
|
29
|
+
|
30
|
+
setup_environment
|
31
|
+
|
32
|
+
setup_load_path
|
33
|
+
|
34
|
+
require_relative '../utopia'
|
35
|
+
end
|
36
|
+
|
37
|
+
def environment_path
|
38
|
+
File.expand_path('environment.yaml', @config_root)
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_encoding
|
42
|
+
# If you don't specify these, it's possible to have issues when encodings mismatch on the server.
|
43
|
+
Encoding.default_external = Encoding::UTF_8
|
44
|
+
Encoding.default_internal = Encoding::UTF_8
|
45
|
+
end
|
46
|
+
|
47
|
+
def setup_environment
|
48
|
+
if File.exist? environment_path
|
49
|
+
require 'yaml'
|
50
|
+
|
51
|
+
# Load the YAML environment file:
|
52
|
+
environment = YAML.load_file(environment_path)
|
53
|
+
|
54
|
+
# Update the process environment:
|
55
|
+
ENV.update(environment)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def setup_load_path
|
60
|
+
# Allow loading library code from lib directory:
|
61
|
+
$LOAD_PATH << File.expand_path('../lib', @config_root)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.setup(config_root = nil, **options)
|
66
|
+
# We extract the directory of the caller to get the path to $root/config
|
67
|
+
if config_root.nil?
|
68
|
+
config_root = File.dirname(caller[0])
|
69
|
+
end
|
70
|
+
|
71
|
+
Bootstrap.new(config_root).setup
|
72
|
+
end
|
73
|
+
end
|
data/lib/utopia/static.rb
CHANGED
@@ -27,8 +27,9 @@ require 'digest/sha1'
|
|
27
27
|
require 'mime/types'
|
28
28
|
|
29
29
|
module Utopia
|
30
|
-
# Serve static files
|
30
|
+
# Serve static files from the specified root directory.
|
31
31
|
class Static
|
32
|
+
# Default mime-types which are common for files served over HTTP:
|
32
33
|
MIME_TYPES = {
|
33
34
|
:xiph => {
|
34
35
|
"ogx" => "application/ogg",
|
@@ -62,6 +63,7 @@ module Utopia
|
|
62
63
|
]
|
63
64
|
}
|
64
65
|
|
66
|
+
# A class to assist with loading mime-type metadata.
|
65
67
|
class MimeTypeLoader
|
66
68
|
def initialize(library)
|
67
69
|
@extensions = {}
|
@@ -116,7 +118,8 @@ module Utopia
|
|
116
118
|
end
|
117
119
|
|
118
120
|
private
|
119
|
-
|
121
|
+
|
122
|
+
# Represents a local file on disk which can be served directly, or passed upstream to sendfile.
|
120
123
|
class LocalFile
|
121
124
|
def initialize(root, path)
|
122
125
|
@root = root
|
@@ -216,6 +219,10 @@ module Utopia
|
|
216
219
|
|
217
220
|
DEFAULT_CACHE_CONTROL = 'public, max-age=3600'.freeze
|
218
221
|
|
222
|
+
# Initialize the middleware with the provided options.
|
223
|
+
# @option options [String] :root The root directory to serve files from.
|
224
|
+
# @option options [Array] :types The mime-types (and file extensions) to recognize/serve.
|
225
|
+
# @option options [String] :cache_control The cache-control header to set.
|
219
226
|
def initialize(app, **options)
|
220
227
|
@app = app
|
221
228
|
@root = (options[:root] || Utopia::default_root).freeze
|
data/lib/utopia/version.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
prepend Actions
|
3
|
+
|
4
|
+
on '**' do |request, path|
|
5
|
+
@page_path = path.components[0..-2]
|
6
|
+
|
7
|
+
if @page_path.empty?
|
8
|
+
goto! "welcome/index"
|
9
|
+
end
|
10
|
+
|
11
|
+
@page_file = File.join(BASE_PATH, @page_path, "content.md")
|
12
|
+
@page_title = Trenni::Strings::to_title @page_path.last
|
13
|
+
end
|
14
|
+
|
15
|
+
def read_contents
|
16
|
+
if File.exist? @page_file
|
17
|
+
File.read(@page_file)
|
18
|
+
else
|
19
|
+
"This page is empty."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
on '**/edit' do |request, path|
|
24
|
+
puts "Editing..."
|
25
|
+
|
26
|
+
if request.post?
|
27
|
+
FileUtils.mkdir_p File.dirname(@page_file)
|
28
|
+
File.write(@page_file, request.params['content'])
|
29
|
+
goto! @page_path
|
30
|
+
else
|
31
|
+
@content = read_contents
|
32
|
+
end
|
33
|
+
|
34
|
+
path.components = ["edit"]
|
35
|
+
end
|
36
|
+
|
37
|
+
on '**/index' do |request, path|
|
38
|
+
@content = read_contents
|
39
|
+
|
40
|
+
path.components = ["index"]
|
41
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<page>
|
2
|
+
<?r response.do_not_cache! ?>
|
3
|
+
|
4
|
+
<heading>Editing #{self[:page_title].inspect}</heading>
|
5
|
+
<form action="#" method="post">
|
6
|
+
<fieldset>
|
7
|
+
<legend>Page Content</legend>
|
8
|
+
<textarea name="content">#{Strings::to_html self[:content]}</textarea>
|
9
|
+
</fieldset>
|
10
|
+
|
11
|
+
<fieldset class="footer">
|
12
|
+
<input type="submit" />
|
13
|
+
</fieldset>
|
14
|
+
</form>
|
15
|
+
</page>
|