Class: User

Inherits:
ApplicationRecord show all
Includes:
SamlInit, UserMerge
Defined in:
app/models/user.rb

Overview

Represents a user. Most of the User’s logic is controlled by Devise and its overrides. A user, as far as the application code (i.e. excluding Devise) is concerned, has many questions, answers, and votes.

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SamlInit

#saml_identifier, #saml_identifier=, #saml_init_email, #saml_init_email=, #saml_init_email_and_identifier, #saml_init_email_and_identifier=, #saml_init_identifier, #saml_init_identifier=, #saml_init_username_no_update, #saml_init_username_no_update=

Methods inherited from ApplicationRecord

#attributes_print, fuzzy_search, match_search, #match_search, sanitize_for_search, sanitize_name, sanitize_sql_in, useful_err_msg, with_lax_group_rules

Class Method Details

.list_includesObject



50
51
52
# File 'app/models/user.rb', line 50

def self.list_includes
  includes(:posts, :avatar_attachment)
end

.search(term) ⇒ Object



54
55
56
# File 'app/models/user.rb', line 54

def self.search(term)
  where('username LIKE ?', "#{sanitize_sql_like(term)}%")
end

Instance Method Details

#active_flags(post) ⇒ Object



350
351
352
# File 'app/models/user.rb', line 350

def active_flags(post)
  post.flags.where(user: self, status: nil)
end

#answersObject



134
135
136
# File 'app/models/user.rb', line 134

def answers
  posts.where(post_type_id: Answer.post_type_id)
end

#block(reason, length: 180.days) ⇒ Object



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'app/models/user.rb', line 293

def block(reason, length: 180.days)
  user_email = email
  user_ip = []

  if 
    user_ip << 
  end

  BlockedItem.create(item_type: 'email', value: user_email, expires: length.from_now,
                     automatic: true, reason: "#{reason}: #" + id.to_s)
  user_ip.compact.uniq.each do |ip|
    BlockedItem.create(item_type: 'ip', value: ip, expires: length.from_now,
                       automatic: true, reason: "#{reason}: #" + id.to_s)
  end
end

#can_push_to_network(post_type) ⇒ Boolean

Checks if the user can push a given post type to network

Parameters:

  • post_type (PostType)

    type of the post to be pushed

Returns:

  • (Boolean)


87
88
89
# File 'app/models/user.rb', line 87

def can_push_to_network(post_type)
  post_type.system? && (is_global_moderator || is_global_admin)
end

#can_update(post, post_type) ⇒ Boolean

Checks if the user can directly update a given post

Parameters:

  • post (Post)

    updated post (owners can unilaterally update)

  • post_type (PostType)

    type of the post (some are freely editable)

Returns:

  • (Boolean)


95
96
97
98
# File 'app/models/user.rb', line 95

def can_update(post, post_type)
  privilege?('edit_posts') || is_moderator || self == post.user || \
    (post_type.is_freely_editable && privilege?('unrestricted'))
end

#category_preference(category_id) ⇒ Object



320
321
322
323
324
# File 'app/models/user.rb', line 320

def category_preference(category_id)
  category_key = "prefs.#{id}.category.#{RequestContext.community_id}.category.#{category_id}"
  AppConfig.preferences.select { |_, v| v['category'] }.transform_values { |v| v['default'] }
           .merge(RequestContext.redis.hgetall(category_key))
end

#create_notification(content, link) ⇒ Object



122
123
124
# File 'app/models/user.rb', line 122

def create_notification(content, link)
  notifications.create!(content: content, link: link)
end

#do_soft_delete(attribute_to) ⇒ Object



354
355
356
357
358
359
360
361
362
# File 'app/models/user.rb', line 354

