user.rb 25.2 KB
Newer Older
1 2
require 'carrierwave/orm/activerecord'

gitlabhq's avatar
gitlabhq committed
3
class User < ActiveRecord::Base
4
  extend Gitlab::ConfigHelper
5 6

  include Gitlab::ConfigHelper
7
  include Gitlab::CurrentSettings
8 9
  include Referable
  include Sortable
10
  include CaseSensitivity
11 12
  include TokenAuthenticatable

13 14
  DEFAULT_NOTIFICATION_LEVEL = :participating

15
  add_authentication_token_field :authentication_token
16

17
  default_value_for :admin, false
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
18
  default_value_for :external, false
19
  default_value_for :can_create_group, gitlab_config.default_can_create_group
20 21
  default_value_for :can_create_team, false
  default_value_for :hide_no_ssh_key, false
22
  default_value_for :hide_no_password, false
23
  default_value_for :theme_id, gitlab_config.default_theme
24

25 26 27 28 29
  attr_encrypted :otp_secret,
    key:       Gitlab::Application.config.secret_key_base,
    mode:      :per_attribute_iv_and_salt,
    algorithm: 'aes-256-cbc'

30
  devise :two_factor_authenticatable,
31
         otp_secret_encryption_key: Gitlab::Application.config.secret_key_base
32

33
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
34 35
  serialize :otp_backup_codes, JSON

36
  devise :lockable, :recoverable, :rememberable, :trackable,
37
    :validatable, :omniauthable, :confirmable, :registerable
gitlabhq's avatar
gitlabhq committed
38

39
  attr_accessor :force_random_password
gitlabhq's avatar
gitlabhq committed
40

41 42 43
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

44 45 46 47
  #
  # Relations
  #

48
  # Namespace for personal projects
49
  has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace"
50 51 52

  # Profile
  has_many :keys, dependent: :destroy
53
  has_many :emails, dependent: :destroy
54
  has_many :personal_access_tokens, dependent: :destroy
55
  has_many :identities, dependent: :destroy, autosave: true
56
  has_many :u2f_registrations, dependent: :destroy
57 58

  # Groups
59
  has_many :members, dependent: :destroy
60
  has_many :group_members, dependent: :destroy, source: 'GroupMember'
61
  has_many :groups, through: :group_members
62 63
  has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
  has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
64

65
  # Projects
66 67
  has_many :groups_projects,          through: :groups, source: :projects
  has_many :personal_projects,        through: :namespace, source: :projects
68
  has_many :project_members,          dependent: :destroy, class_name: 'ProjectMember'
69
  has_many :projects,                 through: :project_members
70
  has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
Ciro Santilli's avatar
Ciro Santilli committed
71 72
  has_many :users_star_projects, dependent: :destroy
  has_many :starred_projects, through: :users_star_projects, source: :project
73

74
  has_many :snippets,                 dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
75 76 77 78
  has_many :issues,                   dependent: :destroy, foreign_key: :author_id
  has_many :notes,                    dependent: :destroy, foreign_key: :author_id
  has_many :merge_requests,           dependent: :destroy, foreign_key: :author_id
  has_many :events,                   dependent: :destroy, foreign_key: :author_id,   class_name: "Event"
79
  has_many :subscriptions,            dependent: :destroy
80
  has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
81 82
  has_many :assigned_issues,          dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
  has_many :assigned_merge_requests,  dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
Valery Sizov's avatar
Valery Sizov committed
83
  has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
84
  has_one  :abuse_report,             dependent: :destroy
85
  has_many :spam_logs,                dependent: :destroy
86
  has_many :builds,                   dependent: :nullify, class_name: 'Ci::Build'
87
  has_many :todos,                    dependent: :destroy
88
  has_many :notification_settings,    dependent: :destroy
89
  has_many :award_emoji,              as: :awardable, dependent: :destroy
90

91 92 93
  #
  # Validations
  #
Cyril's avatar
Cyril committed
94
  validates :name, presence: true
95 96
  validates :notification_email, presence: true, email: true
  validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
97
  validates :bio, length: { maximum: 255 }, allow_blank: true
