utopia 1.7.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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>