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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -3
  3. data/README.md +142 -11
  4. data/benchmarks/string_vs_symbol.rb +12 -0
  5. data/lib/utopia/command.rb +16 -13
  6. data/lib/utopia/content.rb +1 -5
  7. data/lib/utopia/content/node.rb +9 -4
  8. data/lib/utopia/{extensions/rack.rb → content/response.rb} +33 -30
  9. data/lib/utopia/content/tag.rb +14 -17
  10. data/lib/utopia/content/transaction.rb +19 -17
  11. data/lib/utopia/controller.rb +29 -8
  12. data/lib/utopia/controller/actions.rb +148 -0
  13. data/lib/utopia/controller/base.rb +9 -49
  14. data/lib/utopia/controller/respond.rb +1 -1
  15. data/lib/utopia/controller/rewrite.rb +9 -1
  16. data/lib/utopia/controller/variables.rb +1 -0
  17. data/lib/utopia/localization.rb +4 -1
  18. data/lib/utopia/middleware.rb +0 -2
  19. data/lib/utopia/path.rb +9 -0
  20. data/lib/utopia/path/matcher.rb +0 -1
  21. data/lib/utopia/redirection.rb +3 -2
  22. data/lib/utopia/session.rb +119 -2
  23. data/lib/utopia/session/lazy_hash.rb +1 -3
  24. data/lib/utopia/setup.rb +73 -0
  25. data/lib/utopia/static.rb +9 -2
  26. data/lib/utopia/version.rb +1 -1
  27. data/setup/examples/wiki/controller.rb +41 -0
  28. data/setup/examples/wiki/edit.xnode +15 -0
  29. data/setup/examples/wiki/index.xnode +10 -0
  30. data/setup/examples/wiki/welcome/content.md +3 -0
  31. data/setup/server/config/environment.yaml +1 -0
  32. data/setup/server/git/hooks/post-receive +4 -5
  33. data/setup/site/Gemfile +5 -0
  34. data/setup/site/config.ru +2 -1
  35. data/setup/site/config/environment.rb +5 -17
  36. data/setup/site/pages/_page.xnode +4 -2
  37. data/setup/site/pages/links.yaml +1 -1
  38. data/setup/site/pages/welcome/index.xnode +33 -15
  39. data/setup/site/public/_static/site.css +72 -4
  40. data/setup/site/tasks/utopia.rake +8 -0
  41. data/spec/utopia/{rack_spec.rb → content/response_spec.rb} +12 -19
  42. data/spec/utopia/content_spec.rb +2 -3
  43. data/spec/utopia/controller/{action_spec.rb → actions_spec.rb} +18 -32
  44. data/spec/utopia/controller/middleware_spec.rb +10 -10
  45. data/spec/utopia/controller/middleware_spec/controller/controller.rb +3 -3
  46. data/spec/utopia/controller/middleware_spec/controller/nested/controller.rb +1 -1
  47. data/spec/utopia/controller/middleware_spec/redirect/controller.rb +1 -1
  48. data/spec/utopia/controller/respond_spec.rb +3 -2
  49. data/spec/utopia/controller/respond_spec/api/controller.rb +2 -2
  50. data/spec/utopia/controller/respond_spec/errors/controller.rb +1 -1
  51. data/spec/utopia/controller/rewrite_spec.rb +1 -1
  52. data/spec/utopia/controller/sequence_spec.rb +12 -16
  53. data/spec/utopia/exceptions/handler_spec/controller.rb +2 -2
  54. data/spec/utopia/performance_spec/config.ru +1 -0
  55. data/spec/utopia/session_spec.rb +34 -1
  56. data/spec/utopia/session_spec.ru +3 -3
  57. data/spec/utopia/setup_spec.rb +2 -2
  58. data/utopia.gemspec +2 -2
  59. metadata +18 -12
  60. data/lib/utopia/controller/action.rb +0 -116
  61. 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 passthrough(request, path)
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 passthrough(request, path)
127
+ def process!(request, path)
120
128
  catch_response do
121
129
  self.class.rewrite_request(self, request, path)
122
130
  end || super
@@ -20,6 +20,7 @@
20
20
 
21
21
  module Utopia
22
22
  class Controller
23
+ # Provides a stack-based instance variable lookup mechanism. It can flatten a stack of controllers into a single hash.
23
24
  class Variables
24
25
  def initialize
25
26
  @controllers = []
@@ -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
- @default_locales = @all_locales + [nil]
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
 
@@ -23,8 +23,6 @@ require 'logger'
23
23
  require_relative 'http'
24
24
  require_relative 'path'
25
25
 
26
- require_relative 'extensions/rack'
27
-
28
26
  module Utopia
29
27
  PAGES_PATH = 'pages'.freeze
30
28
 
@@ -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
@@ -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
 
@@ -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 = 'index')
127
+ def initialize(app, index: 'index')
127
128
  @app = app
128
- @index = 'index'
129
+ @index = index
129
130
 
130
131
  super(app)
131
132
  end
@@ -1,4 +1,4 @@
1
- # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
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
- module Session
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
- module Session
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)
@@ -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
@@ -27,8 +27,9 @@ require 'digest/sha1'
27
27
  require 'mime/types'
28
28
 
29
29
  module Utopia
30
- # Serve static files and include recursive name resolution using @rel@ directory entries.
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
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Utopia
22
- VERSION = "1.7.1"
22
+ VERSION = "1.8.0"
23
23
  end
@@ -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>