98
  validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
99
  validates :username,
100
    namespace: true,
101
    presence: true,
102
    uniqueness: { case_sensitive: false }
103

104
  validate :namespace_uniq, if: ->(user) { user.username_changed? }
105
  validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
106
  validate :unique_email, if: ->(user) { user.email_changed? }
107
  validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
108
  validate :owns_public_email, if: ->(user) { user.public_email_changed? }
109
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
110

111
  before_validation :generate_password, on: :create
112
  before_validation :restricted_signup_domains, on: :create
113
  before_validation :sanitize_attrs
114
  before_validation :set_notification_email, if: ->(user) { user.email_changed? }
115
  before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
116

117
  after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
Nihad Abbasov's avatar
Nihad Abbasov committed
118
  before_save :ensure_authentication_token
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
119
  before_save :ensure_external_user_rights
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
120
  after_save :ensure_namespace_correct
121
  after_initialize :set_projects_limit
122
  before_create :check_confirmation_email
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
123 124 125
  after_create :post_create_hook
  after_destroy :post_destroy_hook

126
  # User's Layout preference
127
  enum layout: [:fixed, :fluid]
128

129 130
  # User's Dashboard preference
  # Note: When adding an option, it MUST go on the end of the array.
131
  enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
132

133 134
  # User's Project preference
  # Note: When adding an option, it MUST go on the end of the array.
135
  enum project_view: [:readme, :activity, :files]
136

Nihad Abbasov's avatar
Nihad Abbasov committed
137
  alias_attribute :private_token, :authentication_token
138

139
  delegate :path, to: :namespace, allow_nil: true, prefix: true
140

141 142 143
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
144
      transition ldap_blocked: :blocked
145 146
    end

147 148 149 150
    event :ldap_block do
      transition active: :ldap_blocked
    end

151 152
    event :activate do
      transition blocked: :active
153
      transition ldap_blocked: :active
154
    end
155 156 157 158 159 160

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
    end
161 162
  end

163
  mount_uploader :avatar, AvatarUploader
164

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
165
  # Scopes
166
  scope :admins, -> { where(admin: true) }
167
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
168
  scope :external, -> { where(external: true) }
169
  scope :active, -> { with_state(:active) }
skv's avatar
skv committed
170
  scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
171
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
172 173 174 175 176 177 178 179 180 181

  def self.with_two_factor
    joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
      where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
  end

  def self.without_two_factor
    joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
      where("u2f.id IS NULL AND otp_required_for_login = ?", false)
  end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
182

183 184 185
  #
  # Class methods
  #
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
186
  class << self
187
    # Devise method overridden to allow sign in with email or username
188 189 190
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
Gabriel Mazetto's avatar
Gabriel Mazetto committed
191
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase)
192
      else
Gabriel Mazetto's avatar
Gabriel Mazetto committed
193
        find_by(conditions)
194 195
      end
    end
196

Valery Sizov's avatar
Valery Sizov committed
197 198
    def sort(method)
      case method.to_s
199 200 201 202
      when 'recent_sign_in' then reorder(last_sign_in_at: :desc)
      when 'oldest_sign_in' then reorder(last_sign_in_at: :asc)
      else
        order_by(method)
Valery Sizov's avatar
Valery Sizov committed
203 204 205
      end
    end

206 207
    # Find a User by their primary email or any associated secondary email
    def find_by_any_email(email)
208 209 210 211 212 213 214
      sql = 'SELECT *
      FROM users
      WHERE id IN (
        SELECT id FROM users WHERE email = :email
        UNION
        SELECT emails.user_id FROM emails WHERE email = :email
      )
215 216 217
      LIMIT 1;'

      User.find_by_sql([sql, { email: email }]).first
218
    end
219

