scrivito_sdk 0.17.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/app/controllers/scrivito/default_cms_controller.rb +15 -3
  4. data/app/controllers/scrivito/objs_controller.rb +61 -25
  5. data/app/controllers/scrivito/users_controller.rb +7 -0
  6. data/app/controllers/scrivito/webservice_controller.rb +17 -7
  7. data/app/controllers/scrivito/workspaces_controller.rb +85 -17
  8. data/app/helpers/scrivito/default_cms_routing_helper.rb +3 -3
  9. data/config/routes.rb +4 -1
  10. data/lib/assets/javascripts/scrivito_editing.js +4182 -508
  11. data/lib/assets/stylesheets/scrivito_editing.css +489 -13
  12. data/lib/generators/cms/migration/templates/migration.erb +4 -4
  13. data/lib/scrivito/attribute_collection.rb +1 -6
  14. data/lib/scrivito/attribute_content.rb +29 -7
  15. data/lib/scrivito/basic_obj.rb +91 -61
  16. data/lib/scrivito/basic_widget.rb +0 -11
  17. data/lib/scrivito/client_config.rb +40 -25
  18. data/lib/scrivito/cms_backend.rb +54 -0
  19. data/lib/scrivito/cms_cache_storage.rb +8 -0
  20. data/lib/scrivito/cms_field_tag.rb +2 -1
  21. data/lib/scrivito/cms_rest_api.rb +9 -0
  22. data/lib/scrivito/configuration.rb +4 -2
  23. data/lib/scrivito/content_state.rb +8 -0
  24. data/lib/scrivito/content_state_caching.rb +20 -0
  25. data/lib/scrivito/editing_context.rb +35 -34
  26. data/lib/scrivito/membership.rb +22 -3
  27. data/lib/scrivito/memberships_collection.rb +8 -4
  28. data/lib/scrivito/obj_class.rb +45 -100
  29. data/lib/scrivito/obj_class_collection.rb +53 -0
  30. data/lib/scrivito/obj_class_data.rb +33 -0
  31. data/lib/scrivito/obj_data.rb +26 -48
  32. data/lib/scrivito/obj_data_from_hash.rb +5 -5
  33. data/lib/scrivito/obj_data_from_service.rb +9 -3
  34. data/lib/scrivito/obj_search_builder.rb +0 -5
  35. data/lib/scrivito/obj_search_enumerator.rb +3 -20
  36. data/lib/scrivito/objs_collection.rb +7 -0
  37. data/lib/scrivito/restriction_set.rb +2 -2
  38. data/lib/scrivito/user.rb +89 -23
  39. data/lib/scrivito/user_definition.rb +73 -70
  40. data/lib/scrivito/workspace.rb +52 -8
  41. data/lib/scrivito/workspace/publish_checker.rb +126 -0
  42. metadata +6 -2
@@ -1,63 +1,67 @@
1
1
  module Scrivito
2
- # @api beta
3
- class UserDefinition < Struct.new(:user_id)
4
- # Allows user to publish a workspace.
5
- # @api beta
6
- PUBLISH_WORKSPACE_ABILITY = 'publish_workspace'
7
-
8
- # Allows user to do everything.
9
- # @api beta
10
- ADMINISTRATE_CMS_ABILITY = 'administrate_cms'
11
-
12
- # @api beta
13
- ABILITY_NAMES = [
14
- ADMINISTRATE_CMS_ABILITY,
15
- PUBLISH_WORKSPACE_ABILITY,
16
- ]
2
+ # @api public
3
+ class UserDefinition
4
+ def initialize(user_id)
5
+ @user_id = user_id
6
+ @explicit_rules = {}
7
+ end
17
8
 
