xat_support 1.29.3

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 (33) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +176 -0
  3. data/README.md +33 -0
  4. data/config/locales/en.yml +85 -0
  5. data/config/locales/translations/zendesk_apps_support.yml +210 -0
  6. data/lib/xat_support.rb +30 -0
  7. data/lib/zendesk_apps_support/app_file.rb +38 -0
  8. data/lib/zendesk_apps_support/app_requirement.rb +5 -0
  9. data/lib/zendesk_apps_support/app_version.rb +64 -0
  10. data/lib/zendesk_apps_support/assets/default_styles.scss +3 -0
  11. data/lib/zendesk_apps_support/assets/default_template.html.erb +10 -0
  12. data/lib/zendesk_apps_support/assets/installed.js.erb +17 -0
  13. data/lib/zendesk_apps_support/assets/src.js.erb +39 -0
  14. data/lib/zendesk_apps_support/build_translation.rb +50 -0
  15. data/lib/zendesk_apps_support/engine.rb +9 -0
  16. data/lib/zendesk_apps_support/i18n.rb +28 -0
  17. data/lib/zendesk_apps_support/installation.rb +20 -0
  18. data/lib/zendesk_apps_support/installed.rb +22 -0
  19. data/lib/zendesk_apps_support/location.rb +29 -0
  20. data/lib/zendesk_apps_support/package.rb +339 -0
  21. data/lib/zendesk_apps_support/product.rb +15 -0
  22. data/lib/zendesk_apps_support/sass_functions.rb +20 -0
  23. data/lib/zendesk_apps_support/stylesheet_compiler.rb +23 -0
  24. data/lib/zendesk_apps_support/validations/banner.rb +33 -0
  25. data/lib/zendesk_apps_support/validations/manifest.rb +214 -0
  26. data/lib/zendesk_apps_support/validations/marketplace.rb +20 -0
  27. data/lib/zendesk_apps_support/validations/requirements.rb +105 -0
  28. data/lib/zendesk_apps_support/validations/source.rb +95 -0
  29. data/lib/zendesk_apps_support/validations/stylesheets.rb +29 -0
  30. data/lib/zendesk_apps_support/validations/templates.rb +18 -0
  31. data/lib/zendesk_apps_support/validations/translations.rb +64 -0
  32. data/lib/zendesk_apps_support/validations/validation_error.rb +101 -0
  33. metadata +229 -0