220
    def filter(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
221
      case filter_name
222 223 224 225 226 227 228 229 230 231
      when 'admins'
        self.admins
      when 'blocked'
        self.blocked
      when 'two_factor_disabled'
        self.without_two_factor
      when 'two_factor_enabled'
        self.with_two_factor
      when 'wop'
        self.without_projects
232 233
      when 'external'
        self.external
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
234 235 236
      else
        self.active
      end
237 238
    end

239 240 241 242 243 244 245
    # Searches users matching the given query.
    #
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
    #
    # query - The search query as a String
    #
    # Returns an ActiveRecord::Relation.
246
    def search(query)
247
      table   = arel_table
248 249 250 251 252 253 254
      pattern = "%#{query}%"

      where(
        table[:name].matches(pattern).
          or(table[:email].matches(pattern)).
          or(table[:username].matches(pattern))
      )
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
255
    end
256

257
    def by_login(login)
258 259 260 261 262 263 264
      return nil unless login

      if login.include?('@'.freeze)
        unscoped.iwhere(email: login).take
      else
        unscoped.iwhere(username: login).take
      end
265 266
    end

267 268 269 270
    def find_by_username!(username)
      find_by!('lower(username) = ?', username.downcase)
    end

271 272 273 274 275
    def find_by_personal_access_token(token_string)
      personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
      personal_access_token.user if personal_access_token
    end

276
    def by_username_or_id(name_or_id)
Gabriel Mazetto's avatar
Gabriel Mazetto committed
277
      find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
278
    end
279

280 281
    def build_user(attrs = {})
      User.new(attrs)
282
    end
283 284 285 286

    def reference_prefix
      '@'
    end
287 288 289 290 291 292 293 294

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
        #{Regexp.escape(reference_prefix)}
        (?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})
      }x
    end
vsizov's avatar
vsizov committed
295
  end
randx's avatar
randx committed
296

297 298 299
  #
  # Instance methods
  #
300 301 302 303 304

  def to_param
    username
  end

305 306 307 308
  def to_reference(_from_project = nil)
    "#{self.class.reference_prefix}#{username}"
  end

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
309 310 311 312
  def generate_password
    if self.force_random_password
      self.password = self.password_confirmation = Devise.friendly_token.first(8)
    end
randx's avatar
randx committed
313
  end
314

315
  def generate_reset_token
316
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
317 318 319 320

    self.reset_password_token   = enc
    self.reset_password_sent_at = Time.now.utc

321
    @reset_token
322 323
  end

324
  def check_confirmation_email
325
    skip_confirmation! unless current_application_settings.send_user_confirmation_email
326 327
  end

328 329 330 331
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

332
  def disable_two_factor!
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
    transaction do
      update_attributes(
        otp_required_for_login:      false,
        encrypted_otp_secret:        nil,
        encrypted_otp_secret_iv:     nil,
        encrypted_otp_secret_salt:   nil,
        otp_grace_period_started_at: nil,
        otp_backup_codes:            nil
      )
      self.u2f_registrations.destroy_all
    end
  end

  def two_factor_enabled?
    two_factor_otp_enabled? || two_factor_u2f_enabled?
  end

  def two_factor_otp_enabled?
    self.otp_required_for_login?
  end

  def two_factor_u2f_enabled?
    self.u2f_registrations.exists?
356 357
  end

358
  def namespace_uniq
359
    # Return early if username already failed the first uniqueness validation
360 361
    return if self.errors.key?(:username) &&
      self.errors[:username].include?('has already been taken')
362

363
    namespace_name = self.username
364 365
    existing_namespace = Namespace.by_path(namespace_name)
    if existing_namespace && existing_namespace != self.namespace
366
      self.errors.add(:username, 'has already been taken')
367 368
    end
  end
369

370 371 372 373 374 375
  def avatar_type
    unless self.avatar.image?
      self.errors.add :avatar, "only images allowed"
    end
  end

376
  def unique_email
377 378 379
    if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email)
      self.errors.add(:email, 'has already been taken')
    end
380 381
  end

382
  def owns_notification_email
383 384
    return if self.temp_oauth_email?

385 386 387
    self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
  end

388
  def owns_public_email
389 390
    return if self.public_email.blank?

391 392 393 394 395 396 397 398
    self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email)
  end

  def update_emails_with_primary_email
    primary_email_record = self.emails.find_by(email: self.email)
    if primary_email_record
      primary_email_record.destroy
      self.emails.create(email: self.email_was)
399