18
- # Defines a user ability.
19
- # @api beta
20
- # @param [String, Symbol] ability_name name of the ability
21
- # @param [Proc] ability_proc proc to check if the ability can be granted
22
- # @raise [Scrivito::ScrivitoError] if the given ability name is unknown
23
- # @raise [Scrivito::ScrivitoError] if no ability proc is given
24
- # @yieldreturn If the block returns a "truthy" value, then the ability will be granted
25
- # @yieldreturn If the block returns a "falsy" value, then the ability will be prohibited
26
- # @see ABILITY_NAMES
27
- # @see Scrivito::User.define
28
- # @example
29
- # allowed_user = nil
30
- #
31
- # alice = Scrivito::User.define('alice') do |user|
32
- # user.can(:publish_workspace) { allowed_user == 'alice' }
9
+ # Adds an explicit rule, that allows the user to _always_ execute an action.
10
+ # A rule consists of a verb of the action, the subject of the action and an optional message.
11
+ # @api public
12
+ # @param [Symbol] verb the verb of the action (see {Scrivito::User::VERBS}).
13
+ # @param [Symbol] subject the subject of the action. At the moment only +:workspace+ is supported.
14
+ # @param [String] message optional message to be displayed in the UI.
15
+ # @raise [Scrivito::ScrivitoError] if the given verb is invalid
16
+ # @raise [Scrivito::ScrivitoError] if the specified rule conflicts with a rule specified with
17
+ # {Scrivito::UserDefinition#can_never}.
18
+ # @note Normally the memberships of a workspace decide whether a user is allowed or not to
19
+ # execute a specific action. This method allows to add an exception to this logic and thus
20
+ # should be used carefully.
21
+ # @see Scrivito::User::VERBS
22
+ # @see Scrivito::UserDefinition#can_never
23
+ # @example User can _always_ read, write and publish a workspace, ignoring the memberships.
24
+ # Scrivito::User.define('alice') do |user|
25
+ # user.can_always(:read, :workspace)
26
+ # user.can_always(:write, :workspace)
27
+ # user.can_always(:publish, :workspace, 'You can always publish a workspace.)
33
28
  # end
34
- #
35
- # bob = Scrivito::User.define('bob') do |user|
36
- # user.can(:publish_workspace) { allowed_user == 'bob' }
29
+ def can_always(verb, subject, message = nil)
30
+ assert_no_conflict(:can_never, verb, subject)
31
+ @explicit_rules[[:can_always, verb, subject]] = message
32
+ end
33
+
34
+ # Adds an explicit rule, that forbids the user to execute an action.
35
+ # A rule consists of a verb of the action, the subject of the action and an optional message.
36
+ # @api public
37
+ # @param [Symbol] verb the verb of the action (see {Scrivito::User::VERBS}).
38
+ # @param [Symbol] subject the subject of the action. At the moment only +:workspace+ is supported.
39
+ # @param [String] message optional message to be displayed in the UI.
40
+ # @raise [Scrivito::ScrivitoError] if the given verb is invalid
41
+ # @raise [Scrivito::ScrivitoError] if the specified rule conflicts with a rule specified with
42
+ # {Scrivito::UserDefinition#can_always}.
43
+ # @note Normally the memberships of a workspace decide whether a user is allowed or not to
44
+ # execute a specific action. This method allows to add an exception to this logic and thus
45
+ # should be used carefully.
46
+ # @see Scrivito::User::VERBS
47
+ # @see Scrivito::UserDefinition#can_always
48
+ # @example User can _never_ publish a workspace, even if she's a workspace owner.
49
+ # Scrivito::User.define('alice') do |user|
50
+ # user.can_never(:publish, :workspace, 'You can not publish workspaces.')
37
51
  # end
38
- #
39
- # # The users `alice` and `bob` will both be not allowed to publish workspace.
40
- #
41
- # allowed_user = 'alice'
42
- # # The user `alice` will now be allowed to publish workspace,
43
- # # but the user `bob` will still be not allowed.
44
- #
45
- # allowed_user = 'bob'
46
- # # The user `bob` will now be allowed to publish workspace,
47
- # # but the user `alice` will be not allowed again.
48
- def can(ability_name, &ability_proc)
49
- assert_valid_ability(ability_name, ability_proc)
50
- abilities[ability_name] = ability_proc
52
+ def can_never(verb, subject, message = nil)
53
+ assert_no_conflict(:can_always, verb, subject)
54
+ @explicit_rules[[:can_never, verb, subject]] = message
51
55
  end
52
56
 
53
57
  # Defines the user description to be displayed, when the user is shown in the in-place GUI.
54
- # @api beta
58
+ # @api public
55
59
  # @param [Proc] description_proc proc to calculate the description. Defaults to the the user id.
56
60
  # @note The description is calculated "lazy".
57
61
  # @note The calculated description will be cached.
58
62
  # @see Scrivito::User.define
59
63
  # @example
60
- # alice = Scrivito::User.define('alice') {}
64
+ # alice = Scrivito::User.define('alice')
61
65
  # # User `alice` will be displayed as "alice" in the in-place GUI.
62
66
  #
63
67
  # bob = Scrivito::User.define('bob') do |user|
@@ -70,7 +74,9 @@ module Scrivito
70
74
 