@@ -0,0 +1,15 @@
1
+ module ZendeskAppsSupport
2
+ module Product
3
+ # The product code below match the values in the database, do not change them!
4
+ PRODUCTS_AVAILABLE = [
5
+ {
6
+ code: 1,
7
+ name: 'support',
8
+ },
9
+ {
10
+ code: 2,
11
+ name: 'chat',
12
+ }
13
+ ].freeze
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ require 'sass'
2
+
3
+ module Sass::Script::Functions
4
+ module AppAssetUrl
5
+ def app_asset_url(name)
6
+ assert_type name, :String
7
+ result = %{url("#{app_asset_url_helper(name)}")}
8
+ Sass::Script::String.new(result)
9
+ end
10
+
11
+ private
12
+
13
+ def app_asset_url_helper(name)
14
+ url_builder = options[:app_asset_url_builder]
15
+ url_builder.app_asset_url(name.value)
16
+ end
17
+ end
18
+
19
+ include AppAssetUrl
20
+ end
@@ -0,0 +1,23 @@
1
+ require 'sass'
2
+
3
+ module ZendeskAppsSupport
4
+ class StylesheetCompiler
5
+ def initialize(source, app_id, url_prefix)
6
+ @source, @app_id, @url_prefix = source, app_id, url_prefix
7
+ end
8
+
9
+ def compile
10
+ Sass::Engine.new(wrapped_source, syntax: :scss, app_asset_url_builder: self).render
11
+ end
12
+
13
+ def app_asset_url(name)
14
+ "#{@url_prefix}#{name}"
15
+ end
16
+
17
+ private
18
+
19
+ def wrapped_source
20
+ ".app-#{@app_id} {#{@source}}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ require 'image_size'
2
+
3
+ module ZendeskAppsSupport
4
+ module Validations
5
+ module Banner
6
+ BANNER_WIDTH = 830
7
+ BANNER_HEIGHT = 200
8
+
9
+ class <<self
10
+ def call(package)
11
+ File.open(package.path_to('assets/banner.png'), 'rb') do |fh|
12
+ begin
13
+ image = ImageSize.new(fh)
14
+
15
+ unless image.format == :png
16
+ return [ValidationError.new('banner.invalid_format')]
17
+ end
18
+
19
+ unless (image.width == BANNER_WIDTH && image.height == BANNER_HEIGHT) ||
20
+ (image.width == 2 * BANNER_WIDTH && image.height == 2 * BANNER_HEIGHT)
21
+ return [ValidationError.new('banner.invalid_size', required_banner_width: BANNER_WIDTH,
22
+ required_banner_height: BANNER_HEIGHT)]
23
+ end
24
+ rescue
25
+ return [ValidationError.new('banner.invalid_format')]
26
+ end
27
+ end
28
+ []
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,214 @@
1
+ require 'json'
2
+ require 'uri'
3
+
4
+ module ZendeskAppsSupport
5
+ module Validations
6
+ module Manifest
7
+ REQUIRED_MANIFEST_FIELDS = %w( author defaultLocale ).freeze
8
+ OAUTH_REQUIRED_FIELDS = %w( client_id client_secret authorize_uri access_token_uri ).freeze
9
+ TYPES_AVAILABLE = %w( text password checkbox url number multiline hidden ).freeze
10
+
11
+ class <<self
12
+ def call(package)
13
+ return [ValidationError.new(:missing_manifest)] unless package.has_file?('manifest.json')
14
+
15
+ manifest = package.manifest_json
16
+
17
+ errors = []
18
+ errors << missing_keys_error(manifest)
19
+ errors << default_locale_error(manifest, package)
20
+ errors << oauth_error(manifest)
21
+ errors << parameters_error(manifest)
22
+ errors << invalid_hidden_parameter_error(manifest)
23
+ errors << invalid_type_error(manifest)
24
+ errors << name_as_parameter_name_error(manifest)
25
+
26
+ if manifest['requirementsOnly']
27
+ errors << ban_location(manifest)
28
+ errors << ban_framework_version(manifest)
29
+ else
30
+ errors << missing_location_error(package)
31
+ errors << invalid_location_error(package)
32
+ errors << duplicate_location_error(manifest)
33
+ errors << missing_framework_version(manifest)
34
+ errors << invalid_version_error(manifest, package)
35
+ end
36
+
37
+ errors.flatten.compact
38
+ rescue JSON::ParserError => e
39
+ return [ValidationError.new(:manifest_not_json, errors: e)]
40
+ end
41
+
42
+ private
43
+
44
+ def ban_location(manifest)
45
+ ValidationError.new(:no_location_required) unless manifest['location'].nil?
46
+ end
47
+
48
+ def ban_framework_version(manifest)
49
+ ValidationError.new(:no_framework_version_required) unless manifest['frameworkVersion'].nil?
50
+ end
51
+
52
+ def oauth_error(manifest)
53
+ return unless manifest['oauth']
54
+
55
+ missing = OAUTH_REQUIRED_FIELDS.select do |key|
56
+ manifest['oauth'][key].nil? || manifest['oauth'][key].empty?
57
+ end
58
+
59
+ if missing.any?
60
+ ValidationError.new('oauth_keys.missing', missing_keys: missing.join(', '), count: missing.length)
61
+ end
62
+ end
63
+
64
+ def parameters_error(manifest)
65
+ return unless manifest['parameters']
66
+
67
+ unless manifest['parameters'].is_a?(Array)
68
+ return ValidationError.new(:parameters_not_an_array)
69
+ end
70
+
71
+ para_names = manifest['parameters'].collect { |para| para['name'] }
72
+ duplicate_parameters = para_names.select { |name| para_names.count(name) > 1 }.uniq
73
+ unless duplicate_parameters.empty?
74
+ return ValidationError.new(:duplicate_parameters, duplicate_parameters: duplicate_parameters)
75
+ end
76
+ end
77
+
78
+ def missing_keys_error(manifest)
79
+ missing = REQUIRED_MANIFEST_FIELDS.select do |key|
80
+ manifest[key].nil?
81
+ end
82
+
83
+ missing_keys_validation_error(missing) if missing.any?
84
+ end
85
+
86
+ def default_locale_error(manifest, package)
87
+ default_locale = manifest['defaultLocale']
88
+ unless default_locale.nil?
89
+ if default_locale !~ Translations::VALID_LOCALE
90
+ ValidationError.new(:invalid_default_locale, defaultLocale: default_locale)
91
+ elsif package.translation_files.detect { |file| file.relative_path == "translations/#{default_locale}.json" }.nil?
92
+ ValidationError.new(:missing_translation_file, defaultLocale: default_locale)
93
+ end
94
+ end
95
+ end
96
+
97
+ def missing_location_error(package)
98
+ missing_keys_validation_error(['location']) unless package.has_location?
99
+ end
100
+
101
+ def invalid_location_error(package)
102
+ errors = []
103
+ manifest_locations = package.locations
104
+ manifest_locations.find do |host, locations|
105
+ error = if !Location.hosts.include?(host)
106
+ ValidationError.new(:invalid_host, host_name: host)
107
+ elsif (invalid_locations = locations.keys - Location.names_for(host)).any?
108
+ ValidationError.new(:invalid_location,
109
+ invalid_locations: invalid_locations.join(', '),
110
+ host_name: host,
111
+ count: invalid_locations.length)
112
+ end
113
+
114
+ # abort early for invalid host or location name
115
+ if error
116
+ errors << error
117
+ break
118
+ end
119
+
120
+ locations.values.each do |path|
121
+ errors << invalid_location_uri_error(package, path)
122
+ end
123
+ end
124
+ errors
125
+ end
126
+
127
+ def invalid_location_uri_error(package, path)
128
+ return nil if path == Package::LEGACY_URI_STUB
129
+ validation_error = ValidationError.new(:invalid_location_uri, uri: path)
130
+ uri = URI.parse(path)
131
+ unless uri.absolute? ? valid_absolute_uri?(uri) : valid_relative_uri?(package, uri)
132
+ validation_error
133
+ end
134
+ rescue URI::InvalidURIError => e
135
+ validation_error
136
+ end
137
+
138
+ def valid_absolute_uri?(uri)
139
+ uri.scheme == 'https' || uri.host == 'localhost'
140
+ end
141
+
142
+ def valid_relative_uri?(package, uri)
143
+ uri.path.start_with?('assets/') && package.has_file?(uri.path)
144
+ end
145
+
146
+ def duplicate_location_error(manifest)
147
+ locations = *manifest['location']
148
+ duplicate_locations = *locations.select { |location| locations.count(location) > 1 }.uniq
149
+
150
+ unless duplicate_locations.empty?
151
+ ValidationError.new(:duplicate_location, duplicate_locations: duplicate_locations.join(', '), count: duplicate_locations.length)
152
+ end
153
+ end
154
+
155
+ def missing_framework_version(manifest)
156
+ missing_keys_validation_error(['frameworkVersion']) if manifest['frameworkVersion'].nil?
157
+ end
158
+
159
+ def invalid_version_error(manifest, package)
160
+ valid_to_serve = AppVersion::TO_BE_SERVED
161
+ target_version = manifest['frameworkVersion']
162
+
163
+ if target_version == AppVersion::DEPRECATED
164
+ package.warnings << I18n.t('txt.apps.admin.warning.app_build.deprecated_version')
165
+ end
166
+
167
+ unless valid_to_serve.include?(target_version)
168
+ return ValidationError.new(:invalid_version, target_version: target_version, available_versions: valid_to_serve.join(', '))
169
+ end
170
+ end
171
+
172
+ def name_as_parameter_name_error(manifest)
173
+ if manifest['parameters'].is_a?(Array)
174
+ if manifest['parameters'].any? { |p| p['name'] == 'name' }
175
+ ValidationError.new(:name_as_parameter_name)
176
+ end
177
+ end
178
+ end
179
+
180
+ def invalid_hidden_parameter_error(manifest)
181
+ invalid_params = []
182
+
183
+ if manifest.key?('parameters')
184
+ invalid_params = manifest['parameters'].select { |p| p['type'] == 'hidden' && p['required'] }.map { |p| p['name'] }
185
+ end
186
+
187
+ if invalid_params.any?
188
+ ValidationError.new(:invalid_hidden_parameter, invalid_params: invalid_params.join(', '), count: invalid_params.length)
189
+ end
190
+ end
191
+
192
+ def invalid_type_error(manifest)
193
+ return unless manifest['parameters'].is_a?(Array)
194
+
195
+ invalid_types = []
196
+
197
+ manifest['parameters'].each do |parameter|
198
+ parameter_type = parameter.fetch('type', '')
199
+
200
+ invalid_types << parameter_type unless TYPES_AVAILABLE.include?(parameter_type)
201
+ end
202
+
203
+ if invalid_types.any?
204
+ ValidationError.new(:invalid_type_parameter, invalid_types: invalid_types.join(', '), count: invalid_types.length)
205
+ end
206
+ end
207
+
208
+ def missing_keys_validation_error(missing_keys)
209
+ ValidationError.new('manifest_keys.missing', missing_keys: missing_keys.join(', '), count: missing_keys.length)
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,20 @@
1
+ module ZendeskAppsSupport
2
+ module Validations
3
+ module Marketplace
4
+ class << self
5
+ def call(package)
6
+ [no_symlinks(package.root)].compact
7
+ end
8
+
9
+ private
10
+
11
+ def no_symlinks(path)
12
+ if Dir["#{path}/**/{*,.*}"].any? { |f| File.symlink?(f) }
13
+ return ValidationError.new(:symlink_in_zip)
14
+ end
15
+ nil
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,105 @@
1
+ require 'json'
2
+ require 'json/stream'
3
+
4
+ module ZendeskAppsSupport
5
+ module Validations
6
+ module Requirements
7
+ MAX_REQUIREMENTS = 5000
8
+
9
+ class <<self
10
+ def call(package)
11
+ requirements_file = package.files.find { |f| f.relative_path == 'requirements.json' }
12
+
13
+ return [ValidationError.new(:missing_requirements)] unless requirements_file
14
+
15
+ requirements_stream = requirements_file.read
16
+ duplicates = non_unique_type_keys(requirements_stream)
17
+ unless duplicates.empty?
18
+ return [ValidationError.new(:duplicate_requirements, duplicate_keys: duplicates.join(', '), count: duplicates.length)]
19
+ end
20
+
21
+ requirements = JSON.load(requirements_stream)
22
+ [].tap do |errors|
23
+ errors << invalid_requirements_types(requirements)
24
+ errors << excessive_requirements(requirements)
25
+ errors << invalid_channel_integrations(requirements)
26
+ errors << invalid_user_fields(requirements)
27
+ errors << missing_required_fields(requirements)
28
+ errors.flatten!
29
+ errors.compact!
30
+ end
31
+ rescue JSON::ParserError => e
32
+ return [ValidationError.new(:requirements_not_json, errors: e)]
33
+ end
34
+
35
+ private
36
+
37
+ def missing_required_fields(requirements)
38
+ [].tap do |errors|
39
+ requirements.each do |requirement_type, requirement|
40
+ next if requirement_type == 'channel_integrations'
41
+ requirement.each do |identifier, fields|
42
+ next if fields.include? 'title'
43
+ errors << ValidationError.new(:missing_required_fields, field: 'title', identifier: identifier)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def excessive_requirements(requirements)
50
+ requirement_count = requirements.values.map(&:values).flatten.size
51
+ if requirement_count > MAX_REQUIREMENTS
52
+ ValidationError.new(:excessive_requirements, max: MAX_REQUIREMENTS, count: requirement_count)
53
+ end
54
+ end
55
+
56
+ def invalid_user_fields(requirements)
57
+ user_fields = requirements['user_fields']
58
+ return unless user_fields
59
+ [].tap do |errors|
60
+ user_fields.each do |identifier, fields|
61
+ next if fields.include? 'key'
62
+ errors << ValidationError.new(:missing_required_fields, field: 'key', identifier: identifier)
63
+ end
64
+ end
65
+ end
66
+
67
+ def invalid_channel_integrations(requirements)
68
+ channel_integrations = requirements['channel_integrations']
69
+ return unless channel_integrations
70
+ [].tap do |errors|
71
+ if channel_integrations.size > 1
72
+ errors << ValidationError.new(:multiple_channel_integrations, count: channel_integrations.size)
73
+ end
74
+ channel_integrations.each do |identifier, fields|
75
+ unless fields.include? 'manifest_url'
76
+ errors << ValidationError.new(:missing_required_fields, field: 'manifest_url', identifier: identifier)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def invalid_requirements_types(requirements)
83
+ invalid_types = requirements.keys - ZendeskAppsSupport::AppRequirement::TYPES
84
+
85
+ unless invalid_types.empty?
86
+ ValidationError.new(:invalid_requirements_types, invalid_types: invalid_types.join(', '), count: invalid_types.length)
87
+ end
88
+ end
89
+
90
+ def non_unique_type_keys(requirements)
91
+ keys = []
92
+ duplicates = []
93
+ parser = JSON::Stream::Parser.new do
94
+ start_object { keys.push({}) }
95
+ end_object { keys.pop }
96
+ key { |k| duplicates.push(k) if keys.last.include? k; keys.last[k] = nil }
97
+ end
98
+ parser << requirements
99
+
100
+ duplicates
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,95 @@
1
+ require 'eslintrb'
2
+
3
+ module ZendeskAppsSupport
4
+ module Validations
5
+ module Source
6
+ LINTER_OPTIONS = {
7
+ rules: {
8
+ # enforcing options:
9
+ 'semi' => 2,
10
+ 'no-extra-semi' => 2,
11
+ 'no-caller' => 2,
12
+ 'no-undef' => 2,
13
+
14
+ # relaxing options:
15
+ 'no-unused-expressions' => 2,
16
+ 'no-redeclare' => 2,
17
+ 'no-eq-null' => 0,
18
+ 'comma-dangle' => 0,
19
+ 'dot-notation' => 0
20
+ },
21
+ env: {
22
+ 'browser' => true,
23
+ 'commonjs' => true
24
+ },
25
+ parserOptions: {
26
+ 'ecmaVersion': 6,
27
+ 'sourceType': 'module'
28
+ },
29
+ # predefined globals:
30
+ globals: Hash[
31
+ %w(_ Base64 services helpers moment)
32
+ .map { |x| [x, false] }]
33
+ }.freeze
34
+
35
+ ENFORCED_LINTER_OPTIONS = {
36
+ rules: {
37
+ # enforcing options:
38
+ 'no-caller' => 2
39
+ },
40
+ env: {
41
+ 'browser' => true,
42
+ 'commonjs' => true
43
+ },
44
+ # predefined globals:
45
+ globals: Hash[
46
+ %w(_ Base64 services helpers moment)
47
+ .map { |x| [x, false] }]
48
+ }.freeze
49
+
50
+ class <<self
51
+ def call(package)
52
+ files = package.js_files
53
+ app = files.find { |file| file.relative_path == 'app.js' }
54
+ eslint_config_path = "#{package.root}/.eslintrc.json"
55
+ has_eslint_config = File.exists?(eslint_config_path)
56
+ options = has_eslint_config ? JSON.parse(File.read(eslint_config_path)) : LINTER_OPTIONS
57
+
58
+ if package_needs_app_js?(package)
59
+ return [ ValidationError.new(:missing_source) ] unless app
60
+ else
61
+ return (package_has_code?(package) ? [ ValidationError.new(:no_code_for_ifo_notemplate) ] : [])
62
+ end
63
+
64
+ errors = eslint_errors(files, options)
65
+ errors << eslint_errors(files, ENFORCED_LINTER_OPTIONS) if errors.empty? && has_eslint_config
66
+ return app ? errors.flatten! : [ValidationError.new(:missing_source)]
67
+ end
68
+
69
+ private
70
+
71
+ def package_has_code?(package)
72
+ !(package.js_files.empty? && package.template_files.empty? && package.app_css.empty?)
73
+ end
74
+
75
+ def package_needs_app_js?(package)
76
+ return false if package.manifest_json['requirementsOnly']
77
+ return false if package.iframe_only?
78
+ true
79
+ end
80
+
81
+ def eslint_error(file, options)
82
+ errors = Eslintrb.lint(file.read, options)
83
+ [ESLintValidationError.new(file.relative_path, errors)] if errors.any?
84
+ end
85
+
86
+ def eslint_errors(files, options)
87
+ files.each_with_object([]) do |file, errors|
88
+ error = eslint_error(file, options)
89
+ errors << error unless error.nil?
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end