400 401 402 403
      self.update_secondary_emails!
    end
  end

404 405
  # Returns the groups a user has access to
  def authorized_groups
406
    union = Gitlab::SQL::Union.
407
      new([groups.select(:id), authorized_projects.select(:namespace_id)])
408

409
    Group.where("namespaces.id IN (#{union.to_sql})")
410 411
  end

412
  # Returns projects user is authorized to access.
413 414
  def authorized_projects(min_access_level = nil)
    Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
415 416
  end

417 418
  def viewable_starred_projects
    starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})",
Sean McGivern's avatar
Sean McGivern committed
419
                           [Project::PUBLIC, Project::INTERNAL])
420 421
  end

422
  def owned_projects
423
    @owned_projects ||=
424 425
      Project.where('namespace_id IN (?) OR namespace_id = ?',
                    owned_groups.select(:id), namespace.id).joins(:namespace)
426 427
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
428 429 430 431 432 433 434 435
  def is_admin?
    admin
  end

  def require_ssh_key?
    keys.count == 0
  end

436 437 438 439
  def require_password?
    password_automatically_set? && !ldap_user?
  end

440
  def can_change_username?
441
    gitlab_config.username_changing_enabled
442 443
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
444
  def can_create_project?
445
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
446 447 448
  end

  def can_create_group?