71
75
  # Defines the proc for fetching users for the user autocompletion of the in-place GUI.
72
76
  # The user autocompletion is for example used in the details dialog of a workspace.
73
- # @api beta
77
+ # If the proc is not set, then {Scrivito::User.find} will be used to fetch the suggested users with input
78
+ # as the user id.
79
+ # @api public
74
80
  # @param [Proc] suggest_users_proc proc for fetching users to be suggested in the in-place GUI
75
81
  # @yieldparam [String] input an arbitrary string from the input field of a user autocompletion,
76
82
  # e.g. the first letters of a user name
@@ -91,25 +97,25 @@ module Scrivito
91
97
  @suggest_users_proc = suggest_users_proc
92
98
  end
93
99
 
94
- # Lets you restrict the ability of a user to publish a certain object. Each
100
+ # Lets you restrict the rule of a user to publish a certain object. Each
95
101
  # registered callback can access a certain attribute of an object. Multiple
96
102
  # callbacks are possible
97
103
  #
98
- # @api beta
104
+ # @api public
99
105
  # @param [Hash] options
100
- # @option options [Symbol] :uses the attribute you need in the callback
106
+ # @option options [Symbol] :using the attribute you need in the callback
101
107
  # @yield [attribute] the value of the specified attribute
102
108
  # @yieldreturn [String, false] either return a message for the user or false if
103
109
  # no restriction is needed
104
110
  #
105
111
  # @note the callback is only called with {BasicObj Objs} that have the attribute
106
- # specified by the :uses option and if it is not a {BasicWidget Widget}-attribute
112
+ # specified by the :using option and if it is not a {BasicWidget Widget}-attribute
107
113
  #
108
114
  # @example
109
115
  # class MyUserModel
110
116
  # def to_scrivito_user
111
117
  # Scrivito::User.define(id) do |user|
112
- # user.restrict_obj_publish(uses: :_path) do |path|
118
+ # user.restrict_obj_publish(using: :_path) do |path|
113
119
  # if path.start_with?("/en")
114
120
  # false
115
121
  # else
@@ -117,11 +123,11 @@ module Scrivito
117
123
  # end
118
124
  # end
119
125
  #
120
- # user.restrict_obj_publish(uses: :_obj_class) do |obj_class|
121
- # if obj_class == "BlogPost"
126
+ # user.restrict_obj_publish(using: :_obj_class) do |obj_class|
127
+ # if obj_class.name == 'BlogPost'
122
128
  # false
123
129
  # else
124
- # "You are only allowed to edit Blog Posts"
130
+ # 'You are only allowed to edit Blog Posts'
125
131
  # end
126
132
  # end
127
133
  # end
@@ -132,8 +138,13 @@ module Scrivito
132
138
  end
133
139
 
134
140
  def user
135
- User.new(id: user_id, abilities: abilities, description_proc: @description_proc,
136
- suggest_users_proc: @suggest_users_proc, restriction_set: restriction_set)
141
+ User.new(
142
+ id: @user_id,
143
+ explicit_rules: @explicit_rules,
144
+ description_proc: @description_proc,
145
+ suggest_users_proc: @suggest_users_proc,
146
+ restriction_set: restriction_set
147
+ )
137
148
  end
138
149
 
139
150
  private
@@ -142,17 +153,9 @@ module Scrivito
142
153
  @restriction_set ||= RestrictionSet.new
143
154
  end
144
155
 
145
- def abilities
146
- @abilities ||= Hash.new(-> { false }).with_indifferent_access
147
- end
148
-
149
- def assert_valid_ability(ability_name, ability_proc)
150
- unless ABILITY_NAMES.include?(ability_name.to_s)
151
- raise ScrivitoError.new("'#{ability_name}' is not a valid ability name")
152
- end
153
-
154
- unless ability_proc
155
- raise ScrivitoError.new("No proc given for ability '#{ability_name}'")
156
+ def assert_no_conflict(type, verb, subject)
157
+ if @explicit_rules.has_key?([type, verb, subject])
158
+ raise ScrivitoError.new("Conflicting rules for verb '#{verb}' and subject '#{subject}'")
156
159
  end
157
160
  end
158
161
  end
@@ -9,6 +9,8 @@ class Workspace
9
9
 
10
10
  include ModelIdentity
11
11
 
12
+ PublishPreventedDueToContentChange = Class.new(ScrivitoError)
13
+
12
14
  # Set the currently used workspace
13
15
  # @api public
14
16
  # @param [Scrivito::Workspace] workspace
