app/models/user.rb
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
class User < ApplicationModel
include CanBeImported
include HasActivityStreamLog
include ChecksClientNotification
include HasHistory
include HasSearchIndexBackend
include CanCsvImport
include ChecksHtmlSanitized
include HasGroups
include HasRoles
include HasObjectManagerAttributes
include HasTaskbars
include HasTwoFactor
include CanSelector
include CanPerformChanges
include User::Assets
include User::Avatar
include User::Search
include User::SearchIndex
include User::TouchesOrganization
include User::TriggersSubscriptions
include User::PerformsGeoLookup
include User::UpdatesTicketOrganization
has_and_belongs_to_many :organizations, after_add: %i[cache_update create_organization_add_history], after_remove: %i[cache_update create_organization_remove_history], class_name: 'Organization'
has_and_belongs_to_many :overviews, dependent: :nullify
has_many :tokens, after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
has_many :authorizations, after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
has_many :online_notifications, dependent: :destroy
has_many :taskbars, dependent: :destroy
has_many :user_devices, dependent: :destroy
has_one :chat_agent_created_by, class_name: 'Chat::Agent', foreign_key: :created_by_id, dependent: :destroy, inverse_of: :created_by
has_one :chat_agent_updated_by, class_name: 'Chat::Agent', foreign_key: :updated_by_id, dependent: :destroy, inverse_of: :updated_by
has_many :chat_sessions, class_name: 'Chat::Session', dependent: :destroy
has_many :mentions, dependent: :destroy
has_many :cti_caller_ids, class_name: 'Cti::CallerId', dependent: :destroy
has_many :customer_tickets, class_name: 'Ticket', foreign_key: :customer_id, dependent: :destroy, inverse_of: :customer
has_many :owner_tickets, class_name: 'Ticket', foreign_key: :owner_id, inverse_of: :owner
has_many :overview_sortings, dependent: :destroy
has_many :created_recent_views, class_name: 'RecentView', foreign_key: :created_by_id, dependent: :destroy, inverse_of: :created_by
has_many :permissions, -> { where(roles: { active: true }, active: true) }, through: :roles
has_many :data_privacy_tasks, as: :deletable
belongs_to :organization, inverse_of: :members, optional: true
before_validation :check_name, :check_email, :check_login, :ensure_password, :ensure_roles, :ensure_organizations, :ensure_organizations_limit
before_validation :check_mail_delivery_failed, on: :update
before_save :ensure_notification_preferences, if: :reset_notification_config_before_save
before_create :validate_preferences, :validate_ooo, :domain_based_assignment, :set_locale
before_update :validate_preferences, :validate_ooo, :reset_login_failed_after_password_change, :validate_agent_limit_by_attributes, :last_admin_check_by_attribute
before_destroy :destroy_longer_required_objects, :destroy_move_dependency_ownership
after_commit :update_caller_id
validate :ensure_identifier, :ensure_email
validate :ensure_uniq_email, unless: :skip_ensure_uniq_email
available_perform_change_actions :data_privacy_deletion_task, :attribute_updates
# workflow checks should run after before_create and before_update callbacks
# the transaction dispatcher must be run after the workflow checks!
include ChecksCoreWorkflow
include HasTransactionDispatcher
core_workflow_screens 'create', 'edit', 'invite_agent'
core_workflow_admin_screens 'create', 'edit'
store :preferences
association_attributes_ignored :online_notifications,
:templates,
:taskbars,
:user_devices,
:chat_sessions,
:cti_caller_ids,
:text_modules,
:customer_tickets,
:owner_tickets,
:created_recent_views,
:chat_agents,
:data_privacy_tasks,
:overviews,
:mentions,
:permissions
activity_stream_permission 'admin.user'
activity_stream_attributes_ignored :last_login,
:login_failed,
:image,
:image_source,
:preferences
history_attributes_ignored :password,
:last_login,
:image,
:image_source,
:preferences
search_index_attributes_ignored :password,
:image,
:image_source,
:source,
:login_failed
csv_object_ids_ignored 1
csv_attributes_ignored :password,
:login_failed,
:source,
:image_source,
:image,
:authorizations,
:groups,
:user_groups
validates :note, length: { maximum: 5000 }
sanitized_html :note, no_images: true
def ignore_search_indexing?(_action)
# ignore internal user
return true if id == 1
false
end
=begin
fullname of user
user = User.find(123)
result = user.fullname
returns
result = "Bob Smith"
=end
def fullname(email_fallback: true)
name = "#{firstname} #{lastname}".strip
if name.blank? && email.present? && email_fallback
return email
end
name
end
=begin
longname of user
user = User.find(123)
result = user.longname
returns
result = "Bob Smith"
or with org
result = "Bob Smith (Org ABC)"
=end
def longname
name = fullname
if organization_id
organization = Organization.lookup(id: organization_id)
if organization
name += " (#{organization.name})"
end
end
name
end
=begin
check if user is in role
user = User.find(123)
result = user.role?('Customer')
result = user.role?(['Agent', 'Admin'])
returns
result = true|false
=end
def role?(role_name)
roles.where(name: role_name).any?
end
=begin
check if user is in role
user = User.find(123)
result = user.out_of_office?
returns
result = true|false
=end
def out_of_office?
return false if out_of_office != true
return false if out_of_office_start_at.blank?
return false if out_of_office_end_at.blank?
Time.use_zone(Setting.get('timezone_default')) do
start = out_of_office_start_at.beginning_of_day
finish = out_of_office_end_at.end_of_day
Time.zone.now.between? start, finish
end
end
=begin
check if user is in role
user = User.find(123)
result = user.out_of_office_agent
returns
result = user_model
=end
def out_of_office_agent(loop_user_ids: [], stack_depth: 10)
return if !out_of_office?
return if out_of_office_replacement_id.blank?
if stack_depth.zero?
Rails.logger.warn("Found more than 10 replacement levels for agent #{self}.")
return self
end
user = User.find_by(id: out_of_office_replacement_id)
# stop if users are occuring multiple times to prevent endless loops
return user if loop_user_ids.include?(out_of_office_replacement_id)
loop_user_ids |= [out_of_office_replacement_id]
ooo_agent = user.out_of_office_agent(loop_user_ids: loop_user_ids, stack_depth: stack_depth - 1)
return ooo_agent if ooo_agent.present?
user
end
=begin
gets users where user is replacement
user = User.find(123)
result = user.out_of_office_agent_of
returns
result = [user_model1, user_model2]
=end
def out_of_office_agent_of
User.where(id: out_of_office_agent_of_recursive(user_id: id))
end
scope :out_of_office, lambda { |user, interval_start = Time.zone.today, interval_end = Time.zone.today|
where(active: true, out_of_office: true, out_of_office_replacement_id: user)
.where('out_of_office_start_at <= ? AND out_of_office_end_at >= ?', interval_start, interval_end)
}
def someones_out_of_office_replacement?
self.class.out_of_office(self).exists?
end
def out_of_office_agent_of_recursive(user_id:, result: [])
self.class.out_of_office(user_id).each do |user|
# stop if users are occuring multiple times to prevent endless loops
break if result.include?(user.id)
result |= [user.id]
result |= out_of_office_agent_of_recursive(user_id: user.id, result: result)
end
result
end
=begin
get users activity stream
user = User.find(123)
result = user.activity_stream(20)
returns
result = [
{
id: 2,
o_id: 2,
created_by_id: 3,
created_at: '2013-09-28 00:57:21',
object: "User",
type: "created",
},
{
id: 2,
o_id: 2,
created_by_id: 3,
created_at: '2013-09-28 00:59:21',
object: "User",
type: "updated",
},
]
=end
def activity_stream(limit, fulldata = false)
stream = ActivityStream.list(self, limit)
return stream if !fulldata
# get related objects
assets = {}
stream.each do |item|
assets = item.assets(assets)
end
{
stream: stream,
assets: assets,
}
end
=begin
tries to find the matching instance by the given identifier. Currently email and login is supported.
user = User.indentify('User123')
# or
user = User.indentify('user-123@example.com')
returns
# User instance
user.login # 'user123'
=end
def self.identify(identifier)
return if identifier.blank?
# try to find user based on login
user = User.find_by(login: identifier.downcase)
return user if user
# try second lookup with email
User.find_by(email: identifier.downcase)
end
=begin
create user from from omni auth hash
result = User.create_from_hash!(hash)
returns
result = user_model # user model if create was successfully
=end
def self.create_from_hash!(hash)
url = ''
hash['info']['urls']&.each_value do |local_url|
next if local_url.blank?
url = local_url
end
begin
data = {
login: hash['info']['nickname'] || hash['uid'],
firstname: hash['info']['name'] || hash['info']['display_name'],
email: hash['info']['email'],
image_source: hash['info']['image'],
web: url,
address: hash['info']['location'],
note: hash['info']['description'],
source: hash['provider'],
role_ids: Role.signup_role_ids,
updated_by_id: 1,
created_by_id: 1,
}
if hash['info']['first_name'].present? && hash['info']['last_name'].present?
data[:firstname] = hash['info']['first_name']
data[:lastname] = hash['info']['last_name']
end
create!(data)
rescue => e
logger.error e
raise Exceptions::UnprocessableEntity, e.message
end
end
=begin
returns all accessable permission ids of user
user = User.find(123)
user.permissions_with_child_ids
returns
[permission1_id, permission2_id, permission3_id]
=end
def permissions_with_child_ids
permissions_with_child_elements.pluck(:id)
end
=begin
returns all accessable permission names of user
user = User.find(123)
user.permissions_with_child_names
returns
[permission1_name, permission2_name, permission3_name]
=end
def permissions_with_child_names
permissions_with_child_elements.pluck(:name)
end
def permissions?(permissions)
permissions!(permissions)
true
rescue Exceptions::Forbidden
false
end
def permissions!(auth_query)
return true if Auth::RequestCache.permissions?(self, auth_query)
raise Exceptions::Forbidden, __('Not authorized (user)!')
end
=begin
get all users with permission
users = User.with_permissions('ticket.agent')
get all users with permission "admin.session" or "ticket.agent"
users = User.with_permissions(['admin.session', 'ticket.agent'])
returns
[user1, user2, ...]
=end
def self.with_permissions(keys)
if keys.class != Array
keys = [keys]
end
total_role_ids = []
permission_ids = []
keys.each do |key|
role_ids = []
::Permission.with_parents(key).each do |local_key|
permission = ::Permission.lookup(name: local_key)
next if !permission
permission_ids.push permission.id
end
next if permission_ids.blank?
Role.joins(:permissions_roles).joins(:permissions).where('permissions_roles.permission_id IN (?) AND roles.active = ? AND permissions.active = ?', permission_ids, true, true).distinct.pluck(:id).each do |role_id|
role_ids.push role_id
end
total_role_ids.push role_ids
end
return [] if total_role_ids.blank?
condition = ''
total_role_ids.each do |_role_ids|
if condition != ''
condition += ' OR '
end
condition += 'roles_users.role_id IN (?)'
end
User.joins(:roles_users).where("(#{condition}) AND users.active = ?", *total_role_ids, true).distinct.reorder(:id)
end
# Find a user by mobile number, either directly or by number variants stored in the Cti::CallerIds.
def self.by_mobile(number:)
direct_lookup = User.where(mobile: number).reorder(:updated_at).first
return direct_lookup if direct_lookup
cti_lookup = Cti::CallerId.lookup(number.delete('+')).find { |id| id.level == 'known' && id.object == 'User' }
User.find_by(id: cti_lookup.o_id) if cti_lookup
end
=begin
generate new token for reset password
result = User.password_reset_new_token(username)
returns
result = {
token: token,
user: user,
}
=end
def self.password_reset_new_token(username)
return if username.blank?
# try to find user based on login
user = User.find_by(login: username.downcase.strip, active: true)
# try second lookup with email
user ||= User.find_by(email: username.downcase.strip, active: true)
return if !user || !user.email
# Discard any possible previous tokens for safety reasons.
Token.where(action: 'PasswordReset', user_id: user.id).destroy_all
{
token: Token.create(action: 'PasswordReset', user_id: user.id, persistent: false),
user: user,
}
end
=begin
returns the User instance for a given password token if found
result = User.by_reset_token(token)
returns
result = user_model # user_model if token was verified
=end
def self.by_reset_token(token)
Token.check(action: 'PasswordReset', token: token)
end
=begin
reset password with token and set new password
result = User.password_reset_via_token(token,password)
returns
result = user_model # user_model if token was verified
=end
def self.password_reset_via_token(token, password)
# check token
user = by_reset_token(token)
return if !user
# reset password
user.update!(password: password, verified: true)
# delete token
Token.find_by(action: 'PasswordReset', token: token).destroy
user
end
def self.admin_password_auth_new_token(username)
return if username.blank?
# try to find user based on login
user = User.find_by(login: username.downcase.strip, active: true)
# try second lookup with email
user ||= User.find_by(email: username.downcase.strip, active: true)
return if !user || !user.email
return if !user.permissions?('admin.*')
# Discard any possible previous tokens for safety reasons.
Token.where(action: 'AdminAuth', user_id: user.id).destroy_all
{
token: Token.create(action: 'AdminAuth', user_id: user.id, persistent: false),
user: user,
}
end
def self.admin_password_auth_via_token(token)
user = Token.check(action: 'AdminAuth', token: token)
return if !user
Token.find_by(action: 'AdminAuth', token: token).destroy
user
end
=begin
update last login date and reset login_failed (is automatically done by auth and sso backend)
user = User.find(123)
result = user.update_last_login
returns
result = new_user_model
=end
def update_last_login
# reduce DB/ES load by updating last_login every 10 minutes only
if !last_login || last_login < 10.minutes.ago
self.last_login = Time.zone.now
end
# reset login failed
self.login_failed = 0
save
end
=begin
generate new token for signup
result = User.signup_new_token(user) # or email
returns
result = {
token: token,
user: user,
}
=end
def self.signup_new_token(user)
return if !user
return if !user.email
# Discard any possible previous tokens for safety reasons.
Token.where(action: 'Signup', user_id: user.id).destroy_all
# generate token
token = Token.create(action: 'Signup', user_id: user.id)
{
token: token,
user: user,
}
end
=begin
verify signup with token
result = User.signup_verify_via_token(token, user)
returns
result = user_model # user_model if token was verified
=end
def self.signup_verify_via_token(token, user = nil)
# check token
local_user = Token.check(action: 'Signup', token: token)
return if !local_user
# if requested user is different to current user
return if user && local_user.id != user.id
# set verified
local_user.update!(verified: true)
# delete token
Token.find_by(action: 'Signup', token: token).destroy
local_user
end
=begin
merge two users to one
user = User.find(123)
result = user.merge(user_id_of_duplicate_user)
returns
result = new_user_model
=end
def merge(user_id_of_duplicate_user)
# Raise an exception if the user is not found (?)
#
# (This line used to contain a useless variable assignment,
# and was changed to satisfy the linter.
# We're not certain of its original intention,
# so the User.find call has been kept
# to prevent any unexpected regressions.)
User.find(user_id_of_duplicate_user)
# mentions can not merged easily because the new user could have mentioned
# the same ticket so we delete duplicates beforehand
Mention.where(user_id: user_id_of_duplicate_user).find_each do |mention|
if Mention.exists?(mentionable: mention.mentionable, user_id: id)
mention.destroy
else
mention.update(user_id: id)
end
end
# merge missing attributes
Models.merge('User', id, user_id_of_duplicate_user)
true
end
=begin
list of active users in role
result = User.of_role('Agent', group_ids)
result = User.of_role(['Agent', 'Admin'])
returns
result = [user1, user2]
=end
def self.of_role(role, group_ids = nil)
roles_ids = Role.where(active: true, name: role).map(&:id)
if !group_ids
return User.where(active: true).joins(:roles_users).where('roles_users.role_id' => roles_ids).reorder('users.updated_at DESC')
end
User.where(active: true)
.joins(:roles_users)
.joins(:users_groups)
.where('roles_users.role_id IN (?) AND users_groups.group_ids IN (?)', roles_ids, group_ids).reorder('users.updated_at DESC')
end
# Reset agent notification preferences
# Non-agent cannot receive notifications, thus notifications reset
#
# @option user [User] to reset preferences
def self.reset_notifications_preferences!(user)
return if !user.permissions? 'ticket.agent'
user.fill_notification_config_preferences
user.save!
end
=begin
try to find correct name
[firstname, lastname] = User.name_guess('Some Name', 'some.name@example.com')
=end
def self.name_guess(string, email = nil)
return if string.blank? && email.blank?
string.strip!
firstname = ''
lastname = ''
# "Lastname, Firstname"
if string.match?(',')
name = string.split(', ', 2)
if name.count == 2
if name[0].present?
lastname = name[0].strip
end
if name[1].present?
firstname = name[1].strip
end
return [firstname, lastname] if firstname.present? || lastname.present?
end
end
# "Firstname Lastname"
if string =~ %r{^(((Dr\.|Prof\.)[[:space:]]|).+?)[[:space:]](.+?)$}i
if $1.present?
firstname = $1.strip
end
if $4.present?
lastname = $4.strip
end
return [firstname, lastname] if firstname.present? || lastname.present?
end
# -no name- "firstname.lastname@example.com"
if string.blank? && email.present?
scan = email.scan(%r{^(.+?)\.(.+?)@.+?$})
if scan[0].present?
if scan[0][0].present?
firstname = scan[0][0].strip
end
if scan[0][1].present?
lastname = scan[0][1].strip
end
return [firstname, lastname] if firstname.present? || lastname.present?
end
end
nil
end
def no_name?
firstname.blank? && lastname.blank?
end
# get locale identifier of user or system if user's own is not set
def locale
preferences.fetch(:locale) { Locale.default }
end
attr_accessor :skip_ensure_uniq_email
def shared_organizations?
all_organizations.exists? shared: true
end
def all_organizations
Organization.where(id: all_organization_ids)
end
def all_organization_ids
([organization_id] + organization_ids).uniq
end
def organization_id?(organization_id)
all_organization_ids.include?(organization_id)
end
def create_organization_add_history(org)
organization_history_log(org, 'added')
end
def create_organization_remove_history(org)
organization_history_log(org, 'removed')
end
def fill_notification_config_preferences
preferences[:notification_config] ||= {}
preferences[:notification_config][:matrix] = Setting.get('ticket_agent_default_notifications')
end
private
def organization_history_log(org, type)
return if id.blank?
attributes = {
history_attribute: 'organization_ids',
id_to: org.id,
value_to: org.name
}
history_log(type, id, attributes)
end
def check_name
self.firstname = sanitize_name(firstname)
self.lastname = sanitize_name(lastname)
return if firstname.present? && lastname.present?
if (firstname.blank? && lastname.present?) || (firstname.present? && lastname.blank?)
used_name = firstname.presence || lastname
(local_firstname, local_lastname) = User.name_guess(used_name, email)
elsif firstname.blank? && lastname.blank? && email.present?
(local_firstname, local_lastname) = User.name_guess('', email)
end
check_name_apply(:firstname, local_firstname)
check_name_apply(:lastname, local_lastname)
end
def sanitize_name(value)
result = value&.strip
return result if result.blank?
result.split(%r{\s}).map { |v| strip_uri(v) }.join("\s")
end
def strip_uri(value)
uri = URI.parse(value)
return value if !uri || uri.scheme.blank? || uri.hostname.blank?
# Strip the scheme from the URI.
uri.hostname + uri.path
rescue
value
end
def check_name_apply(identifier, input)
self[identifier] = input if input.present?
self[identifier].capitalize! if self[identifier]&.match? %r{^([[:upper:]]+|[[:lower:]]+)$}
end
def check_email
return if Setting.get('import_mode')
return if email.blank?
# https://bugs.chromium.org/p/chromium/issues/detail?id=410937
self.email = EmailHelper::Idn.to_unicode(email).downcase.strip
end
def ensure_email
return if Setting.get('import_mode')
return if email.blank?
return if id == 1
email_address_validation = EmailAddressValidation.new(email)
return if email_address_validation.valid?
errors.add :base, __("Invalid email '%{email}'"), email: email
end
def check_login
# use email as login if not given
if login.blank?
self.login = email
end
# if email has changed, login is old email, change also login
if email_changed? && email_was == login
self.login = email
end
# generate auto login
if login.blank?
self.login = "auto-#{SecureRandom.uuid}"
end
# check if login already exists
base_login = login.downcase.strip
alternatives = [nil] + Array(1..20) + [ SecureRandom.uuid ]
alternatives.each do |suffix|
self.login = "#{base_login}#{suffix}"
exists = User.find_by(login: login)
return true if !exists || exists.id == id
end
raise Exceptions::UnprocessableEntity, "Invalid user login generation for login #{login}!"
end
def check_mail_delivery_failed
return if email_change.blank?
preferences.delete(:mail_delivery_failed)
end
def ensure_roles
return if role_ids.present?
self.role_ids = Role.signup_role_ids
end
def ensure_identifier
return if login.present? && !login.start_with?('auto-')
return if [email, firstname, lastname, phone].any?(&:present?)
errors.add :base, __('At least one identifier (firstname, lastname, phone or email) for user is required.')
end
def ensure_uniq_email
return if Setting.get('user_email_multiple_use')
return if Setting.get('import_mode')
return if email.blank?
return if !email_changed?
return if !User.exists?(email: email.downcase.strip)
errors.add :base, __("Email address '%{email}' is already used for another user."), email: email.downcase.strip
end
def ensure_organizations
return if organization_ids.blank?
return if organization_id.present?
errors.add :base, __('Secondary organizations are only allowed when the primary organization is given.')
end
def ensure_organizations_limit
return if organization_ids.size <= 250
errors.add :base, __('More than 250 secondary organizations are not allowed.')
end
def permissions_with_child_elements
where = ''
where_bind = [true]
permissions.pluck(:name).each do |permission_name|
where += ' OR ' if where != ''
where += 'permissions.name = ? OR permissions.name LIKE ?'
where_bind.push permission_name
where_bind.push "#{SqlHelper.quote_like(permission_name)}.%"
end
return [] if where == ''
::Permission.where("permissions.active = ? AND (#{where})", *where_bind)
end
def validate_roles(role)
return true if !role_ids # we need role_ids for checking in role_ids below, in this method
return true if role.preferences[:not].blank?
role.preferences[:not].each do |local_role_name|
local_role = Role.lookup(name: local_role_name)
next if !local_role
next if role_ids.exclude?(local_role.id)
raise "Role #{role.name} conflicts with #{local_role.name}"
end
true
end
def validate_ooo
return true if out_of_office != true
raise Exceptions::UnprocessableEntity, 'out of office start is required' if out_of_office_start_at.blank?
raise Exceptions::UnprocessableEntity, 'out of office end is required' if out_of_office_end_at.blank?
raise Exceptions::UnprocessableEntity, 'out of office end is before start' if out_of_office_start_at > out_of_office_end_at
raise Exceptions::UnprocessableEntity, 'out of office replacement user is required' if out_of_office_replacement_id.blank?
raise Exceptions::UnprocessableEntity, 'out of office no such replacement user' if !User.exists?(id: out_of_office_replacement_id)
true
end
def validate_preferences
return true if !changes
return true if !changes['preferences']
return true if preferences.blank?
return true if !preferences[:notification_sound]
return true if !preferences[:notification_sound][:enabled]
case preferences[:notification_sound][:enabled]
when 'true'
preferences[:notification_sound][:enabled] = true
when 'false'
preferences[:notification_sound][:enabled] = false
end
class_name = preferences[:notification_sound][:enabled].class.to_s
raise Exceptions::UnprocessableEntity, "preferences.notification_sound.enabled needs to be an boolean, but it was a #{class_name}" if class_name != 'TrueClass' && class_name != 'FalseClass'
true
end
def ensure_notification_preferences
fill_notification_config_preferences
self.reset_notification_config_before_save = false
end
=begin
checks if the current user is the last one with admin permissions.
Raises
raise 'At least one user need to have admin permissions'
=end
def last_admin_check_by_attribute
return true if !will_save_change_to_attribute?('active')
return true if active != false
return true if !permissions?(['admin', 'admin.user'])
raise Exceptions::UnprocessableEntity, __('At least one user needs to have admin permissions.') if last_admin_check_admin_count < 1
true
end
def last_admin_check_by_role(role)
return true if Setting.get('import_mode')
return true if !role.with_permission?(['admin', 'admin.user'])
raise Exceptions::UnprocessableEntity, __('At least one user needs to have admin permissions.') if last_admin_check_admin_count < 1
true
end
def last_admin_check_admin_count
admin_role_ids = Role.joins(:permissions).where(permissions: { name: ['admin', 'admin.user'], active: true }, roles: { active: true }).pluck(:id)
User.joins(:roles).where(roles: { id: admin_role_ids }, users: { active: true }).distinct.count - 1
end
def validate_agent_limit_by_attributes
return true if Setting.get('system_agent_limit').blank?
return true if !will_save_change_to_attribute?('active')
return true if active != true
return true if !permissions?('ticket.agent')
ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent', active: true }, roles: { active: true }).pluck(:id)
count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).distinct.count + 1
raise Exceptions::UnprocessableEntity, __('Agent limit exceeded, please check your account settings.') if count > Setting.get('system_agent_limit').to_i
true
end
def validate_agent_limit_by_role(role)
return true if Setting.get('system_agent_limit').blank?
return true if active != true
return true if role.active != true
return true if !role.with_permission?('ticket.agent')
ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent', active: true }, roles: { active: true }).pluck(:id)
count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).distinct.count
# if new added role is a ticket.agent role
if ticket_agent_role_ids.include?(role.id)
# if user already has a ticket.agent role
hint = false
role_ids.each do |locale_role_id|
next if ticket_agent_role_ids.exclude?(locale_role_id)
hint = true
break
end
# user has not already a ticket.agent role
if hint == false
count += 1
end
end
raise Exceptions::UnprocessableEntity, __('Agent limit exceeded, please check your account settings.') if count > Setting.get('system_agent_limit').to_i
true
end
def domain_based_assignment
return true if !email
return true if organization_id
begin
domain = Mail::Address.new(email).domain
return true if !domain
organization = Organization.find_by(domain: domain.downcase, domain_assignment: true)
return true if !organization
self.organization_id = organization.id
rescue
return true
end
true
end
# sets locale of the user
def set_locale
# set the user's locale to the one of the "executing" user
return true if !UserInfo.current_user_id
user = User.find_by(id: UserInfo.current_user_id)
return true if !user
return true if !user.preferences[:locale]
preferences[:locale] = user.preferences[:locale]
true
end
def destroy_longer_required_objects
::Avatar.remove(self.class.to_s, id)
::UserDevice.remove(id)
::StatsStore.where(stats_storable: self).destroy_all
end
def destroy_move_dependency_ownership
result = Models.references(self.class.to_s, id)
user_columns = %w[created_by_id updated_by_id out_of_office_replacement_id origin_by_id owner_id archived_by_id published_by_id internal_by_id]
result.each do |class_name, references|
next if class_name.blank?
next if references.blank?
ref_class = class_name.constantize
ref_update_columns = []
references.each do |column, reference_found|
next if !reference_found
if user_columns.include?(column)
ref_update_columns << column
elsif ref_class.exists?(column => id)
raise "Failed deleting references! Check logic for #{class_name}->#{column}."
end
end
next if ref_update_columns.blank?
where_sql = ref_update_columns.map { |column| "#{column} = #{id}" }.join(' OR ')
ref_class.where(where_sql).find_in_batches(batch_size: 1000) do |batch_list|
batch_list.each do |record|
ref_update_columns.each do |column|
next if record[column] != id
record[column] = 1
end
record.save!(validate: false)
rescue => e
Rails.logger.error e
end
end
end
true
end
def ensure_password
return if !password_changed?
self.password = ensured_password
end
def ensured_password
# ensure unset password for blank values of new users
return nil if new_record? && password.blank?
# don't permit empty password update for existing users
return password_was if password.blank?
# don't re-hash passwords
return password if PasswordHash.crypted?(password)
if !PasswordPolicy::MaxLength.valid? password
errors.add :password, __('is too long')
return nil
end
# hash the plaintext password
PasswordHash.crypt(password)
end
# reset login_failed if password is changed
def reset_login_failed_after_password_change
return true if !will_save_change_to_attribute?('password')
self.login_failed = 0
true
end
# When adding/removing a phone number from the User table,
# update caller ID table
# to adopt/orphan matching Cti::Logs accordingly
# (see https://github.com/zammad/zammad/issues/2057)
def update_caller_id
# skip if "phone/mobile" does not change, or changes like [nil, ""]
return if persisted? && previous_changes.slice(:phone, :mobile).values.flatten.none?(&:present?)
return if destroyed? && phone.blank?
Cti::CallerId.build(self)
end
end