def do_soft_delete(attribute_to)
  AuditLog.moderator_audit(event_type: 'user_delete', related: self, user: attribute_to,
                           comment: attributes_print(join: "\n"))
  assign_attributes(deleted: true, deleted_by_id: attribute_to.id, deleted_at: DateTime.now,
                    username: "user#{id}", email: "#{id}@deleted.localhost",
                    password: SecureRandom.hex(32))
  skip_reconfirmation!
  save
end

#email_domain_not_blocklistedObject



220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'app/models/user.rb', line 220

def email_domain_not_blocklisted
  return unless File.exist?(Rails.root.join('../.qpixel-domain-blocklist.txt'))
  return unless saved_changes.include? 'email'

  blocklist = File.read(Rails.root.join('../.qpixel-domain-blocklist.txt')).split("\n")
  email_domain = email.split('@')[-1]
  matched = blocklist.select { |x| email_domain == x }
  if matched.any?
    errors.add(:base, ApplicationRecord.useful_err_msg.sample)
    matched_domains = matched.map { |d| "equals: #{d}" }
    AuditLog.block_log(event_type: 'user_email_domain_blocked',
                       comment: "email: #{email}\n#{matched_domains.join("\n")}\nsource: file")
  end
end

#email_not_bad_patternObject



254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'app/models/user.rb', line 254

def email_not_bad_pattern
  return unless File.exist?(Rails.root.join('../.qpixel-email-patterns.txt'))
  return unless changes.include? 'email'

  patterns = File.read(Rails.root.join('../.qpixel-email-patterns.txt')).split("\n")
  matched = patterns.select { |p| email.match? Regexp.new(p) }
  if matched.any?
    errors.add(:base, ApplicationRecord.useful_err_msg.sample)
    matched_patterns = matched.map { |p| "matched: #{p}" }
    AuditLog.block_log(event_type: 'user_email_pattern_match',
                       comment: "email: #{email}\n#{matched_patterns.join("\n")}")
  end
end

#ensure_community_user!Object



268
269
270
# File 'app/models/user.rb', line 268

def ensure_community_user!
  community_user || create_community_user(reputation: SiteSetting['NewUserInitialRep'])
end

#ensure_websitesObject



146
147
148
149
150
151
152
# File 'app/models/user.rb', line 146

def ensure_websites
  pos = user_websites.size
  while pos < UserWebsite::MAX_ROWS
    pos += 1
    UserWebsite.create(user_id: id, position: pos)
  end
end

#extract_ip_from(request) ⇒ Object



280
281
282
283
284
# File 'app/models/user.rb', line 280

def extract_ip_from(request)
  # Customize this to your environment: if you're not behind a reverse proxy like Cloudflare, you probably
  # don't need this (or you can change it to another header if that's what your reverse proxy uses).
  request.headers['CF-Connecting-IP'] || request.ip
end

#has_ability_on(community_id, ability_internal_id) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'app/models/user.rb', line 184

def has_ability_on(community_id, ability_internal_id)
  cu = community_users.where(community_id: community_id).first
  if cu&.is_moderator || cu&.is_admin || is_global_moderator || is_global_admin || cu&.privilege?('mod')
    true
  elsif cu.nil?
    false
  else
    Ability.unscoped do
      UserAbility.joins(:ability).where(community_user_id: cu&.id, is_suspended: false,
                                        ability: { internal_id: ability_internal_id }).exists?
    end
  end
end

#has_active_flags?(post) ⇒ Boolean

Returns:

  • (Boolean)


346
347
348
# File 'app/models/user.rb', line 346

def has_active_flags?(post)
  !post.flags.where(user: self, status: nil).empty?
end

#has_post_privilege?(name, post) ⇒ Boolean

This class makes heavy use of predicate names, and their use is prevalent throughout the codebase because of the importance of these methods. rubocop:disable Naming/PredicateName

Returns:

  • (Boolean)


76
77
78
79
80
81
82
# File 'app/models/user.rb', line 76

def has_post_privilege?(name, post)
  if post.user == self
    true
  else
    privilege?(name)
  end