449
    can?(:create_group, nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
450 451 452
  end

  def abilities
Ciro Santilli's avatar
Ciro Santilli committed
453
    Ability.abilities
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
454 455
  end

456 457 458 459
  def can_select_namespace?
    several_namespaces? || admin
  end

460
  def can?(action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
461 462 463 464 465 466 467 468
    abilities.allowed?(self, action, subject)
  end

  def first_name
    name.split.first unless name.blank?
  end

  def cared_merge_requests
469
    MergeRequest.cared(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
470 471
  end

472
  def projects_limit_left
473
    projects_limit - personal_projects.count
474 475
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
476 477
  def projects_limit_percent
    return 100 if projects_limit.zero?
478
    (personal_projects.count.to_f / projects_limit) * 100
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
479 480
  end

481
  def recent_push(project_id = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
482 483 484 485
    # Get push events not earlier than 2 hours ago
    events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
    events = events.where(project_id: project_id) if project_id

486 487 488 489 490 491 492 493 494 495 496 497 498
    # Use the latest event that has not been pushed or merged recently
    events.recent.find do |event|
      project = Project.find_by_id(event.project_id)
      next unless project
      repo = project.repository

      if repo.branch_names.include?(event.branch_name)
        merge_requests = MergeRequest.where("created_at >= ?", event.created_at).
            where(source_project_id: project.id,
                  source_branch: event.branch_name)
        merge_requests.empty?
      end
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
499 500 501 502 503 504 505
  end

  def projects_sorted_by_activity
    authorized_projects.sorted_by_activity
  end

  def several_namespaces?
506
    owned_groups.any? || masters_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
507 508 509 510 511
  end

  def namespace_id
    namespace.try :id
  end
512

513 514 515
  def name_with_username
    "#{name} (#{username})"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
516

517
  def already_forked?(project)
518 519 520
    !!fork_of(project)
  end

521
  def fork_of(project)
522 523 524 525 526 527 528 529
    links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects)

    if links.any?
      links.first.forked_to_project
    else
      nil
    end
  end
530 531

  def ldap_user?
532 533 534 535 536
    identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
  end

  def ldap_identity
    @ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"])
537
  end
538

539
  def project_deploy_keys
540
    DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id)
541 542
  end

543
  def accessible_deploy_keys
544 545 546 547 548
    @accessible_deploy_keys ||= begin
      key_ids = project_deploy_keys.pluck(:id)
      key_ids.push(*DeployKey.are_public.pluck(:id))
      DeployKey.where(id: key_ids)
    end
549
  end
550 551

  def created_by
skv's avatar
skv committed
552
    User.find_by(id: created_by_id) if created_by_id
553
  end
554 555

  def sanitize_attrs
556
    %w(name username skype linkedin twitter).each do |attr|
557 558 559 560
      value = self.send(attr)
      self.send("#{attr}=", Sanitize.clean(value)) if value.present?
    end
  end
561

562 563
  def set_notification_email
    if self.notification_email.blank? || !self.all_emails.include?(self.notification_email)
564
      self.notification_email = self.email
565 566 567
    end
  end

568 569
  def set_public_email
    if self.public_email.blank? || !self.all_emails.include?(self.public_email)
570
      self.public_email = ''
571 572 573
    end
  end

574 575 576 577 578 579
  def update_secondary_emails!
    self.set_notification_email
    self.set_public_email
    self.save if self.notification_email_changed? || self.public_email_changed?
  end

580 581 582 583 584 585 586
  def set_projects_limit
    connection_default_value_defined = new_record? && !projects_limit_changed?
    return unless self.projects_limit.nil? || connection_default_value_defined

    self.projects_limit = current_application_settings.default_projects_limit
  end

587
  def requires_ldap_check?
588 589 590
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
591 592 593 594 595 596
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
597 598 599 600 601 602 603
  def try_obtain_ldap_lease
    # After obtaining this lease LDAP checks will be blocked for 600 seconds
    # (10 minutes) for this user.
    lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600)
    lease.try_obtain
  end

604 605 606 607 608
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
609 610

  def with_defaults
611 612
    User.defaults.each do |k, v|
      self.send("#{k}=", v)
613
    end
614 615

    self
616
  end
617

618 619 620 621
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
622 623 624 625 626 627 628 629 630 631 632 633 634 635

  # Reset project events cache related to this user
  #
  # Since we do cache @event we need to reset cache in special cases:
  # * when the user changes their avatar
  # Events cache stored like  events/23-20130109142513.
  # The cache key includes updated_at timestamp.
  # Thus it will automatically generate a new fragment
  # when the event is updated because the key changes.
  def reset_events_cache
    Event.where(author_id: self.id).
      order('id DESC').limit(1000).
      update_all(updated_at: Time.now)
  end
Jerome Dalbert's avatar
Jerome Dalbert committed
636 637

  def full_website_url
638
    return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
Jerome Dalbert's avatar
Jerome Dalbert committed
639 640 641 642 643

    website_url
  end

  def short_website_url
644
    website_url.sub(/\Ahttps?:\/\//, '')
Jerome Dalbert's avatar
Jerome Dalbert committed
645
  end
GitLab's avatar
GitLab committed
646

647
  def all_ssh_keys
648
    keys.map(&:publishable_key)
649
  end
650 651

  def temp_oauth_email?
652
    email.start_with?('temp-email-for-oauth')
653 654
  end

655
  def avatar_url(size = nil, scale = 2)
656
    if avatar.present?
657
      [gitlab_config.url, avatar.url].join
658
    else
659
      GravatarService.new.execute(email, size, scale)
660 661
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
662

663
  def all_emails
664 665 666 667
    all_emails = []
    all_emails << self.email unless self.temp_oauth_email?
    all_emails.concat(self.emails.map(&:email))
    all_emails
668 669
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
670 671 672 673 674 675 676 677
  def hook_attrs
    {
      name: name,
      username: username,
      avatar_url: avatar_url
    }
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
678 679 680 681 682 683 684 685 686 687 688
  def ensure_namespace_correct
    # Ensure user has namespace
    self.create_namespace!(path: self.username, name: self.username) unless self.namespace

    if self.username_changed?
      self.namespace.update_attributes(path: self.username, name: self.username)
    end
  end

  def post_create_hook
    log_info("User \"#{self.name}\" (#{self.email}) was created")
689
    notification_service.new_user(self, @reset_token) if self.created_by_id
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
690 691 692 693 694 695 696 697
    system_hook_service.execute_hooks_for(self, :create)
  end

  def post_destroy_hook
    log_info("User \"#{self.name}\" (#{self.email})  was removed")
    system_hook_service.execute_hooks_for(self, :destroy)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
698
  def notification_service
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
699 700 701
    NotificationService.new
  end

702
  def log_info(message)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
703 704 705 706 707 708
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
Ciro Santilli's avatar
Ciro Santilli committed
709 710

  def starred?(project)
711
    starred_projects.exists?(project.id)
Ciro Santilli's avatar
Ciro Santilli committed
712 713 714
  end

  def toggle_star(project)
715 716 717 718 719 720 721 722 723
    UsersStarProject.transaction do
      user_star_project = users_star_projects.
          where(project: project, user: self).lock(true).first

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
Ciro Santilli's avatar
Ciro Santilli committed
724 725
    end
  end
726 727

  def manageable_namespaces
728
    @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
729
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
730

731 732 733 734 735 736
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
737 738 739
  def oauth_authorized_tokens
    Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
  end
740

741 742 743 744 745 746 747 748 749
  # Returns the projects a user contributed to in the last year.
  #
  # This method relies on a subquery as this performs significantly better
  # compared to a JOIN when coupled with, for example,
  # `Project.visible_to_user`. That is, consider the following code:
  #
  #     some_user.contributed_projects.visible_to_user(other_user)
  #
  # If this method were to use a JOIN the resulting query would take roughly 200
750
  # ms on a database with a similar size to GitLab.com's database. On the other
751 752 753 754
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
    events = Event.select(:project_id).
      contributions.where(author_id: self).
755
      where("created_at > ?", Time.now - 1.year).
756
      uniq.
757 758 759
      reorder(nil)

    Project.where(id: events)
760
  end
761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783

  def restricted_signup_domains
    email_domains = current_application_settings.restricted_signup_domains

    unless email_domains.blank?
      match_found = email_domains.any? do |domain|
        escaped = Regexp.escape(domain).gsub('\*','.*?')
        regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
        email_domain = Mail::Address.new(self.email).domain
        email_domain =~ regexp
      end

      unless match_found
        self.errors.add :email,
                        'is not whitelisted. ' +
                        'Email domains valid for registration are: ' +
                        email_domains.join(', ')
        return false
      end
    end

    true
  end
784 785 786 787

  def can_be_removed?
    !solo_owned_groups.present?
  end
788 789

  def ci_authorized_runners
790
    @ci_authorized_runners ||= begin
791 792
      runner_ids = Ci::RunnerProject.
        where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})").