@@ -108,7 +110,17 @@ class Workspace
108
110
  # @api public
109
111
  def reload
110
112
  @workspace_data = CmsBackend.instance.find_workspace_data_by_id(self.id)
111
- @revision = @base_revision = @memberships = nil
113
+
114
+ # Clear all cached instance variables.
115
+ @base_revision = nil
116
+ @memberships = nil
117
+ @revision = nil
118
+ end
119
+
120
+ def api_request(verb, path, payload = nil)
121
+ response = CmsRestApi.public_send(verb, "#{backend_url}#{path}", payload)
122
+ reload if [:post, :put, :delete].include?(verb)
123
+ response
112
124
  end
113
125
 
114
126
  # Updates this workspace's attributes
@@ -117,8 +129,7 @@ class Workspace
117
129
  # @return [Scrivito::Workspace]
118
130
  def update(attributes)
119
131
  CmsRestApi.put(backend_url, workspace: attributes)
120
-
121
- self.reload
132
+ reload
122
133
  end
123
134
 
124
135
  # Destroy this workspace
@@ -131,17 +142,24 @@ class Workspace
131
142
  # Publish the changes of this workspace
132
143
  # @api public
133
144
  def publish
134
- CmsRestApi.put("#{backend_url}/publish", {})
145
+ internal_publish
146
+ end
135
147
 
136
- reset_workspace_if_current
148
+ def conditional_publish
149
+ internal_publish(if_content_state_id_equals: content_state.content_state_id)
150
+ rescue Scrivito::ClientError => e
151
+ if e.message.starts_with?('The specified condition for content_state_id')
152
+ raise PublishPreventedDueToContentChange.new(e.message)
153
+ else
154
+ raise e
155
+ end
137
156
  end
138
157
 
139
158
  # Rebases the current workspace on the published content
140
159
  # @api public
141
160
  def rebase
142
161
  CmsRestApi.put("#{backend_url}/rebase", {})
143
-
144
- self.reload
162
+ reload
145
163
  end
146
164
 
147
165
  # Returns the id of the workspace
@@ -162,7 +180,7 @@ class Workspace
162
180
  @workspace_data.title
163
181
  end
164
182
 
165
- # @api beta
183
+ # @api public
166
184
  # Returns the members of this workspace and their roles
167
185
  #
168
186
  # @return [MembershipsCollection]
@@ -216,12 +234,38 @@ class Workspace
216
234
  @objs ||= ObjsCollection.new(self)
217
235
  end
218
236
 
237
+ # Returns all obj classes of this working copy.
238
+ #
239
+ # @api public
240
+ #
241
+ # @example Find the obj class named "Homepage" in the "rtc" {Workspace}.
242
+ # Workspace.find('rtc').obj_classes['Homepage']
243
+ #
244
+ # @return {ObjClassCollection}
245
+ def obj_classes
246
+ @obj_classes ||= ObjClassCollection.new(self)
247
+ end
248
+
219
249
  def inspect
220
250
  "<#{self.class} id=\"#{id}\" title=\"#{title}\">"
221
251
  end
222
252
 
253
+ def as_json(options = {})
254
+ {
255
+ id: id,
256
+ title: title,
257
+ memberships: memberships,
258
+ }
259
+ end
260
+
223
261
  private
224
262
 
263
+ def internal_publish(data={})
264
+ CmsRestApi.put("#{backend_url}/publish", data)
265
+
266
+ reset_workspace_if_current
267
+ end
268
+
225
269
  def backend_url
226
270
  "/workspaces/#{id}"
227
271
  end