end

#has_profile_on(community_id) ⇒ Object

Used by network profile: does this user have a profile on that other comm?



163
164
165
166
# File 'app/models/user.rb', line 163

def has_profile_on(community_id)
  cu = community_users.where(community_id: community_id).first
  !cu&.user_id.nil? || false
end

#inspectObject



58
59
60
# File 'app/models/user.rb', line 58

def inspect
  "#<User #{attributes.compact.map { |k, v| "#{k}: #{v}" }.join(', ')}>"
end

#is_adminObject



158
159
160
# File 'app/models/user.rb', line 158

def is_admin
  is_global_admin || community_user&.is_admin || false
end

#is_moderatorObject



154
155
156
# File 'app/models/user.rb', line 154

def is_moderator
  is_global_moderator || community_user&.is_moderator || is_admin || community_user&.privilege?('mod') || false
end

#is_moderator_on(community_id) ⇒ Object



178
179
180
181
182
# File 'app/models/user.rb', line 178

def is_moderator_on(community_id)
  cu = community_users.where(community_id: community_id).first
  # is_moderator is a DB check, not a call to is_moderator()
  is_global_moderator || is_admin || cu&.is_moderator || cu&.privilege?('mod') || false
end

#is_not_blocklistedObject



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'app/models/user.rb', line 235

def is_not_blocklisted
  return unless saved_changes.include? 'email'

  email_domain = email.split('@')[-1]
  is_mail_blocked = BlockedItem.emails.where(value: email)
  is_mail_host_blocked = BlockedItem.email_hosts.where(value: email_domain)
  if is_mail_blocked.any? || is_mail_host_blocked.any?
    errors.add(:base, ApplicationRecord.useful_err_msg.sample)
    if is_mail_blocked.any?
      AuditLog.block_log(event_type: 'user_email_blocked', related: is_mail_blocked.first,
                         comment: "email: #{email}\nfull match to: #{is_mail_blocked.first.value}")
    end
    if is_mail_host_blocked.any?
      AuditLog.block_log(event_type: 'user_email_domain_blocked', related: is_mail_host_blocked.first,
                         comment: "email: #{email}\ndomain match to: #{is_mail_host_blocked.first.value}")
    end
  end
end

#metric(key) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'app/models/user.rb', line 100

def metric(key)
  Rails.cache.fetch("community_user/#{community_user.id}/metric/#{key}", expires_in: 24.hours) do
    case key
    when 'p'
      Post.qa_only.undeleted.where(user: self).count
    when '1'
      Post.undeleted.where(post_type: PostType.top_level, user: self).count
    when '2'
      Post.undeleted.where(post_type: PostType.second_level, user: self).count
    when 's'
      Vote.where(recv_user_id: id, vote_type: 1).count - \
        Vote.where(recv_user_id: id, vote_type: -1).count
    when 'v'
      Vote.where(recv_user_id: id).count
    when 'V'
      votes.count
    when 'E'
      PostHistory.where(user: self, post_history_type: PostHistoryType.find_by(name: 'post_edited')).count
    end
  end
end

#no_blank_unicode_in_usernameObject



213
214
215
216
217
218
# File 'app/models/user.rb', line 213

def no_blank_unicode_in_username
  not_valid = !username.scan(/[\u200B-\u200D\uFEFF]/).empty?
  if not_valid
    errors.add(:username, 'may not contain blank unicode characters')
  end
end


272
273
274
275
276
277
278
# File 'app/models/user.rb', line 272