793
        select(:runner_id)
794 795
      Ci::Runner.specific.where(id: runner_ids)
    end
796
  end
797

798 799 800 801
  def notification_settings_for(source)
    notification_settings.find_or_initialize_by(source: source)
  end

802 803 804
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
805 806 807 808 809 810
    return @global_notification_setting if defined?(@global_notification_setting)

    @global_notification_setting = notification_settings.find_or_initialize_by(source: nil)
    @global_notification_setting.update_attributes(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?

    @global_notification_setting
811 812
  end

813 814
  def assigned_open_merge_request_count(force: false)
    Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do
815 816 817 818
      assigned_merge_requests.opened.count
    end
  end

819 820
  def assigned_open_issues_count(force: false)
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
821 822
      assigned_issues.opened.count
    end
823 824
  end

825 826 827 828 829
  def update_cache_counts
    assigned_open_merge_request_count(force: true)
    assigned_open_issues_count(force: true)
  end

830 831
  private

832
  def projects_union(min_access_level = nil)
833 834 835 836 837 838 839 840 841 842
    relations = [personal_projects.select(:id),
                 groups_projects.select(:id),
                 projects.select(:id),
                 groups.joins(:shared_projects).select(:project_id)]


    if min_access_level
      scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } }
      relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) }
    end
843 844

    Gitlab::SQL::Union.new(relations)
845
  end
846 847 848 849 850 851 852 853 854

  def ci_projects_union
    scope  = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
    groups = groups_projects.where(members: scope)
    other  = projects.where(members: scope)

    Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id),
                            other.select(:id)])
  end
855 856 857 858 859

  # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
  def send_devise_notification(notification, *args)
    devise_mailer.send(notification, self, *args).deliver_later
  end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
860 861 862 863 864 865 866

  def ensure_external_user_rights
    return unless self.external?

    self.can_create_group   = false
    self.projects_limit     = 0
  end
gitlabhq's avatar
gitlabhq committed
867
end