zendesk_apps_support 4.29.4

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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +176 -0
  3. data/README.md +39 -0
  4. data/config/locales/en.yml +163 -0
  5. data/config/locales/translations/zendesk_apps_support.yml +405 -0
  6. data/lib/zendesk_apps_support.rb +42 -0
  7. data/lib/zendesk_apps_support/app_file.rb +44 -0
  8. data/lib/zendesk_apps_support/app_requirement.rb +11 -0
  9. data/lib/zendesk_apps_support/app_version.rb +78 -0
  10. data/lib/zendesk_apps_support/assets/default_app_logo.svg +10 -0
  11. data/lib/zendesk_apps_support/assets/default_styles.scss +3 -0
  12. data/lib/zendesk_apps_support/assets/default_template.html.erb +10 -0
  13. data/lib/zendesk_apps_support/assets/installed.js.erb +21 -0
  14. data/lib/zendesk_apps_support/assets/src.js.erb +37 -0
  15. data/lib/zendesk_apps_support/build_translation.rb +51 -0
  16. data/lib/zendesk_apps_support/engine.rb +11 -0
  17. data/lib/zendesk_apps_support/finders.rb +36 -0
  18. data/lib/zendesk_apps_support/i18n.rb +31 -0
  19. data/lib/zendesk_apps_support/installation.rb +22 -0
  20. data/lib/zendesk_apps_support/installed.rb +27 -0
  21. data/lib/zendesk_apps_support/location.rb +71 -0
  22. data/lib/zendesk_apps_support/manifest.rb +197 -0
  23. data/lib/zendesk_apps_support/manifest/location_options.rb +35 -0
  24. data/lib/zendesk_apps_support/manifest/no_override_hash.rb +50 -0
  25. data/lib/zendesk_apps_support/manifest/parameter.rb +23 -0
  26. data/lib/zendesk_apps_support/package.rb +402 -0
  27. data/lib/zendesk_apps_support/product.rb +31 -0
  28. data/lib/zendesk_apps_support/sass_functions.rb +43 -0
  29. data/lib/zendesk_apps_support/stylesheet_compiler.rb +38 -0
  30. data/lib/zendesk_apps_support/validations/banner.rb +35 -0
  31. data/lib/zendesk_apps_support/validations/manifest.rb +412 -0
  32. data/lib/zendesk_apps_support/validations/marketplace.rb +31 -0
  33. data/lib/zendesk_apps_support/validations/mime.rb +41 -0
  34. data/lib/zendesk_apps_support/validations/requests.rb +47 -0
  35. data/lib/zendesk_apps_support/validations/requirements.rb +154 -0
  36. data/lib/zendesk_apps_support/validations/secrets.rb +77 -0
  37. data/lib/zendesk_apps_support/validations/secure_settings.rb +37 -0
  38. data/lib/zendesk_apps_support/validations/source.rb +25 -0
  39. data/lib/zendesk_apps_support/validations/stylesheets.rb +28 -0
  40. data/lib/zendesk_apps_support/validations/svg.rb +81 -0
  41. data/lib/zendesk_apps_support/validations/templates.rb +20 -0
  42. data/lib/zendesk_apps_support/validations/translations.rb +160 -0
  43. data/lib/zendesk_apps_support/validations/validation_error.rb +77 -0
  44. metadata +327 -0
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ class Product
5
+ extend ZendeskAppsSupport::Finders
6
+ attr_reader :code, :name, :legacy_name
7
+
8
+ def initialize(attrs)
9
+ @code = attrs.fetch(:code)
10
+ @name = attrs.fetch(:name)
11
+ @legacy_name = attrs.fetch(:legacy_name)
12
+ end
13
+
14
+ def self.all
15
+ PRODUCTS_AVAILABLE
16
+ end
17
+
18
+ # The product codes below match the values in the database, do not change them!
19
+ PRODUCTS_AVAILABLE = [
20
+ Product.new(code: 1, name: 'support', legacy_name: 'zendesk'),
21
+ Product.new(code: 2, name: 'chat', legacy_name: 'zopim'),
22
+ Product.new(code: 3, name: 'standalone_chat', legacy_name: 'lotus_box'),
23
+ Product.new(code: 4, name: 'sell', legacy_name: 'sell')
24
+ ].freeze
25
+
26
+ SUPPORT = find_by!(name: 'support')
27
+ CHAT = find_by!(name: 'chat')
28
+ STANDALONE_CHAT = find_by!(name: 'standalone_chat')
29
+ SELL = find_by!(name: 'sell')
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sass'
4
+
5
+ module Sass::Script::Functions
6
+ module AppAssetUrl
7
+ def app_asset_url(name)
8
+ assert_type name, :String
9
+ result = %{url("#{app_asset_url_helper(name)}")}
10
+ Sass::Script::String.new(result)
11
+ end
12
+
13
+ private
14
+
15
+ def app_asset_url_helper(name)
16
+ url_builder = options[:app_asset_url_builder]
17
+ url_builder.app_asset_url(name.value)
18
+ end
19
+ end
20
+
21
+ include AppAssetUrl
22
+ end
23
+
24
+ require 'sassc'
25
+
26
+ module SassC::Script::Functions
27
+ module AppAssetUrl
28
+ def app_asset_url(name)
29
+ raise ArgumentError, "Expected #{name} to be a string" unless name.is_a? Sass::Script::Value::String
30
+ result = %{url("#{app_asset_url_helper(name)}")}
31
+ SassC::Script::String.new(result)
32
+ end
33
+
34
+ private
35
+
36
+ def app_asset_url_helper(name)
37
+ url_builder = options[:app_asset_url_builder]
38
+ url_builder.app_asset_url(name.value)
39
+ end
40
+ end
41
+
42
+ include AppAssetUrl
43
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sass'
4
+ require 'sassc'
5
+ require 'zendesk_apps_support/sass_functions'
6
+
7
+ module ZendeskAppsSupport
8
+ class StylesheetCompiler
9
+ def initialize(source, app_id, url_prefix)
10
+ @source = source
11
+ @app_id = app_id
12
+ @url_prefix = url_prefix
13
+ end
14
+
15
+ def compile(sassc: false)
16
+ options = {
17
+ syntax: :scss, app_asset_url_builder: self
18
+ }
19
+ if sassc
20
+ compiler_class = SassC
21
+ options[:style] = :compressed
22
+ else
23
+ compiler_class = Sass
24
+ end
25
+ compiler_class::Engine.new(wrapped_source.dup, options).render
26
+ end
27
+
28
+ def app_asset_url(name)
29
+ "#{@url_prefix}#{name}"
30
+ end
31
+
32
+ private
33
+
34
+ def wrapped_source
35
+ ".app-#{@app_id} {#{@source}}"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_size'
4
+
5
+ module ZendeskAppsSupport
6
+ module Validations
7
+ module Banner
8
+ BANNER_WIDTH = 830
9
+ BANNER_HEIGHT = 200
10
+
11
+ class << self
12
+ def call(package)
13
+ File.open(package.path_to('assets/banner.png'), 'rb') do |fh|
14
+ begin
15
+ image = ImageSize.new(fh)
16
+
17
+ unless image.format == :png
18
+ return [ValidationError.new('banner.invalid_format')]
19
+ end
20
+
21
+ unless (image.width == BANNER_WIDTH && image.height == BANNER_HEIGHT) ||
22
+ (image.width == 2 * BANNER_WIDTH && image.height == 2 * BANNER_HEIGHT)
23
+ return [ValidationError.new('banner.invalid_size', required_banner_width: BANNER_WIDTH,
24
+ required_banner_height: BANNER_HEIGHT)]
25
+ end
26
+ rescue
27
+ return [ValidationError.new('banner.invalid_format')]
28
+ end
29
+ end
30
+ []
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,412 @@
1
+ # rubocop:disable ModuleLength
2
+ # frozen_string_literal: true
3
+
4
+ require 'uri'
5
+
6
+ module ZendeskAppsSupport
7
+ module Validations
8
+ module Manifest
9
+ RUBY_TO_JSON = ZendeskAppsSupport::Manifest::RUBY_TO_JSON
10
+ REQUIRED_MANIFEST_FIELDS = RUBY_TO_JSON.select { |k| %i[author default_locale].include? k }.freeze
11
+ OAUTH_REQUIRED_FIELDS = %w[client_id client_secret authorize_uri access_token_uri].freeze
12
+ PARAMETER_TYPES = ZendeskAppsSupport::Manifest::Parameter::TYPES
13
+ OAUTH_MANIFEST_LINK = 'https://developer.zendesk.com/apps/docs/developer-guide/manifest#oauth'
14
+
15
+ class << self
16
+ def call(package)
17
+ unless package.has_file?('manifest.json')
18
+ nested_manifest = package.files.find { |file| file =~ %r{\A[^/]+?/manifest\.json\Z} }
19
+ if nested_manifest
20
+ return [ValidationError.new(:nested_manifest, found_path: nested_manifest.relative_path)]
21
+ end
22
+ return [ValidationError.new(:missing_manifest)]
23
+ end
24
+
25
+ collate_manifest_errors(package)
26
+ rescue JSON::ParserError => e
27
+ return [ValidationError.new(:manifest_not_json, errors: e)]
28
+ rescue ZendeskAppsSupport::Manifest::OverrideError => e
29
+ return [ValidationError.new(:duplicate_manifest_keys, errors: e.message)]
30
+ end
31
+
32
+ private
33
+
34
+ def collate_manifest_errors(package)
35
+ manifest = package.manifest
36
+
37
+ errors = [
38
+ missing_keys_error(manifest),
39
+ type_checks(manifest),
40
+ oauth_error(manifest),
41
+ default_locale_error(manifest, package),
42
+ validate_parameters(manifest),
43
+ if manifest.requirements_only? || manifest.marketing_only?
44
+ [ ban_location(manifest),
45
+ ban_framework_version(manifest) ]
46
+ else
47
+ [ validate_location(package),
48
+ missing_framework_version(manifest),
49
+ invalid_version_error(manifest) ]
50
+ end,
51
+ ban_no_template(manifest)
52
+ ]
53
+ errors.flatten.compact
54
+ end
55
+
56
+ def validate_location(package)
57
+ manifest = package.manifest
58
+ [
59
+ missing_location_error(package),
60
+ invalid_location_error(package),
61
+ invalid_v1_location(package),
62
+ location_framework_mismatch(manifest)
63
+ ]
64
+ end
65
+
66
+ def validate_parameters(manifest)
67
+ if manifest.marketing_only?
68
+ marketing_only_errors(manifest)
69
+ else
70
+ [
71
+ parameters_error(manifest),
72
+ invalid_hidden_parameter_error(manifest),
73
+ invalid_type_error(manifest),
74
+ too_many_oauth_parameters(manifest),
75
+ oauth_cannot_be_secure(manifest),
76
+ name_as_parameter_name_error(manifest)
77
+ ]
78
+ end
79
+ end
80
+
81
+ def oauth_cannot_be_secure(manifest)
82
+ manifest.parameters.map do |parameter|
83
+ if parameter.type == 'oauth' && parameter.secure
84
+ return ValidationError.new('oauth_parameter_cannot_be_secure')
85
+ end
86
+ end
87
+ end
88
+
89
+ def marketing_only_errors(manifest)
90
+ [
91
+ ban_parameters(manifest),
92
+ private_marketing_app_error(manifest)
93
+ ]
94
+ end
95
+
96
+ def type_checks(manifest)
97
+ errors = [
98
+ boolean_error(manifest),
99
+ string_error(manifest),
100
+ no_template_format_error(manifest)
101
+ ]
102
+ unless manifest.experiments.is_a?(Hash)
103
+ errors << ValidationError.new(
104
+ :unacceptable_hash,
105
+ field: 'experiments',
106
+ value: manifest.experiments.class.to_s
107
+ )
108
+ end
109
+ whitelist = manifest.domain_whitelist
110
+ unless whitelist.nil? || whitelist.is_a?(Array) && whitelist.all? { |dom| dom.is_a? String }
111
+ errors << ValidationError.new(:unacceptable_array_of_strings, field: 'domainWhitelist')
112
+ end
113
+ parameters = manifest.original_parameters
114
+ unless parameters.nil? || parameters.is_a?(Array)
115
+ errors << ValidationError.new(
116
+ :unacceptable_array,
117
+ field: 'parameters',
118
+ value: parameters.class.to_s
119
+ )
120
+ end
121
+ errors
122
+ end
123
+
124
+ def string_error(manifest)
125
+ manifest_strings = %i[
126
+ default_locale
127
+ version
128
+ framework_version
129
+ remote_installation_url
130
+ terms_conditions_url
131
+ google_analytics_code
132
+ ]
133
+ errors = manifest_strings.map do |field|
134
+ validate_string(manifest.public_send(field), field)
135
+ end
136
+
137
+ if manifest.author
138
+ author_strings = %w[name email url]
139
+ errors << (author_strings.map do |field|
140
+ validate_string(manifest.author[field], "author #{field}")
141
+ end)
142
+ end
143
+ errors
144
+ end
145
+
146
+ def boolean_error(manifest)
147
+ booleans = %i[requirements_only marketing_only single_install signed_urls private]
148
+ errors = []
149
+ RUBY_TO_JSON.each do |ruby, json|
150
+ if booleans.include? ruby
151
+ errors << validate_boolean(manifest.public_send(ruby), json)
152
+ end
153
+ end
154
+ errors.compact
155
+ end
156
+
157
+ def validate_string(value, label_for_error)
158
+ unless value.is_a?(String) || value.nil?
159
+ ValidationError.new(:unacceptable_string, field: label_for_error, value: value)
160
+ end
161
+ end
162
+
163
+ def validate_boolean(value, label_for_error)
164
+ unless [true, false].include? value
165
+ ValidationError.new(:unacceptable_boolean, field: label_for_error, value: value)
166
+ end
167
+ end
168
+
169
+ def check_errors(error_types, collector, *checked_objects)
170
+ error_types.each do |error_type|
171
+ collector << send(error_type, *checked_objects)
172
+ end
173
+ end
174
+
175
+ def ban_no_template(manifest)
176
+ return unless manifest.iframe_only?
177
+ no_template_migration_link = 'https://developer.zendesk.com/apps/docs/apps-v2/manifest#location'
178
+ if manifest.no_template? || !manifest.no_template_locations.empty?
179
+ ValidationError.new(:no_template_deprecated_in_v2, link: no_template_migration_link)
180
+ end
181
+ end
182
+
183
+ def ban_parameters(manifest)
184
+ ValidationError.new(:no_parameters_required) unless manifest.parameters.empty?
185
+ end
186
+
187
+ def ban_location(manifest)
188
+ ValidationError.new(:no_location_required) unless manifest.location_options.empty?
189
+ end
190
+
191
+ def ban_framework_version(manifest)
192
+ ValidationError.new(:no_framework_version_required) unless manifest.framework_version.nil?
193
+ end
194
+
195
+ def private_marketing_app_error(manifest)
196
+ ValidationError.new(:marketing_only_app_cant_be_private) if manifest.private?
197
+ end
198
+
199
+ def oauth_error(manifest)
200
+ return unless manifest.oauth
201
+ oauth_errors = []
202
+ missing = OAUTH_REQUIRED_FIELDS.select do |key|
203
+ manifest.oauth[key].nil? || manifest.oauth[key].empty?
204
+ end
205
+
206
+ if missing.any?
207
+ oauth_errors << \
208
+ ValidationError.new('oauth_keys.missing', missing_keys: missing.join(', '), count: missing.length)
209
+ end
210
+
211
+ unless manifest.parameters.any? { |param| param.type == 'oauth' }
212
+ oauth_errors << ValidationError.new('oauth_parameter_required',
213
+ link: OAUTH_MANIFEST_LINK)
214
+ end
215
+ oauth_errors
216
+ end
217
+
218
+ def parameters_error(manifest)
219
+ original = manifest.original_parameters
220
+ unless original.nil? || original.is_a?(Array)
221
+ return ValidationError.new(:parameters_not_an_array)
222
+ end
223
+
224
+ return unless manifest.parameters.any?
225
+
226
+ para_names = manifest.parameters.map(&:name)
227
+ duplicate_parameters = para_names.select { |name| para_names.count(name) > 1 }.uniq
228
+ unless duplicate_parameters.empty?
229
+ return ValidationError.new(:duplicate_parameters, duplicate_parameters: duplicate_parameters)
230
+ end
231
+
232
+ invalid_required = manifest.parameters.map do |parameter|
233
+ validate_boolean(parameter.required, "parameters.#{parameter.name}.required")
234
+ end.compact
235
+ return invalid_required if invalid_required.any?
236
+
237
+ invalid_secure = manifest.parameters.map do |parameter|
238
+ validate_boolean(parameter.secure, "parameters.#{parameter.name}.secure")
239
+ end.compact
240
+ return invalid_secure if invalid_secure.any?
241
+ end
242
+
243
+ def missing_keys_error(manifest)
244
+ missing = REQUIRED_MANIFEST_FIELDS.map do |ruby_method, manifest_value|
245
+ manifest_value if manifest.public_send(ruby_method).nil?
246
+ end.compact
247
+
248
+ missing_keys_validation_error(missing) if missing.any?
249
+ end
250
+
251
+ def default_locale_error(manifest, package)
252
+ default_locale = manifest.default_locale
253
+ unless default_locale.nil?
254
+ if default_locale !~ Translations::VALID_LOCALE
255
+ ValidationError.new(:invalid_default_locale, default_locale: default_locale)
256
+ elsif package.translation_files.detect { |f| f.relative_path == "translations/#{default_locale}.json" }.nil?
257
+ ValidationError.new(:missing_translation_file, default_locale: default_locale)
258
+ end
259
+ end
260
+ end
261
+
262
+ def missing_location_error(package)
263
+ missing_keys_validation_error(['location']) if package.manifest.location_options.empty?
264
+ end
265
+
266
+ def invalid_location_error(package)
267
+ errors = []
268
+ package.manifest.location_options.each do |location_options|
269
+ if location_options.url.is_a?(String) && !location_options.url.empty?
270
+ errors << invalid_location_uri_error(package, location_options)
271
+ elsif location_options.auto_load?
272
+ errors << ValidationError.new(:blank_location_uri, location: location_options.location.name)
273
+ end
274
+ end
275
+
276
+ Product::PRODUCTS_AVAILABLE.each do |product|
277
+ invalid_locations = package.manifest.unknown_locations(product.name)
278
+ next if invalid_locations.empty?
279
+ errors << ValidationError.new(:invalid_location,
280
+ invalid_locations: invalid_locations.join(', '),
281
+ host_name: product.name.capitalize,
282
+ count: invalid_locations.length)
283
+ end
284
+
285
+ package.manifest.unknown_hosts.each do |unknown_host|
286
+ errors << ValidationError.new(:invalid_host, host_name: unknown_host)
287
+ end
288
+
289
+ errors
290
+ end
291
+
292
+ def invalid_v1_location(package)
293
+ return unless package.manifest.framework_version &&
294
+ Gem::Version.new(package.manifest.framework_version) < Gem::Version.new('2')
295
+
296
+ invalid_locations = package.manifest.location_options
297
+ .map(&:location)
298
+ .compact
299
+ .select(&:v2_only)
300
+ .map(&:name)
301
+
302
+ unless invalid_locations.empty?
303
+ return ValidationError.new(:invalid_v1_location,
304
+ invalid_locations: invalid_locations.join(', '),
305
+ count: invalid_locations.length)
306
+ end
307
+ end
308
+
309
+ def invalid_location_uri_error(package, location_options)
310
+ path = location_options.url
311
+ return nil if path == ZendeskAppsSupport::Manifest::LEGACY_URI_STUB
312
+ return nil if path.include?('{{setting.')
313
+ validation_error = ValidationError.new(:invalid_location_uri, uri: path)
314
+ uri = URI.parse(path)
315
+ unless uri.absolute? ? valid_absolute_uri?(uri) : valid_relative_uri?(package, uri)
316
+ validation_error
317
+ end
318
+ rescue URI::InvalidURIError
319
+ validation_error
320
+ end
321
+
322
+ def valid_absolute_uri?(uri)
323
+ uri.scheme == 'https' || uri.host == 'localhost'
324
+ end
325
+
326
+ def valid_relative_uri?(package, uri)
327
+ uri.path.start_with?('assets/') && package.has_file?(uri.path)
328
+ end
329
+
330
+ def missing_framework_version(manifest)
331
+ missing_keys_validation_error([RUBY_TO_JSON[:framework_version]]) if manifest.framework_version.nil?
332
+ end
333
+
334
+ def invalid_version_error(manifest)
335
+ valid_to_serve = AppVersion::TO_BE_SERVED
336
+ target_version = manifest.framework_version
337
+
338
+ unless valid_to_serve.include?(target_version)
339
+ return ValidationError.new(:invalid_version,
340
+ target_version: target_version,
341
+ available_versions: valid_to_serve.join(', '))
342
+ end
343
+ end
344
+
345
+ def name_as_parameter_name_error(manifest)
346
+ if manifest.parameters.any? { |p| p.name == 'name' }
347
+ ValidationError.new(:name_as_parameter_name)
348
+ end
349
+ end
350
+
351
+ def invalid_hidden_parameter_error(manifest)
352
+ invalid_params = manifest.parameters.select { |p| p.type == 'hidden' && p.required }.map(&:name)
353
+
354
+ if invalid_params.any?
355
+ ValidationError.new(:invalid_hidden_parameter,
356
+ invalid_params: invalid_params.join(', '),
357
+ count: invalid_params.length)
358
+ end
359
+ end
360
+
361
+ def invalid_type_error(manifest)
362
+ invalid_types = []
363
+ manifest.parameters.each do |parameter|
364
+ parameter_type = parameter.type
365
+
366
+ invalid_types << parameter_type unless PARAMETER_TYPES.include?(parameter_type)
367
+ end
368
+
369
+ if invalid_types.any?
370
+ ValidationError.new(:invalid_type_parameter,
371
+ invalid_types: invalid_types.join(', '),
372
+ count: invalid_types.length)
373
+ end
374
+ end
375
+
376
+ def too_many_oauth_parameters(manifest)
377
+ oauth_parameters = manifest.parameters.select do |parameter|
378
+ parameter.type == 'oauth'
379
+ end
380
+
381
+ if oauth_parameters.count > 1
382
+ ValidationError.new(:too_many_oauth_parameters)
383
+ end
384
+ end
385
+
386
+ def missing_keys_validation_error(missing_keys)
387
+ ValidationError.new('manifest_keys.missing',
388
+ missing_keys: missing_keys.join(', '),
389
+ count: missing_keys.length)
390
+ end
391
+
392
+ def location_framework_mismatch(manifest)
393
+ legacy_locations, iframe_locations = manifest.location_options.partition(&:legacy?)
394
+ if manifest.iframe_only?
395
+ return ValidationError.new(:locations_must_be_urls) unless legacy_locations.empty?
396
+ elsif !iframe_locations.empty?
397
+ return ValidationError.new(:locations_cant_be_urls)
398
+ end
399
+ end
400
+
401
+ # TODO: check the app actually runs in included locations
402
+ def no_template_format_error(manifest)
403
+ no_template = manifest.no_template
404
+ return if [false, true].include? no_template
405
+ unless no_template.is_a?(Array) && manifest.no_template_locations.all? { |loc| Location.find_by(name: loc) }
406
+ ValidationError.new(:invalid_no_template)
407
+ end
408
+ end
409
+ end
410
+ end
411
+ end
412
+ end