summaryrefslogtreecommitdiff
path: root/app/models/broadcast_message.rb
blob: 949902fbb77b12ac62c5a475560183084aceb803 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# frozen_string_literal: true

class BroadcastMessage < ApplicationRecord
  include CacheMarkdownField
  include Sortable

  ALLOWED_TARGET_ACCESS_LEVELS = [
    Gitlab::Access::GUEST,
    Gitlab::Access::REPORTER,
    Gitlab::Access::DEVELOPER,
    Gitlab::Access::MAINTAINER,
    Gitlab::Access::OWNER
  ].freeze

  cache_markdown_field :message, pipeline: :broadcast_message, whitelisted: true

  validates :message,   presence: true
  validates :starts_at, presence: true
  validates :ends_at,   presence: true
  validates :broadcast_type, presence: true
  validates :target_access_levels, inclusion: { in: ALLOWED_TARGET_ACCESS_LEVELS }

  validates :color, allow_blank: true, color: true
  validates :font,  allow_blank: true, color: true

  default_value_for :color, '#E75E40'
  default_value_for :font,  '#FFFFFF'

  CACHE_KEY = 'broadcast_message_current_json'
  BANNER_CACHE_KEY = 'broadcast_message_current_banner_json'
  NOTIFICATION_CACHE_KEY = 'broadcast_message_current_notification_json'

  after_commit :flush_redis_cache

  enum broadcast_type: {
    banner: 1,
    notification: 2
  }

  class << self
    def current_banner_messages(current_path: nil, user_access_level: nil)
      fetch_messages BANNER_CACHE_KEY, current_path, user_access_level do
        current_and_future_messages.banner
      end
    end

    def current_notification_messages(current_path: nil, user_access_level: nil)
      fetch_messages NOTIFICATION_CACHE_KEY, current_path, user_access_level do
        current_and_future_messages.notification
      end
    end

    def current(current_path: nil, user_access_level: nil)
      fetch_messages CACHE_KEY, current_path, user_access_level do
        current_and_future_messages
      end
    end

    def current_and_future_messages
      where('ends_at > :now', now: Time.current).order_id_asc
    end

    def cache
      ::Gitlab::SafeRequestStore.fetch(:broadcast_message_json_cache) do
        Gitlab::JsonCache.new
      end
    end

    def cache_expires_in
      2.weeks
    end

    private

    def fetch_messages(cache_key, current_path, user_access_level)
      messages = cache.fetch(cache_key, as: BroadcastMessage, expires_in: cache_expires_in) do
        yield
      end

      now_or_future = messages.select(&:now_or_future?)

      # If there are cached entries but they don't match the ones we are
      # displaying we'll refresh the cache so we don't need to keep filtering.
      cache.expire(cache_key) if now_or_future != messages

      messages = now_or_future.select(&:now?)
      messages = messages.select do |message|
        message.matches_current_user_access_level?(user_access_level)
      end
      messages.select do |message|
        message.matches_current_path(current_path)
      end
    end
  end

  def active?
    started? && !ended?
  end

  def started?
    Time.current >= starts_at
  end

  def ended?
    ends_at < Time.current
  end

  def now?
    (starts_at..ends_at).cover?(Time.current)
  end

  def future?
    starts_at > Time.current
  end

  def now_or_future?
    now? || future?
  end

  def matches_current_user_access_level?(user_access_level)
    return false if target_access_levels.present? && Feature.disabled?(:role_targeted_broadcast_messages, default_enabled: :yaml)
    return true unless target_access_levels.present?

    target_access_levels.include? user_access_level
  end

  def matches_current_path(current_path)
    return false if current_path.blank? && target_path.present?
    return true if current_path.blank? || target_path.blank?

    # Ensure paths are consistent across callers.
    # This fixes a mismatch between requests in the GUI and CLI
    #
    # This has to be reassigned due to frozen strings being provided.
    unless current_path.start_with?("/")
      current_path = "/#{current_path}"
    end

    escaped = Regexp.escape(target_path).gsub('\\*', '.*')
    regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE

    regexp.match(current_path)
  end

  def flush_redis_cache
    [CACHE_KEY, BANNER_CACHE_KEY, NOTIFICATION_CACHE_KEY].each do |key|
      self.class.cache.expire(key)
    end
  end
end

BroadcastMessage.prepend_mod_with('BroadcastMessage')