@@ -0,0 +1,126 @@
1
+ module Scrivito
2
+ class Workspace
3
+ class PublishChecker < Struct.new(:workspace, :user)
4
+ MAX_EXECUTION_TIME = (0.5).second
5
+
6
+ def passing_certificates?(certificates)
7
+ begin
8
+ responses = decrypt_responses(certificates).map do |response|
9
+ response.with_indifferent_access
10
+ end
11
+
12
+ return false unless responses.all? do |response|
13
+ raise_invalid_certificates_error if response[:content_state_id].nil?
14
+
15
+ response[:content_state_id] == workspace.content_state.content_state_id
16
+ end
17
+
18
+ assert_passing_result(responses)
19
+ true
20
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
21
+ raise_invalid_certificates_error
22
+ end
23
+ end
24
+
25
+ def call(offset)
26
+ batch_size = Configuration.check_batch_size
27
+ objs, changed_obj_count = fetch_objs_and_count(offset, batch_size)
28
+ passed, objs = compute_publishable(objs)
29
+
30
+ if passed
31
+ passing_result(objs, changed_obj_count, offset)
32
+ else
33
+ failing_result
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def decrypt_responses(certificates)
40
+ certificates.map(&message_verifier.method(:verify))
41
+ end
42
+
43
+ def fetch_objs_and_count(offset, batch_size)
44
+ enumerator = workspace.objs.changes(offset, batch_size)
45
+ [enumerator.take(batch_size), enumerator.size]
46
+ end
47
+
48
+ def passing_result(objs, changed_obj_count, offset)
49
+ until_element = offset + objs.size - 1
50
+ until_element = 'END' if until_element >= (changed_obj_count - 1)
51
+
52
+ pass_sub_hash = {
53
+ workspace_id: workspace.id,
54
+ content_state_id: workspace.content_state.content_state_id,
55
+ from: offset,
56
+ until: until_element,
57
+ }
58
+
59
+ { result: 'pass', pass: pass_sub_hash, certificate: sign(pass_sub_hash) }
60
+ end
61
+
62
+ def compute_publishable(objs)
63
+ start_time = Time.now
64
+ result = []
65
+
66
+ objs.each do |obj|
67
+ if publishable?(obj)
68
+ result << obj
69
+ else
70
+ return false, []
71
+ end
72
+
73
+ break if (Time.now - start_time) > MAX_EXECUTION_TIME
74
+ end
75
+
76
+ return true, result
77
+ end
78
+
79
+ def failing_result
80
+ {result: 'fail' }
81
+ end
82
+
83
+ def publishable?(obj)
84
+ obj.publishable? && user.can_publish?(obj)
85
+ end
86
+
87
+ def sign(object)
88
+ message_verifier.generate(object)
89
+ end
90
+
91
+ def message_verifier
92
+ @message_verifier ||= ActiveSupport::MessageVerifier.new(
93
+ Rails.application.secrets.secret_key_base, serializer: JSON
94
+ )
95
+ end
96
+
97
+ def assert_passing_result(responses)
98
+ sorted_responses = responses.sort_by do |response|
99
+ response[:from].to_i
100
+ end
101
+
102
+ last_until = sorted_responses.inject(-1) do |last_until, response|
103
+ if last_until == 'END'
104
+ raise_invalid_certificates_error
105
+ elsif (last_until + 1) == response[:from].to_i
106
+ if response[:until] == 'END'
107
+ 'END'
108
+ else
109
+ response[:until].to_i
110
+ end
111
+ else
112
+ raise_invalid_certificates_error
113
+ end
114
+ end
115
+
116
+ if last_until != 'END'
117
+ raise_invalid_certificates_error
118
+ end
119
+ end
120
+
121
+ def raise_invalid_certificates_error
122
+ raise ScrivitoError, 'Invalid certificates'
123
+ end
124
+ end
125
+ end
126
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scrivito_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Infopark AG
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-22 00:00:00.000000000 Z
11
+ date: 2014-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -149,6 +149,7 @@ files:
149
149
  - app/controllers/scrivito/default_cms_controller.rb
150
150
  - app/controllers/scrivito/objs_controller.rb
151
151
  - app/controllers/scrivito/tasks_controller.rb
152
+ - app/controllers/scrivito/users_controller.rb
152
153
  - app/controllers/scrivito/webservice_controller.rb
153
154
  - app/controllers/scrivito/workspaces_controller.rb
154
155
  - app/helpers/cms_helper.rb
@@ -254,6 +255,8 @@ files:
254
255
  - lib/scrivito/named_link.rb
255
256
  - lib/scrivito/network_error.rb
256
257
  - lib/scrivito/obj_class.rb
258
+ - lib/scrivito/obj_class_collection.rb
259
+ - lib/scrivito/obj_class_data.rb
257
260
  - lib/scrivito/obj_data.rb
258
261
  - lib/scrivito/obj_data_from_hash.rb
259
262
  - lib/scrivito/obj_data_from_service.rb
@@ -273,6 +276,7 @@ files:
273
276
  - lib/scrivito/widget_field_params.rb
274
277
  - lib/scrivito/widget_garbage_collection.rb
275
278
  - lib/scrivito/workspace.rb
279
+ - lib/scrivito/workspace/publish_checker.rb
276
280
  - lib/scrivito/workspace_data_from_service.rb
277
281
  - lib/scrivito/workspace_selection_middleware.rb
278
282
  - lib/scrivito_sdk.rb