def no_links_in_username
  if %r{(?:http|ftp)s?://(?:\w+\.)+[a-zA-Z]{2,10}}.match?(username)
    errors.add(:username, 'cannot contain links')
    AuditLog.block_log(event_type: 'user_username_link_blocked',
                       comment: "username: #{username}")
  end
end

#post_count_on(community_id) ⇒ Object



173
174
175
176
# File 'app/models/user.rb', line 173

def post_count_on(community_id)
  cu = community_users.where(community_id: community_id).first
  cu&.post_count || 0
end

#preference(name, community: false) ⇒ Object



342
343
344
# File 'app/models/user.rb', line 342

def preference(name, community: false)
  preferences[community ? :community : :global][name]
end

#preferencesObject



309
310
311
312
313
314
315
316
317
318
# File 'app/models/user.rb', line 309

def preferences
  global_key = "prefs.#{id}"
  community_key = "prefs.#{id}.community.#{RequestContext.community_id}"
  {
    global: AppConfig.preferences.select { |_, v| v['global'] }.transform_values { |v| v['default'] }
                     .merge(RequestContext.redis.hgetall(global_key)),
    community: AppConfig.preferences.select { |_, v| v['community'] }.transform_values { |v| v['default'] }
                        .merge(RequestContext.redis.hgetall(community_key))
  }
end

#questionsObject



130
131
132
# File 'app/models/user.rb', line 130

def questions
  posts.where(post_type_id: Question.post_type_id)
end

#reputation_on(community_id) ⇒ Object



168
169
170
171
# File 'app/models/user.rb', line 168

def reputation_on(community_id)
  cu = community_users.where(community_id: community_id).first
  cu&.reputation || 1
end

#rtl_safe_usernameObject



198
199
200
# File 'app/models/user.rb', line 198

def rtl_safe_username
  "#{username}\u202D"
end

#same_as?(user) ⇒ Boolean

Checks whether this user is the same as a given user

Parameters:

  • user (User)

    user to compare with

Returns:

  • (Boolean)


68
69
70
# File 'app/models/user.rb', line 68

def same_as?(user)
  id == user.id
end

#send_welcome_tour_messageObject



286
287
288
289
290
291
# File 'app/models/user.rb', line 286

def send_welcome_tour_message
  return if id == -1 || RequestContext.community.nil?

  create_notification("👋 Welcome to #{SiteSetting['SiteName'] || 'Codidact'}! Take our tour to find out " \
                      'how this site works.', '/tour')
end

#trust_levelObject



62
63
64
# File 'app/models/user.rb', line 62

def trust_level
  community_user.trust_level
end

#unread_countObject



126
127
128
# File 'app/models/user.rb', line 126

def unread_count
  notifications.unscoped.where(user: self, is_read: false).count
end

#username_not_fake_adminObject



202
203
204
205
206
207
208
209
210
211
# File 'app/models/user.rb', line 202

def username_not_fake_admin
  admin_badge = SiteSetting['AdminBadgeCharacter']
  mod_badge = SiteSetting['ModBadgeCharacter']

  [admin_badge, mod_badge].each do |badge|
    if badge.present? && username.include?(badge)
      errors.add(:username, "may not include the #{badge} character")
    end
  end
end

#valid_websites_forObject



142
143
144
# File 'app/models/user.rb', line 142

def valid_websites_for
  user_websites.where.not(url: [nil, '']).order(position: :asc)
end

#validate_prefs!Object



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'app/models/user.rb', line 326

def validate_prefs!
  global_key = "prefs.#{id}"
  community_key = "prefs.#{id}.community.#{RequestContext.community_id}"
  {
    global_key => AppConfig.preferences.reject { |_, v| v['community'] },
    community_key => AppConfig.preferences.select { |_, v| v['community'] }
  }.each do |key, prefs|
    saved = RequestContext.redis.hgetall(key)
    valid_prefs = prefs.keys
    deprecated = saved.reject { |k, _v| valid_prefs.include? k }.map { |k, _v| k }
    unless deprecated.empty?
      RequestContext.redis.hdel key, *deprecated
    end
  end
end

#website_domainObject



138
139
140
# File 'app/models/user.rb', line 138

def website_domain
  website.nil? ? website : URI.parse(website).hostname
end