# 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