zendesk_apps_support 4.29.4

Sign up to get free protection for your applications and to get access to all the features.
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,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ class Location
5
+ extend ZendeskAppsSupport::Finders
6
+ attr_reader :id, :name, :orderable, :collapsible, :visible, :product_code, :v2_only
7
+
8
+ def self.unique_ids
9
+ @ids ||= Set.new
10
+ end
11
+
12
+ def initialize(attrs)
13
+ @id = attrs.fetch(:id)
14
+ raise 'Duplicate id' if Location.unique_ids.include? @id
15
+ Location.unique_ids.add @id
16
+ @name = attrs.fetch(:name)
17
+ @orderable = attrs.fetch(:orderable, false)
18
+ @collapsible = attrs.fetch(:collapsible, false)
19
+ @visible = attrs.fetch(:visible, false)
20
+ @product_code = attrs.fetch(:product_code)
21
+ @v2_only = attrs.fetch(:v2_only, product != Product::SUPPORT)
22
+ end
23
+
24
+ def product
25
+ Product.find_by(code: product_code)
26
+ end
27
+
28
+ def self.all
29
+ LOCATIONS_AVAILABLE
30
+ end
31
+
32
+ # the ids below match the enum values on the database, do not change them!
33
+ LOCATIONS_AVAILABLE = [
34
+ Location.new(id: 1, orderable: true, name: 'top_bar',
35
+ product_code: Product::SUPPORT.code, visible: true),
36
+ Location.new(id: 2, orderable: true, name: 'nav_bar',
37
+ product_code: Product::SUPPORT.code, visible: true),
38
+ Location.new(id: 3, orderable: true, collapsible: true, name: 'ticket_sidebar',
39
+ product_code: Product::SUPPORT.code, visible: true),
40
+ Location.new(id: 4, orderable: true, collapsible: true, name: 'new_ticket_sidebar',
41
+ product_code: Product::SUPPORT.code, visible: true),
42
+ Location.new(id: 5, orderable: true, collapsible: true, name: 'user_sidebar',
43
+ product_code: Product::SUPPORT.code, visible: true),
44
+ Location.new(id: 6, orderable: true, collapsible: true, name: 'organization_sidebar',
45
+ product_code: Product::SUPPORT.code, visible: true),
46
+ Location.new(id: 7, name: 'background', product_code: Product::SUPPORT.code),
47
+ Location.new(id: 8, orderable: true, collapsible: true, name: 'chat_sidebar',
48
+ product_code: Product::CHAT.code, visible: true),
49
+ Location.new(id: 9, name: 'modal', product_code: Product::SUPPORT.code, v2_only: true),
50
+ Location.new(id: 10, name: 'ticket_editor', product_code: Product::SUPPORT.code, v2_only: true, visible: true),
51
+ Location.new(id: 11, name: 'nav_bar', product_code: Product::STANDALONE_CHAT.code, v2_only: false, visible: true),
52
+ Location.new(id: 12, name: 'system_top_bar', product_code: Product::SUPPORT.code),
53
+ Location.new(id: 13, name: 'system_top_bar',
54
+ product_code: Product::STANDALONE_CHAT.code, v2_only: false),
55
+ Location.new(id: 14, name: 'background',
56
+ product_code: Product::CHAT.code),
57
+ Location.new(id: 15, name: 'deal_card', product_code: Product::SELL.code, collapsible: true, visible: true),
58
+ Location.new(id: 16, name: 'person_card', product_code: Product::SELL.code, collapsible: true, visible: true),
59
+ Location.new(id: 17, name: 'company_card', product_code: Product::SELL.code, collapsible: true, visible: true),
60
+ Location.new(id: 18, name: 'lead_card', product_code: Product::SELL.code, collapsible: true, visible: true),
61
+ Location.new(id: 19, name: 'background', product_code: Product::SELL.code),
62
+ Location.new(id: 20, name: 'modal', product_code: Product::SELL.code),
63
+ Location.new(id: 21, name: 'dashboard', product_code: Product::SELL.code, visible: true),
64
+ Location.new(id: 22, name: 'note_editor', product_code: Product::SELL.code, visible: true),
65
+ Location.new(id: 23, name: 'call_log_editor', product_code: Product::SELL.code, visible: true),
66
+ Location.new(id: 24, name: 'email_editor', product_code: Product::SELL.code, visible: true),
67
+ Location.new(id: 25, name: 'top_bar', product_code: Product::SELL.code, visible: true),
68
+ Location.new(id: 26, name: 'visit_editor', product_code: Product::SELL.code, visible: true)
69
+ ].freeze
70
+ end
71
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ class Manifest
5
+ LEGACY_URI_STUB = '_legacy'
6
+
7
+ RUBY_TO_JSON = {
8
+ requirements_only: 'requirementsOnly',
9
+ marketing_only: 'marketingOnly',
10
+ version: 'version',
11
+ author: 'author',
12
+ name: 'name', # currently only used in ZAT
13
+ experiments: 'experiments',
14
+ framework_version: 'frameworkVersion',
15
+ single_install: 'singleInstall',
16
+ signed_urls: 'signedUrls',
17
+ no_template: 'noTemplate',
18
+ default_locale: 'defaultLocale',
19
+ original_locations: 'location',
20
+ private: 'private',
21
+ oauth: 'oauth',
22
+ original_parameters: 'parameters',
23
+ domain_whitelist: 'domainWhitelist',
24
+ remote_installation_url: 'remoteInstallationURL',
25
+ terms_conditions_url: 'termsConditionsURL',
26
+ google_analytics_code: 'gaID'
27
+ }.freeze
28
+
29
+ attr_reader(*RUBY_TO_JSON.keys)
30
+
31
+ alias_method :requirements_only?, :requirements_only
32
+ alias_method :marketing_only?, :marketing_only
33
+ alias_method :signed_urls?, :signed_urls
34
+ alias_method :single_install?, :single_install
35
+ alias_method :private?, :private
36
+
37
+ def no_template?
38
+ if no_template.is_a?(Array)
39
+ false
40
+ else
41
+ no_template
42
+ end
43
+ end
44
+
45
+ def no_template_locations
46
+ no_template || []
47
+ end
48
+
49
+ def products
50
+ @products ||=
51
+ if requirements_only?
52
+ [ Product::SUPPORT ]
53
+ elsif marketing_only?
54
+ products_ignore_locations || [ Product::SUPPORT ]
55
+ else
56
+ products_from_locations
57
+ end
58
+ end
59
+
60
+ def products_ignore_locations
61
+ locations.keys.map do |product_name|
62
+ Product.find_by(name: product_name)
63
+ end
64
+ end
65
+
66
+ def location_options
67
+ @location_options ||= locations.flat_map do |product_key, locations|
68
+ product = Product.find_by(name: product_key)
69
+ locations.map do |location_key, location_options|
70
+ location = product && Location.find_by(product_code: product.code, name: location_key)
71
+ options_with_defaults = {
72
+ 'signed' => signed_urls?
73
+ }.merge(location_options)
74
+ Manifest::LocationOptions.new(location, options_with_defaults)
75
+ end
76
+ end
77
+ end
78
+
79
+ def app_class_properties
80
+ {
81
+ experiments: experiments,
82
+ location: locations,
83
+ noTemplate: no_template_locations,
84
+ singleInstall: single_install?,
85
+ signedUrls: signed_urls?
86
+ }.reject { |_k, v| v.nil? }
87
+ end
88
+
89
+ def unknown_locations(host)
90
+ product = Product.find_by(name: host)
91
+
92
+ if locations.key?(host)
93
+ product_locations = Location.where(product_code: product.code)
94
+ locations[host].keys.uniq - product_locations.map(&:name)
95
+ else
96
+ []
97
+ end
98
+ end
99
+
100
+ def unknown_hosts
101
+ @unknown_hosts ||=
102
+ @used_hosts - Product::PRODUCTS_AVAILABLE.flat_map { |p| [p.name, p.legacy_name] }
103
+ end
104
+
105
+ def iframe_only?
106
+ framework_version && Gem::Version.new(framework_version) >= Gem::Version.new('2')
107
+ end
108
+
109
+ def parameters
110
+ @parameters ||= begin
111
+ parameter_array = @original_parameters.is_a?(Array) ? @original_parameters : []
112
+ parameter_array.map do |parameter_hash|
113
+ Parameter.new(parameter_hash)
114
+ end
115
+ end
116
+ end
117
+
118
+ def enabled_experiments
119
+ experiments.select { |_k, v| v }.keys
120
+ end
121
+
122
+ def initialize(manifest_text)
123
+ m = parse_json(manifest_text)
124
+ RUBY_TO_JSON.each do |ruby, json|
125
+ instance_variable_set(:"@#{ruby}", m[json])
126
+ end
127
+ @requirements_only ||= false
128
+ @marketing_only ||= false
129
+ @single_install ||= false
130
+ @private = m.fetch('private', true)
131
+ @signed_urls ||= false
132
+ @no_template ||= false
133
+ @experiments ||= {}
134
+ set_locations_and_hosts
135
+ end
136
+
137
+ LEGACY_LOCATION_OBJECT = { 'url' => LEGACY_URI_STUB }.freeze
138
+ private_constant :LEGACY_LOCATION_OBJECT
139
+
140
+ private
141
+
142
+ attr_reader :locations
143
+
144
+ def products_from_locations
145
+ location_options.map { |lo| lo.location && lo.location.product_code }
146
+ .compact
147
+ .uniq
148
+ .map { |code| Product.find_by(code: code) }
149
+ end
150
+
151
+ def set_locations_and_hosts
152
+ @locations =
153
+ case original_locations
154
+ when Hash
155
+ @used_hosts = original_locations.keys
156
+ replace_legacy_locations original_locations
157
+ when Array
158
+ @used_hosts = ['support']
159
+ new_locations = NoOverrideHash[original_locations.map { |location| [ location, LEGACY_LOCATION_OBJECT ] }]
160
+ { 'support' => new_locations }
161
+ when String
162
+ @used_hosts = ['support']
163
+ { 'support' => { original_locations => LEGACY_LOCATION_OBJECT } }
164
+ # TODO: error out for numbers and Booleans
165
+ else # NilClass
166
+ @used_hosts = ['support']
167
+ { 'support' => {} }
168
+ end
169
+ end
170
+
171
+ def replace_legacy_locations(original_locations)
172
+ NoOverrideHash.new.tap do |new_locations_obj|
173
+ Product::PRODUCTS_AVAILABLE.each do |product|
174
+ product_key = product.name.to_s
175
+ legacy_key = product.legacy_name.to_s
176
+ value_for_product = original_locations.fetch(product_key, original_locations[legacy_key])
177
+ value_for_product && new_locations_obj[product_key] = replace_string_uris(value_for_product)
178
+ end
179
+ end
180
+ end
181
+
182
+ def replace_string_uris(product_locations)
183
+ product_locations.each_with_object({}) do |(k, v), new_locations|
184
+ new_locations[k] = if v.is_a? Hash
185
+ v
186
+ else
187
+ { 'url' => v }
188
+ end
189
+ end
190
+ end
191
+
192
+ def parse_json(manifest_text)
193
+ parser_opts = { object_class: Manifest::NoOverrideHash }
194
+ JSON.parse(manifest_text, parser_opts)
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ class Manifest
5
+ class LocationOptions
6
+ RUBY_TO_JSON = {
7
+ legacy: 'legacy',
8
+ auto_load: 'autoLoad',
9
+ auto_hide: 'autoHide',
10
+ signed: 'signed',
11
+ url: 'url'
12
+ }.freeze
13
+
14
+ attr_reader :location
15
+ attr_reader(*RUBY_TO_JSON.keys)
16
+
17
+ alias_method :signed?, :signed
18
+ alias_method :legacy?, :legacy
19
+ alias_method :auto_load?, :auto_load
20
+ alias_method :auto_hide?, :auto_hide
21
+
22
+ def initialize(location, options)
23
+ @location = location
24
+
25
+ RUBY_TO_JSON.each do |ruby, json|
26
+ instance_variable_set(:"@#{ruby}", options[json])
27
+ end
28
+ @legacy ||= @url == ZendeskAppsSupport::Manifest::LEGACY_URI_STUB
29
+ @auto_load = options.fetch('autoLoad', true)
30
+ @auto_hide ||= false
31
+ @signed ||= false
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ class Manifest
5
+ class OverrideError < StandardError
6
+ attr_reader :key, :original, :attempted
7
+ attr_accessor :message
8
+ def initialize(key, original, attempted)
9
+ @key = key
10
+ @original = original
11
+ @attempted = attempted
12
+ end
13
+
14
+ def message
15
+ @message ||= begin
16
+ translated_error_key = 'txt.apps.admin.error.app_build.duplicate_reference'
17
+ translated_error = ZendeskAppsSupport::I18n.t(translated_error_key, key: key)
18
+
19
+ # if the error contains the word `_legacy` in the second sentence, let's
20
+ # only use the first one.
21
+ if [original, attempted].any? { |val| val =~ /_legacy/ }
22
+ return translated_error
23
+ end
24
+ translated_detail_key = 'txt.apps.admin.error.app_build.duplicate_reference_values'
25
+ translated_detail = ZendeskAppsSupport::I18n.t(translated_detail_key,
26
+ original: original,
27
+ attempted: attempted)
28
+ "#{translated_error} #{translated_detail}"
29
+ end
30
+ end
31
+ end
32
+
33
+ class NoOverrideHash < Hash
34
+ class << self
35
+ def [](array)
36
+ new.tap do |hash|
37
+ array.each do |key, value|
38
+ hash[key] = value
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def []=(key, value)
45
+ raise OverrideError.new(key, self[key], value) if key? key
46
+ super
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZendeskAppsSupport
4
+ class Manifest
5
+ class Parameter
6
+ TYPES = %w[text password checkbox url number multiline hidden oauth].freeze
7
+ ATTRIBUTES = %i[name type required secure default].freeze
8
+ attr_reader(*ATTRIBUTES)
9
+ def default?
10
+ @has_default
11
+ end
12
+
13
+ def initialize(p)
14
+ @name = p['name']
15
+ @type = p['type'] || 'text'
16
+ @required = p['required'] || false
17
+ @secure = p['secure'] || false
18
+ @has_default = p.key? 'default'
19
+ @default = p['default'] if @has_default
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,402 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'erubis'
5
+ require 'json'
6
+
7
+ module ZendeskAppsSupport
8
+ class Package
9
+ extend Gem::Deprecate
10
+ include ZendeskAppsSupport::BuildTranslation
11
+
12
+ MANIFEST_FILENAME = 'manifest.json'
13
+ REQUIREMENTS_FILENAME = 'requirements.json'
14
+
15
+ DEFAULT_LAYOUT = Erubis::Eruby.new(File.read(File.expand_path('../assets/default_template.html.erb', __FILE__)))
16
+ DEFAULT_SCSS = File.read(File.expand_path('../assets/default_styles.scss', __FILE__))
17
+ SRC_TEMPLATE = Erubis::Eruby.new(File.read(File.expand_path('../assets/src.js.erb', __FILE__)))
18
+
19
+ LOCATIONS_WITH_ICONS_PER_PRODUCT = {
20
+ Product::SUPPORT => %w[top_bar nav_bar system_top_bar ticket_editor].freeze,
21
+ Product::SELL => %w[top_bar].freeze
22
+ }.freeze
23
+
24
+ attr_reader :lib_root, :root, :warnings
25
+
26
+ def initialize(dir, is_cached = true)
27
+ @root = Pathname.new(File.expand_path(dir))
28
+ @lib_root = Pathname.new(File.join(root, 'lib'))
29
+
30
+ @is_cached = is_cached # disabled by ZAT for development
31
+ @warnings = []
32
+ end
33
+
34
+ def validate(marketplace: true, skip_marketplace_translations: false)
35
+ errors = []
36
+ errors << Validations::Manifest.call(self)
37
+
38
+ if has_valid_manifest?(errors)
39
+ errors << Validations::Marketplace.call(self) if marketplace
40
+ errors << Validations::Source.call(self)
41
+ errors << Validations::Translations.call(self, skip_marketplace_translations: skip_marketplace_translations)
42
+ errors << Validations::Requirements.call(self)
43
+
44
+ # only adds warnings
45
+ Validations::SecureSettings.call(self)
46
+ Validations::Requests.call(self)
47
+
48
+ unless manifest.requirements_only? || manifest.marketing_only? || manifest.iframe_only?
49
+ errors << Validations::Templates.call(self)
50
+ errors << Validations::Stylesheets.call(self)
51
+ end
52
+ end
53
+
54
+ errors << Validations::Banner.call(self) if has_banner?
55
+ errors << Validations::Svg.call(self) if has_svgs?
56
+ errors << Validations::Mime.call(self)
57
+
58
+ # only adds warnings
59
+ Validations::Secrets.call(self)
60
+
61
+ errors.flatten.compact
62
+ end
63
+
64
+ def validate!(marketplace: true, skip_marketplace_translations: false)
65
+ errors = validate(marketplace: marketplace, skip_marketplace_translations: skip_marketplace_translations)
66
+ raise errors.first if errors.any?
67
+ true
68
+ end
69
+
70
+ def assets
71
+ @assets ||= Dir.chdir(root) do
72
+ Dir['assets/**/*'].select { |f| File.file?(f) }
73
+ end
74
+ end
75
+
76
+ def path_to(file)
77
+ File.join(root, file)
78
+ end
79
+
80
+ def requirements_path
81
+ path_to(REQUIREMENTS_FILENAME)
82
+ end
83
+
84
+ def locales
85
+ translations.keys
86
+ end
87
+
88
+ def files
89
+ files = []
90
+ Dir[root.join('**/**')].each do |f|
91
+ next unless File.file?(f)
92
+ relative_file_name = f.sub(%r{#{root}/?}, '')
93
+ next if relative_file_name =~ %r{^tmp/}
94
+ files << AppFile.new(self, relative_file_name)
95
+ end
96
+ files
97
+ end
98
+
99
+ def text_files
100
+ @text_files ||= files.select { |f| f =~ %r{.*(html?|xml|js|json?)$} }
101
+ end
102
+
103
+ def js_files
104
+ @js_files ||= files.select { |f| f =~ %r{^.*\.jsx?$} }
105
+ end
106
+
107
+ def html_files
108
+ @html_files ||= files.select { |f| f =~ %r{.*\.html?$} }
109
+ end
110
+
111
+ def lib_files
112
+ @lib_files ||= js_files.select { |f| f =~ %r{^lib/} }
113
+ end
114
+
115
+ def svg_files
116
+ @svg_files ||= files.select { |f| f =~ %r{^assets/.*\.svg$} }
117
+ end
118
+
119
+ def template_files
120
+ files.select { |f| f =~ %r{^templates/.*\.hdbs$} }
121
+ end
122
+
123
+ def translation_files
124
+ files.select { |f| f =~ %r{^translations/} }
125
+ end
126
+
127
+ # this is not really compile_js, it compiles the whole app including scss for v1 apps
128
+ def compile(options)
129
+ begin
130
+ app_id = options.fetch(:app_id)
131
+ asset_url_prefix = options.fetch(:assets_dir)
132
+ name = options.fetch(:app_name)
133
+ rescue KeyError => e
134
+ raise ArgumentError, e.message
135
+ end
136
+
137
+ locale = options.fetch(:locale, 'en')
138
+
139
+ source = manifest.iframe_only? ? nil : app_js
140
+ app_class_name = "app-#{app_id}"
141
+ # if no_template is an array, we still need the templates
142
+ templates = manifest.no_template == true ? {} : compiled_templates(app_id, asset_url_prefix)
143
+
144
+ SRC_TEMPLATE.result(
145
+ name: name,
146
+ version: manifest.version,
147
+ source: source,
148
+ app_class_properties: manifest.app_class_properties,
149
+ asset_url_prefix: asset_url_prefix,
150
+ logo_asset_hash: generate_logo_hash(manifest.products),
151
+ location_icons: location_icons,
152
+ app_class_name: app_class_name,
153
+ author: manifest.author,
154
+ translations: manifest.iframe_only? ? nil : runtime_translations(translations_for(locale)),
155
+ framework_version: manifest.framework_version,
156
+ templates: templates,
157
+ modules: commonjs_modules,
158
+ iframe_only: manifest.iframe_only?
159
+ )
160
+ end
161
+
162
+ alias compile_js compile
163
+ deprecate :compile_js, :compile, 2017, 1
164
+
165
+ def manifest_json
166
+ @manifest_json ||= read_json(MANIFEST_FILENAME)
167
+ end
168
+ deprecate :manifest_json, :manifest, 2016, 9
169
+
170
+ def manifest
171
+ @manifest ||= Manifest.new(read_file(MANIFEST_FILENAME))
172
+ end
173
+
174
+ def requirements_json
175
+ return nil unless has_requirements?
176
+ @requirements ||= read_json(REQUIREMENTS_FILENAME, object_class: Manifest::NoOverrideHash)
177
+ end
178
+
179
+ def is_no_template
180
+ manifest.no_template?
181
+ end
182
+ deprecate :is_no_template, 'manifest.no_template?', 2016, 9
183
+
184
+ def no_template_locations
185
+ manifest.no_template_locations
186
+ end
187
+ deprecate :no_template_locations, 'manifest.no_template_locations', 2016, 9
188
+
189
+ def compiled_templates(app_id, asset_url_prefix)
190
+ compiler = ZendeskAppsSupport::StylesheetCompiler.new(DEFAULT_SCSS + app_css, app_id, asset_url_prefix)
191
+ compiled_css = compiler.compile(sassc: manifest.enabled_experiments.include?('newCssCompiler'))
192
+
193
+ layout = templates['layout'] || DEFAULT_LAYOUT.result
194
+
195
+ templates.tap do |templates|
196
+ templates['layout'] = "<style>\n#{compiled_css}</style>\n#{layout}"
197
+ end
198
+ end
199
+
200
+ def translations_for(locale)
201
+ trans = translations
202
+ return trans[locale] if trans[locale]
203
+ trans[manifest.default_locale]
204
+ end
205
+
206
+ def has_file?(path)
207
+ File.file?(path_to(path))
208
+ end
209
+
210
+ def has_svgs?
211
+ svg_files.any?
212
+ end
213
+
214
+ def has_requirements?
215
+ has_file?(REQUIREMENTS_FILENAME)
216
+ end
217
+
218
+ def self.has_custom_object_requirements?(requirements_hash)
219
+ return false if requirements_hash.nil?
220
+
221
+ custom_object_requirements = requirements_hash.fetch(AppRequirement::CUSTOM_OBJECTS_KEY, {})
222
+ types = custom_object_requirements.fetch(AppRequirement::CUSTOM_OBJECTS_TYPE_KEY, [])
223
+ relationships = custom_object_requirements.fetch(AppRequirement::CUSTOM_OBJECTS_RELATIONSHIP_TYPE_KEY, [])
224
+
225
+ (types | relationships).any?
226
+ end
227
+
228
+ def app_css
229
+ return File.read(path_to('app.scss')) if has_file?('app.scss')
230
+ return File.read(path_to('app.css')) if has_file?('app.css')
231
+ ''
232
+ end
233
+
234
+ def app_js
235
+ if @is_cached
236
+ @app_js ||= read_file('app.js')
237
+ else
238
+ read_file('app.js')
239
+ end
240
+ end
241
+
242
+ def iframe_only?
243
+ manifest.iframe_only?
244
+ end
245
+ deprecate :iframe_only?, 'manifest.iframe_only?', 2016, 9
246
+
247
+ def templates
248
+ templates_dir = path_to('templates')
249
+ Dir["#{templates_dir}/*.hdbs"].each_with_object({}) do |file, memo|
250
+ str = File.read(file)
251
+ str.chomp!
252
+ memo[File.basename(file, File.extname(file))] = str
253
+ memo
254
+ end
255
+ end
256
+
257
+ def translations
258
+ return @translations if @is_cached && @translations
259
+
260
+ @translations = begin
261
+ translation_dir = path_to('translations')
262
+ return {} unless File.directory?(translation_dir)
263
+
264
+ locale_path = "#{translation_dir}/#{manifest.default_locale}.json"
265
+ default_translations = process_translations(locale_path, default_locale: true)
266
+
267
+ Dir["#{translation_dir}/*.json"].each_with_object({}) do |path, memo|
268
+ locale = File.basename(path, File.extname(path))
269
+
270
+ locale_translations = if locale == manifest.default_locale
271
+ default_translations
272
+ else
273
+ deep_merge_hash(default_translations, process_translations(path))
274
+ end
275
+
276
+ memo[locale] = locale_translations
277
+ end
278
+ end
279
+ end
280
+
281
+ def location_icons
282
+ Hash.new { |h, k| h[k] = {} }.tap do |location_icons|
283
+ manifest.location_options.each do |location_options|
284
+ # no location information in the manifest
285
+ next unless location_options.location
286
+
287
+ product = location_options.location.product
288
+ location_name = location_options.location.name
289
+ # the location on the product does not support icons
290
+ next unless LOCATIONS_WITH_ICONS_PER_PRODUCT.fetch(product, []).include?(location_name)
291
+
292
+ host = location_options.location.product.name
293
+ product_directory = manifest.products.count > 1 ? "#{host}/" : ''
294
+ location_icons[host][location_name] = build_location_icons_hash(location_name, product_directory)
295
+ end
296
+ end
297
+ end
298
+
299
+ private
300
+
301
+ def generate_logo_hash(products)
302
+ {}.tap do |logo_hash|
303
+ products.each do |product|
304
+ product_directory = products.count > 1 ? "#{product.name.downcase}/" : ''
305
+ logo_hash[product.name.downcase] = "#{product_directory}logo-small.png"
306
+ end
307
+ end
308
+ end
309
+
310
+ def has_valid_manifest?(errors)
311
+ has_manifest? && errors.flatten.empty?
312
+ end
313
+
314
+ def runtime_translations(translations)
315
+ result = translations.dup
316
+ result.delete('name')
317
+ result.delete('short_description')
318
+ result.delete('long_description')
319
+ result.delete('installation_instructions')
320
+ result
321
+ end
322
+
323
+ def process_translations(locale_path, default_locale: false)
324
+ translations = File.exist?(locale_path) ? JSON.parse(File.read(locale_path)) : {}
325
+ translations['app'].delete('name') if !default_locale && translations.key?('app')
326
+ translations['app'].delete('package') if translations.key?('app')
327
+ remove_zendesk_keys(translations)
328
+ end
329
+
330
+ def has_lib_js?
331
+ lib_files.any?
332
+ end
333
+
334
+ def has_manifest?
335
+ has_file?(MANIFEST_FILENAME)
336
+ end
337
+
338
+ def has_banner?
339
+ has_file?('assets/banner.png')
340
+ end
341
+
342
+ def build_location_icons_hash(location, product_directory)
343
+ inactive_png = "icon_#{location}_inactive.png"
344
+ if has_file?("assets/#{product_directory}icon_#{location}.svg")
345
+ build_svg_icon_hash(location, product_directory)
346
+ elsif has_file?("assets/#{product_directory}#{inactive_png}")
347
+ build_png_icons_hash(location, product_directory)
348
+ else
349
+ {}
350
+ end
351
+ end
352
+
353
+ def build_svg_icon_hash(location, product_directory)
354
+ cache_busting_param = "?#{Time.now.to_i}" unless @is_cached
355
+ { 'svg' => "#{product_directory}icon_#{location}.svg#{cache_busting_param}" }
356
+ end
357
+
358
+ def build_png_icons_hash(location, product_directory)
359
+ inactive_png = "#{product_directory}icon_#{location}_inactive.png"
360
+ {
361
+ 'inactive' => inactive_png
362
+ }.tap do |icon_state_hash|
363
+ %w[active hover].each do |state|
364
+ specific_png = "#{product_directory}icon_#{location}_#{state}.png"
365
+ selected_png = has_file?("assets/#{specific_png}") ? specific_png : inactive_png
366
+ icon_state_hash[state] = selected_png
367
+ end
368
+ end
369
+ end
370
+
371
+ def commonjs_modules
372
+ return {} unless has_lib_js?
373
+
374
+ lib_files.each_with_object({}) do |file, modules|
375
+ name = file.relative_path.gsub(/^lib\//, '')
376
+ content = file.read
377
+ modules[name] = content
378
+ end
379
+ end
380
+
381
+ def deep_merge_hash(h, another_h)
382
+ result_h = h.dup
383
+ another_h.each do |key, value|
384
+ result_h[key] = if h.key?(key) && h[key].is_a?(Hash) && value.is_a?(Hash)
385
+ deep_merge_hash(h[key], value)
386
+ else
387
+ value
388
+ end
389
+ end
390
+ result_h
391
+ end
392
+
393
+ def read_file(path)
394
+ File.read(path_to(path))
395
+ end
396
+
397
+ def read_json(path, parser_opts = {})
398
+ file = read_file(path)
399
+ JSON.parse(read_file(path), parser_opts) unless file.nil?
400
+ end
401
+ end
402
+ end