knowledge_base.rb 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class KnowledgeBase < ApplicationModel
  3. include HasTranslations
  4. include HasAgentAllowedParams
  5. include ChecksKbClientNotification
  6. AGENT_ALLOWED_NESTED_RELATIONS = %i[translations].freeze
  7. LAYOUTS = %w[grid list].freeze
  8. ICONSETS = %w[FontAwesome anticon material ionicons Simple-Line-Icons].freeze
  9. has_many :kb_locales, class_name: 'KnowledgeBase::Locale',
  10. inverse_of: :knowledge_base,
  11. dependent: :destroy
  12. accepts_nested_attributes_for :kb_locales, allow_destroy: true
  13. validates :kb_locales, presence: true
  14. validates :kb_locales, length: { maximum: 1, message: __('System supports only one locale for knowledge base. Upgrade your plan to use more locales.') }, unless: :multi_lingual_support?
  15. has_many :categories, class_name: 'KnowledgeBase::Category',
  16. inverse_of: :knowledge_base,
  17. dependent: :restrict_with_exception
  18. has_many :answers, through: :categories
  19. has_many :permissions, class_name: 'KnowledgeBase::Permission',
  20. as: :permissionable,
  21. autosave: true,
  22. dependent: :destroy
  23. validates :category_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
  24. validates :homepage_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
  25. validates :color_highlight, presence: true, 'validations/color': true
  26. validates :color_header, presence: true, 'validations/color': true
  27. validates :color_header_link, presence: true, 'validations/color': true
  28. validates :iconset, inclusion: { in: KnowledgeBase::ICONSETS }
  29. validate :validate_custom_address
  30. before_validation :patch_custom_address
  31. after_create :set_defaults
  32. after_destroy :set_kb_active_setting
  33. after_save :set_kb_active_setting
  34. scope :active, -> { where(active: true) }
  35. alias assets_essential assets
  36. def assets(data)
  37. return data if assets_added_to?(data)
  38. data = super(data)
  39. ApplicationModel::CanAssets.reduce(kb_locales + translations, data)
  40. end
  41. # assets without unnecessary bits
  42. def assets_public(data)
  43. data = assets_essential(data)
  44. data[:KnowledgeBase].each_value do |elem|
  45. elem.delete_if do |k, _|
  46. k.end_with?('_ids')
  47. end
  48. end
  49. data
  50. end
  51. def custom_address_uri
  52. return nil if custom_address.blank?
  53. scheme = Setting.get('http_type') || 'http'
  54. URI("#{scheme}://#{custom_address}")
  55. rescue URI::InvalidURIError
  56. nil
  57. end
  58. def custom_address_matches?(request)
  59. uri = custom_address_uri
  60. return false if uri.blank?
  61. given_fqdn = request.headers.env['SERVER_NAME']&.downcase
  62. given_path = request.headers.env['HTTP_X_ORIGINAL_URL']&.downcase
  63. # original url header not present, server not configured
  64. return false if given_path.nil?
  65. # path doesn't match
  66. return false if uri.path.downcase != given_path[0, uri.path.length]
  67. # domain present, but doesn't match
  68. return false if uri.host.present? && uri.host.downcase != given_fqdn
  69. true
  70. rescue URI::InvalidURIError
  71. false
  72. end
  73. def custom_address_prefix(request)
  74. host = custom_address_uri.host.presence || request.headers.env['SERVER_NAME']
  75. port = request.headers.env['SERVER_PORT']
  76. port_silent = (request.ssl? && port == '443') || (!request.ssl? && port == '80')
  77. port_string = port_silent ? '' : ":#{port}"
  78. "#{custom_address_uri.scheme}://#{host}#{port_string}"
  79. end
  80. def custom_address_path(path)
  81. uri = custom_address_uri
  82. return path if !uri
  83. custom_path = custom_address_uri.path || ''
  84. applied_path = path.gsub(%r{^/help}, custom_path)
  85. applied_path.presence || '/'
  86. end
  87. def canonical_host
  88. custom_address_uri&.host.presence || Setting.get('fqdn')
  89. end
  90. def canonical_scheme_host
  91. "#{Setting.get('http_type')}://#{canonical_host}"
  92. end
  93. def canonical_url(path)
  94. "#{canonical_scheme_host}#{custom_address_path(path)}"
  95. end
  96. def full_destroy!
  97. ChecksKbClientNotification.disable_in_all_classes!
  98. transaction do
  99. # get all categories with their children and reverse to delete children first
  100. categories.root.map(&:self_with_children).flatten.reverse.each(&:full_destroy!)
  101. translations.each(&:destroy!)
  102. kb_locales.each(&:destroy!)
  103. destroy!
  104. end
  105. ensure
  106. ChecksKbClientNotification.enable_in_all_classes!
  107. end
  108. def visible?
  109. active?
  110. end
  111. def api_url
  112. Rails.application.routes.url_helpers.knowledge_base_path(self)
  113. end
  114. def load_category(locale, id)
  115. categories.localed(locale).find_by(id: id)
  116. end
  117. def self.with_multiple_locales_exists?
  118. KnowledgeBase
  119. .active
  120. .joins(:kb_locales)
  121. .group('knowledge_bases.id')
  122. .pluck(Arel.sql('COUNT(knowledge_base_locales.id) as locales_count'))
  123. .any? { |e| e > 1 }
  124. end
  125. def permissions_effective
  126. cache_key = KnowledgeBase::Permission.cache_key self
  127. Rails.cache.fetch cache_key do
  128. permissions
  129. end
  130. end
  131. def attributes_with_association_ids
  132. attrs = super
  133. attrs[:permissions_effective] = permissions_effective
  134. attrs
  135. end
  136. def self.granular_permissions?
  137. KnowledgeBase::Permission.any?
  138. end
  139. def public_content?(kb_locale = nil)
  140. scope = answers.published
  141. scope = scope.localed(kb_locale.system_locale) if kb_locale
  142. scope.any?
  143. end
  144. private
  145. def set_defaults
  146. self.translations = kb_locales.map do |kb_locale|
  147. name = Setting.get('organization').presence || Setting.get('product_name').presence || 'Zammad'
  148. kb_suffix = ::Translation.translate kb_locale.system_locale.locale, 'Knowledge Base'
  149. KnowledgeBase::Translation.new(
  150. title: "#{name} #{kb_suffix}",
  151. footer_note: "© #{name}",
  152. kb_locale: kb_locale
  153. )
  154. end
  155. end
  156. def validate_custom_address
  157. return if custom_address.nil?
  158. # not domain, but no leading slash
  159. if custom_address.exclude?('.') && custom_address[0] != '/'
  160. errors.add(:custom_address, __('must begin with a slash ("/")'))
  161. end
  162. if custom_address.include?('://')
  163. errors.add(:custom_address, __('must not include a protocol (e.g., "http://" or "https://")'))
  164. end
  165. if custom_address.last == '/'
  166. errors.add(:custom_address, __('must not end with a slash ("/")'))
  167. end
  168. if custom_address == '/' # rubocop:disable Style/GuardClause
  169. errors.add(:custom_address, __('must be a valid path or domain'))
  170. end
  171. end
  172. def patch_custom_address
  173. self.custom_address = nil if custom_address == ''
  174. end
  175. def multi_lingual_support?
  176. Setting.get 'kb_multi_lingual_support'
  177. end
  178. def set_kb_active_setting
  179. Setting.set 'kb_active', KnowledgeBase.active.exists?
  180. CanBePublished.update_active_publicly!
  181. end
  182. end