scrivito_sdk 0.17.0 → 0.18.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 (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