xat_support 1.29.3

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