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,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