user.rb 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. class User < ApplicationModel
  3. include CanBeImported
  4. include HasActivityStreamLog
  5. include ChecksClientNotification
  6. include HasHistory
  7. include HasSearchIndexBackend
  8. include CanCsvImport
  9. include ChecksHtmlSanitized
  10. include HasGroups
  11. include HasRoles
  12. include HasObjectManagerAttributes
  13. include HasTaskbars
  14. include HasTwoFactor
  15. include CanSelector
  16. include CanPerformChanges
  17. include User::Assets
  18. include User::Avatar
  19. include User::Search
  20. include User::SearchIndex
  21. include User::TouchesOrganization
  22. include User::TriggersSubscriptions
  23. include User::PerformsGeoLookup
  24. include User::UpdatesTicketOrganization
  25. include User::OutOfOffice
  26. include User::Permissions
  27. 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'
  28. has_and_belongs_to_many :overviews, dependent: :nullify
  29. has_many :tokens, after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
  30. has_many :authorizations, after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
  31. has_many :online_notifications, dependent: :destroy
  32. has_many :taskbars, dependent: :destroy
  33. has_many :user_devices, dependent: :destroy
  34. has_one :chat_agent_created_by, class_name: 'Chat::Agent', foreign_key: :created_by_id, dependent: :destroy, inverse_of: :created_by
  35. has_one :chat_agent_updated_by, class_name: 'Chat::Agent', foreign_key: :updated_by_id, dependent: :destroy, inverse_of: :updated_by
  36. has_many :chat_sessions, class_name: 'Chat::Session', dependent: :destroy
  37. has_many :mentions, dependent: :destroy
  38. has_many :cti_caller_ids, class_name: 'Cti::CallerId', dependent: :destroy
  39. has_many :customer_tickets, class_name: 'Ticket', foreign_key: :customer_id, dependent: :destroy, inverse_of: :customer
  40. has_many :owner_tickets, class_name: 'Ticket', foreign_key: :owner_id, inverse_of: :owner
  41. has_many :overview_sortings, dependent: :destroy
  42. has_many :created_recent_views, class_name: 'RecentView', foreign_key: :created_by_id, dependent: :destroy, inverse_of: :created_by
  43. has_many :data_privacy_tasks, as: :deletable
  44. belongs_to :organization, inverse_of: :members, optional: true
  45. before_validation :check_name, :check_email, :check_login, :ensure_password, :ensure_roles, :ensure_organizations, :ensure_organizations_limit
  46. before_validation :check_mail_delivery_failed, on: :update
  47. before_save :ensure_notification_preferences, if: :reset_notification_config_before_save
  48. before_create :validate_preferences, :domain_based_assignment, :set_locale
  49. before_update :validate_preferences, :reset_login_failed_after_password_change, :validate_agent_limit_by_attributes, :last_admin_check_by_attribute
  50. before_destroy :destroy_longer_required_objects, :destroy_move_dependency_ownership
  51. after_commit :update_caller_id
  52. validate :ensure_identifier, :ensure_email
  53. validate :ensure_uniq_email, unless: :skip_ensure_uniq_email
  54. available_perform_change_actions :data_privacy_deletion_task, :attribute_updates
  55. # workflow checks should run after before_create and before_update callbacks
  56. # the transaction dispatcher must be run after the workflow checks!
  57. include ChecksCoreWorkflow
  58. include HasTransactionDispatcher
  59. core_workflow_screens 'create', 'edit', 'invite_agent'
  60. core_workflow_admin_screens 'create', 'edit'
  61. store :preferences
  62. association_attributes_ignored :online_notifications,
  63. :templates,
  64. :taskbars,
  65. :user_devices,
  66. :chat_sessions,
  67. :cti_caller_ids,
  68. :text_modules,
  69. :customer_tickets,
  70. :owner_tickets,
  71. :created_recent_views,
  72. :chat_agents,
  73. :data_privacy_tasks,
  74. :overviews,
  75. :mentions
  76. activity_stream_permission 'admin.user'
  77. activity_stream_attributes_ignored :last_login,
  78. :login_failed,
  79. :image,
  80. :image_source,
  81. :preferences
  82. history_attributes_ignored :password,
  83. :last_login,
  84. :image,
  85. :image_source,
  86. :preferences
  87. search_index_attributes_ignored :password,
  88. :image,
  89. :image_source,
  90. :source,
  91. :login_failed
  92. csv_object_ids_ignored 1
  93. csv_attributes_ignored :password,
  94. :login_failed,
  95. :source,
  96. :image_source,
  97. :image,
  98. :authorizations,
  99. :groups,
  100. :user_groups,
  101. :two_factor_preferences,
  102. :chat_agent_created_by,
  103. :chat_agent_updated_by,
  104. :overview_sortings
  105. validates :note, length: { maximum: 5000 }
  106. sanitized_html :note, no_images: true
  107. def ignore_search_indexing?(_action)
  108. # ignore internal user
  109. return true if id == 1
  110. false
  111. end
  112. =begin
  113. fullname of user
  114. user = User.find(123)
  115. result = user.fullname
  116. returns
  117. result = "Bob Smith"
  118. =end
  119. def fullname(email_fallback: true)
  120. name = "#{firstname} #{lastname}".strip
  121. if name.blank? && email.present? && email_fallback
  122. return email
  123. end
  124. return name if name.present?
  125. %w[phone mobile].each do |item|
  126. next if self[item].blank?
  127. return self[item]
  128. end
  129. name
  130. end
  131. =begin
  132. longname of user
  133. user = User.find(123)
  134. result = user.longname
  135. returns
  136. result = "Bob Smith"
  137. or with org
  138. result = "Bob Smith (Org ABC)"
  139. =end
  140. def longname
  141. name = fullname
  142. if organization_id
  143. organization = Organization.lookup(id: organization_id)
  144. if organization
  145. name += " (#{organization.name})"
  146. end
  147. end
  148. name
  149. end
  150. =begin
  151. check if user is in role
  152. user = User.find(123)
  153. result = user.role?('Customer')
  154. result = user.role?(['Agent', 'Admin'])
  155. returns
  156. result = true|false
  157. =end
  158. def role?(role_name)
  159. roles.where(name: role_name).any?
  160. end
  161. =begin
  162. get users activity stream
  163. user = User.find(123)
  164. result = user.activity_stream(20)
  165. returns
  166. result = [
  167. {
  168. id: 2,
  169. o_id: 2,
  170. created_by_id: 3,
  171. created_at: '2013-09-28 00:57:21',
  172. object: "User",
  173. type: "created",
  174. },
  175. {
  176. id: 2,
  177. o_id: 2,
  178. created_by_id: 3,
  179. created_at: '2013-09-28 00:59:21',
  180. object: "User",
  181. type: "updated",
  182. },
  183. ]
  184. =end
  185. def activity_stream(limit, fulldata = false)
  186. stream = ActivityStream.list(self, limit)
  187. return stream if !fulldata
  188. # get related objects
  189. assets = {}
  190. stream.each do |item|
  191. assets = item.assets(assets)
  192. end
  193. {
  194. stream: stream,
  195. assets: assets,
  196. }
  197. end
  198. =begin
  199. tries to find the matching instance by the given identifier. Currently email and login is supported.
  200. user = User.indentify('User123')
  201. # or
  202. user = User.indentify('user-123@example.com')
  203. returns
  204. # User instance
  205. user.login # 'user123'
  206. =end
  207. def self.identify(identifier)
  208. return if identifier.blank?
  209. # try to find user based on login
  210. user = User.find_by(login: identifier.downcase)
  211. return user if user
  212. # try second lookup with email
  213. User.find_by(email: identifier.downcase)
  214. end
  215. =begin
  216. create user from from omni auth hash
  217. result = User.create_from_hash!(hash)
  218. returns
  219. result = user_model # user model if create was successfully
  220. =end
  221. def self.create_from_hash!(hash)
  222. url = ''
  223. hash['info']['urls']&.each_value do |local_url|
  224. next if local_url.blank?
  225. url = local_url
  226. end
  227. begin
  228. data = {
  229. login: hash['info']['nickname'] || hash['uid'],
  230. firstname: hash['info']['name'] || hash['info']['display_name'],
  231. email: hash['info']['email'],
  232. image_source: hash['info']['image'],
  233. web: url,
  234. address: hash['info']['location'],
  235. note: hash['info']['description'],
  236. source: hash['provider'],
  237. role_ids: Role.signup_role_ids,
  238. updated_by_id: 1,
  239. created_by_id: 1,
  240. }
  241. if hash['info']['first_name'].present? && hash['info']['last_name'].present?
  242. data[:firstname] = hash['info']['first_name']
  243. data[:lastname] = hash['info']['last_name']
  244. end
  245. create!(data)
  246. rescue => e
  247. logger.error e
  248. raise Exceptions::UnprocessableEntity, e.message
  249. end
  250. end
  251. # Find a user by mobile number, either directly or by number variants stored in the Cti::CallerIds.
  252. def self.by_mobile(number:)
  253. direct_lookup = User.where(mobile: number).reorder(:updated_at).first
  254. return direct_lookup if direct_lookup
  255. cti_lookup = Cti::CallerId.lookup(number.delete('+')).find { |id| id.level == 'known' && id.object == 'User' }
  256. User.find_by(id: cti_lookup.o_id) if cti_lookup
  257. end
  258. =begin
  259. generate new token for reset password
  260. result = User.password_reset_new_token(username)
  261. returns
  262. result = {
  263. token: token,
  264. user: user,
  265. }
  266. =end
  267. def self.password_reset_new_token(username)
  268. return if username.blank?
  269. # try to find user based on login
  270. user = User.find_by(login: username.downcase.strip, active: true)
  271. # try second lookup with email
  272. user ||= User.find_by(email: username.downcase.strip, active: true)
  273. return if !user || !user.email
  274. # Discard any possible previous tokens for safety reasons.
  275. Token.where(action: 'PasswordReset', user_id: user.id).destroy_all
  276. {
  277. token: Token.create(action: 'PasswordReset', user_id: user.id, persistent: false),
  278. user: user,
  279. }
  280. end
  281. =begin
  282. returns the User instance for a given password token if found
  283. result = User.by_reset_token(token)
  284. returns
  285. result = user_model # user_model if token was verified
  286. =end
  287. def self.by_reset_token(token)
  288. Token.check(action: 'PasswordReset', token: token)
  289. end
  290. =begin
  291. reset password with token and set new password
  292. result = User.password_reset_via_token(token,password)
  293. returns
  294. result = user_model # user_model if token was verified
  295. =end
  296. def self.password_reset_via_token(token, password)
  297. # check token
  298. user = by_reset_token(token)
  299. return if !user
  300. # reset password
  301. user.update!(password: password, verified: true)
  302. # delete token
  303. Token.find_by(action: 'PasswordReset', token: token).destroy
  304. user
  305. end
  306. def self.admin_password_auth_new_token(username)
  307. return if username.blank?
  308. # try to find user based on login
  309. user = User.find_by(login: username.downcase.strip, active: true)
  310. # try second lookup with email
  311. user ||= User.find_by(email: username.downcase.strip, active: true)
  312. return if !user || !user.email
  313. return if !user.permissions?('admin.*')
  314. # Discard any possible previous tokens for safety reasons.
  315. Token.where(action: 'AdminAuth', user_id: user.id).destroy_all
  316. {
  317. token: Token.create(action: 'AdminAuth', user_id: user.id, persistent: false),
  318. user: user,
  319. }
  320. end
  321. def self.admin_password_auth_via_token(token)
  322. user = Token.check(action: 'AdminAuth', token: token)
  323. return if !user
  324. Token.find_by(action: 'AdminAuth', token: token).destroy
  325. user
  326. end
  327. =begin
  328. update last login date and reset login_failed (is automatically done by auth and sso backend)
  329. user = User.find(123)
  330. result = user.update_last_login
  331. returns
  332. result = new_user_model
  333. =end
  334. def update_last_login
  335. # reduce DB/ES load by updating last_login every 10 minutes only
  336. if !last_login || last_login < 10.minutes.ago
  337. self.last_login = Time.zone.now
  338. end
  339. # reset login failed
  340. self.login_failed = 0
  341. save
  342. end
  343. =begin
  344. generate new token for signup
  345. result = User.signup_new_token(user) # or email
  346. returns
  347. result = {
  348. token: token,
  349. user: user,
  350. }
  351. =end
  352. def self.signup_new_token(user)
  353. return if !user
  354. return if !user.email
  355. # Discard any possible previous tokens for safety reasons.
  356. Token.where(action: 'Signup', user_id: user.id).destroy_all
  357. # generate token
  358. token = Token.create(action: 'Signup', user_id: user.id)
  359. {
  360. token: token,
  361. user: user,
  362. }
  363. end
  364. =begin
  365. verify signup with token
  366. result = User.signup_verify_via_token(token, user)
  367. returns
  368. result = user_model # user_model if token was verified
  369. =end
  370. def self.signup_verify_via_token(token, user = nil)
  371. # check token
  372. local_user = Token.check(action: 'Signup', token: token)
  373. return if !local_user
  374. # if requested user is different to current user
  375. return if user && local_user.id != user.id
  376. # set verified
  377. local_user.update!(verified: true)
  378. # delete token
  379. Token.find_by(action: 'Signup', token: token).destroy
  380. local_user
  381. end
  382. =begin
  383. merge two users to one
  384. user = User.find(123)
  385. result = user.merge(user_id_of_duplicate_user)
  386. returns
  387. result = new_user_model
  388. =end
  389. def merge(user_id_of_duplicate_user)
  390. # Raise an exception if the user is not found (?)
  391. #
  392. # (This line used to contain a useless variable assignment,
  393. # and was changed to satisfy the linter.
  394. # We're not certain of its original intention,
  395. # so the User.find call has been kept
  396. # to prevent any unexpected regressions.)
  397. User.find(user_id_of_duplicate_user)
  398. # mentions can not merged easily because the new user could have mentioned
  399. # the same ticket so we delete duplicates beforehand
  400. Mention.where(user_id: user_id_of_duplicate_user).find_each do |mention|
  401. if Mention.exists?(mentionable: mention.mentionable, user_id: id)
  402. mention.destroy
  403. else
  404. mention.update(user_id: id)
  405. end
  406. end
  407. # merge missing attributes
  408. Models.merge('User', id, user_id_of_duplicate_user)
  409. true
  410. end
  411. =begin
  412. list of active users in role
  413. result = User.of_role('Agent', group_ids)
  414. result = User.of_role(['Agent', 'Admin'])
  415. returns
  416. result = [user1, user2]
  417. =end
  418. def self.of_role(role, group_ids = nil)
  419. roles_ids = Role.where(active: true, name: role).map(&:id)
  420. if !group_ids
  421. return User.where(active: true).joins(:roles_users).where('roles_users.role_id' => roles_ids).reorder('users.updated_at DESC')
  422. end
  423. User.where(active: true)
  424. .joins(:roles_users)
  425. .joins(:users_groups)
  426. .where('roles_users.role_id IN (?) AND users_groups.group_ids IN (?)', roles_ids, group_ids).reorder('users.updated_at DESC')
  427. end
  428. # Reset agent notification preferences
  429. # Non-agent cannot receive notifications, thus notifications reset
  430. #
  431. # @option user [User] to reset preferences
  432. def self.reset_notifications_preferences!(user)
  433. return if !user.permissions? 'ticket.agent'
  434. user.fill_notification_config_preferences
  435. user.save!
  436. end
  437. =begin
  438. try to find correct name
  439. [firstname, lastname] = User.name_guess('Some Name', 'some.name@example.com')
  440. =end
  441. def self.name_guess(string, email = nil)
  442. return if string.blank? && email.blank?
  443. string.strip!
  444. firstname = ''
  445. lastname = ''
  446. # "Lastname, Firstname"
  447. if string.match?(',')
  448. name = string.split(', ', 2)
  449. if name.count == 2
  450. if name[0].present?
  451. lastname = name[0].strip
  452. end
  453. if name[1].present?
  454. firstname = name[1].strip
  455. end
  456. return [firstname, lastname] if firstname.present? || lastname.present?
  457. end
  458. end
  459. # "Firstname Lastname"
  460. if string =~ %r{^(((Dr\.|Prof\.)[[:space:]]|).+?)[[:space:]](.+?)$}i
  461. if $1.present?
  462. firstname = $1.strip
  463. end
  464. if $4.present?
  465. lastname = $4.strip
  466. end
  467. return [firstname, lastname] if firstname.present? || lastname.present?
  468. end
  469. # -no name- "firstname.lastname@example.com"
  470. if string.blank? && email.present?
  471. scan = email.scan(%r{^(.+?)\.(.+?)@.+?$})
  472. if scan[0].present?
  473. if scan[0][0].present?
  474. firstname = scan[0][0].strip
  475. end
  476. if scan[0][1].present?
  477. lastname = scan[0][1].strip
  478. end
  479. return [firstname, lastname] if firstname.present? || lastname.present?
  480. end
  481. end
  482. nil
  483. end
  484. def no_name?
  485. firstname.blank? && lastname.blank?
  486. end
  487. # get locale identifier of user or system if user's own is not set
  488. def locale
  489. preferences.fetch(:locale) { Locale.default }
  490. end
  491. attr_accessor :skip_ensure_uniq_email
  492. def shared_organizations?
  493. all_organizations.exists? shared: true
  494. end
  495. def all_organizations
  496. Organization.where(id: all_organization_ids)
  497. end
  498. def all_organization_ids
  499. ([organization_id] + organization_ids).uniq
  500. end
  501. def organization_id?(organization_id)
  502. all_organization_ids.include?(organization_id)
  503. end
  504. def create_organization_add_history(org)
  505. organization_history_log(org, 'added')
  506. end
  507. def create_organization_remove_history(org)
  508. organization_history_log(org, 'removed')
  509. end
  510. def fill_notification_config_preferences
  511. preferences[:notification_config] ||= {}
  512. preferences[:notification_config][:matrix] = Setting.get('ticket_agent_default_notifications')
  513. end
  514. def mail_delivery_failed_blocked_days
  515. return 0 if !preferences[:mail_delivery_failed]
  516. return 0 if preferences[:mail_delivery_failed_data].blank?
  517. # Blocked for 60 full days; see #4459.
  518. remaining_days = (preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
  519. return remaining_days if remaining_days.positive?
  520. reset_mail_delivery_failed
  521. 0
  522. end
  523. def reset_mail_delivery_failed
  524. preferences[:mail_delivery_failed] = false
  525. preferences[:mail_delivery_failed_data] = nil
  526. save!
  527. end
  528. private
  529. def organization_history_log(org, type)
  530. return if id.blank?
  531. attributes = {
  532. history_attribute: 'organization_ids',
  533. id_to: org.id,
  534. value_to: org.name
  535. }
  536. history_log(type, id, attributes)
  537. end
  538. def check_name
  539. self.firstname = sanitize_name(firstname)
  540. self.lastname = sanitize_name(lastname)
  541. return if firstname.present? && lastname.present?
  542. if (firstname.blank? && lastname.present?) || (firstname.present? && lastname.blank?)
  543. used_name = firstname.presence || lastname
  544. (local_firstname, local_lastname) = User.name_guess(used_name, email)
  545. elsif firstname.blank? && lastname.blank? && email.present?
  546. (local_firstname, local_lastname) = User.name_guess('', email)
  547. end
  548. check_name_apply(:firstname, local_firstname)
  549. check_name_apply(:lastname, local_lastname)
  550. end
  551. def sanitize_name(value)
  552. result = value&.strip
  553. return result if result.blank?
  554. result.split(%r{\s}).map { |v| strip_uri(v) }.join("\s")
  555. end
  556. def strip_uri(value)
  557. uri = URI.parse(value)
  558. return value if !uri || uri.scheme.blank? || uri.hostname.blank?
  559. # Strip the scheme from the URI.
  560. uri.hostname + uri.path
  561. rescue
  562. value
  563. end
  564. def check_name_apply(identifier, input)
  565. self[identifier] = input if input.present?
  566. self[identifier].capitalize! if self[identifier]&.match? %r{^([[:upper:]]+|[[:lower:]]+)$}
  567. end
  568. def check_email
  569. return if Setting.get('import_mode')
  570. return if email.blank?
  571. # https://bugs.chromium.org/p/chromium/issues/detail?id=410937
  572. self.email = EmailHelper::Idn.to_unicode(email).downcase.strip
  573. end
  574. def ensure_email
  575. return if Setting.get('import_mode')
  576. return if email.blank?
  577. return if id == 1
  578. email_address_validation = EmailAddressValidation.new(email)
  579. return if email_address_validation.valid?
  580. errors.add :base, __("Invalid email '%{email}'"), email: email
  581. end
  582. def check_login
  583. # use email as login if not given
  584. if login.blank?
  585. self.login = email
  586. end
  587. # if email has changed, login is old email, change also login
  588. if email_changed? && email_was == login
  589. self.login = email
  590. end
  591. # generate auto login
  592. if login.blank?
  593. self.login = "auto-#{SecureRandom.uuid}"
  594. end
  595. # check if login already exists
  596. base_login = login.downcase.strip
  597. alternatives = [nil] + Array(1..20) + [ SecureRandom.uuid ]
  598. alternatives.each do |suffix|
  599. self.login = "#{base_login}#{suffix}"
  600. exists = User.find_by(login: login)
  601. return true if !exists || exists.id == id
  602. end
  603. raise Exceptions::UnprocessableEntity, "Invalid user login generation for login #{login}!"
  604. end
  605. def check_mail_delivery_failed
  606. return if email_change.blank?
  607. preferences.delete(:mail_delivery_failed)
  608. end
  609. def ensure_roles
  610. return if role_ids.present?
  611. self.role_ids = Role.signup_role_ids
  612. end
  613. def ensure_identifier
  614. return if login.present? && !login.start_with?('auto-')
  615. return if [email, firstname, lastname, phone, mobile].any?(&:present?)
  616. errors.add :base, __('At least one identifier (firstname, lastname, phone, mobile or email) for user is required.')
  617. end
  618. def ensure_uniq_email
  619. return if Setting.get('user_email_multiple_use')
  620. return if Setting.get('import_mode')
  621. return if email.blank?
  622. return if !email_changed?
  623. return if !User.exists?(email: email.downcase.strip)
  624. errors.add :base, __("Email address '%{email}' is already used for another user."), email: email.downcase.strip
  625. end
  626. def ensure_organizations
  627. return if organization_ids.blank?
  628. return if organization_id.present?
  629. errors.add :base, __('Secondary organizations are only allowed when the primary organization is given.')
  630. end
  631. def ensure_organizations_limit
  632. return if organization_ids.size <= 250
  633. errors.add :base, __('More than 250 secondary organizations are not allowed.')
  634. end
  635. def validate_roles(role)
  636. return true if !role_ids # we need role_ids for checking in role_ids below, in this method
  637. return true if role.preferences[:not].blank?
  638. role.preferences[:not].each do |local_role_name|
  639. local_role = Role.lookup(name: local_role_name)
  640. next if !local_role
  641. next if role_ids.exclude?(local_role.id)
  642. raise "Role #{role.name} conflicts with #{local_role.name}"
  643. end
  644. true
  645. end
  646. def validate_preferences
  647. return true if !changes
  648. return true if !changes['preferences']
  649. return true if preferences.blank?
  650. return true if !preferences[:notification_sound]
  651. return true if !preferences[:notification_sound][:enabled]
  652. case preferences[:notification_sound][:enabled]
  653. when 'true'
  654. preferences[:notification_sound][:enabled] = true
  655. when 'false'
  656. preferences[:notification_sound][:enabled] = false
  657. end
  658. class_name = preferences[:notification_sound][:enabled].class.to_s
  659. 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'
  660. true
  661. end
  662. def ensure_notification_preferences
  663. fill_notification_config_preferences
  664. self.reset_notification_config_before_save = false
  665. end
  666. =begin
  667. checks if the current user is the last one with admin permissions.
  668. Raises
  669. raise 'At least one user need to have admin permissions'
  670. =end
  671. def last_admin_check_by_attribute
  672. return true if !will_save_change_to_attribute?('active')
  673. return true if active != false
  674. return true if !permissions?(['admin', 'admin.user'])
  675. raise Exceptions::UnprocessableEntity, __('At least one user needs to have admin permissions.') if last_admin_check_admin_count < 1
  676. true
  677. end
  678. def last_admin_check_by_role(role)
  679. return true if Setting.get('import_mode')
  680. return true if !role.with_permission?(['admin', 'admin.user'])
  681. raise Exceptions::UnprocessableEntity, __('At least one user needs to have admin permissions.') if last_admin_check_admin_count < 1
  682. true
  683. end
  684. def last_admin_check_admin_count
  685. admin_role_ids = Role.joins(:permissions).where(permissions: { name: ['admin', 'admin.user'], active: true }, roles: { active: true }).pluck(:id)
  686. User.joins(:roles).where(roles: { id: admin_role_ids }, users: { active: true }).distinct.count - 1
  687. end
  688. def validate_agent_limit_by_attributes
  689. return true if Setting.get('system_agent_limit').blank?
  690. return true if !will_save_change_to_attribute?('active')
  691. return true if active != true
  692. return true if !permissions?('ticket.agent')
  693. ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent', active: true }, roles: { active: true }).pluck(:id)
  694. count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).distinct.count + 1
  695. raise Exceptions::UnprocessableEntity, __('Agent limit exceeded, please check your account settings.') if count > Setting.get('system_agent_limit').to_i
  696. true
  697. end
  698. def validate_agent_limit_by_role(role)
  699. return true if Setting.get('system_agent_limit').blank?
  700. return true if active != true
  701. return true if role.active != true
  702. return true if !role.with_permission?('ticket.agent')
  703. ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent', active: true }, roles: { active: true }).pluck(:id)
  704. count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).distinct.count
  705. # if new added role is a ticket.agent role
  706. if ticket_agent_role_ids.include?(role.id)
  707. # if user already has a ticket.agent role
  708. hint = false
  709. role_ids.each do |locale_role_id|
  710. next if ticket_agent_role_ids.exclude?(locale_role_id)
  711. hint = true
  712. break
  713. end
  714. # user has not already a ticket.agent role
  715. if hint == false
  716. count += 1
  717. end
  718. end
  719. raise Exceptions::UnprocessableEntity, __('Agent limit exceeded, please check your account settings.') if count > Setting.get('system_agent_limit').to_i
  720. true
  721. end
  722. def domain_based_assignment
  723. return true if !email
  724. return true if organization_id
  725. begin
  726. domain = Mail::Address.new(email).domain
  727. return true if !domain
  728. organization = Organization.find_by(domain: domain.downcase, domain_assignment: true)
  729. return true if !organization
  730. self.organization_id = organization.id
  731. rescue
  732. return true
  733. end
  734. true
  735. end
  736. # sets locale of the user
  737. def set_locale
  738. # set the user's locale to the one of the "executing" user
  739. return true if !UserInfo.current_user_id
  740. user = UserInfo.current_user
  741. return true if !user
  742. return true if !user.preferences[:locale]
  743. preferences[:locale] = user.preferences[:locale]
  744. true
  745. end
  746. def destroy_longer_required_objects
  747. ::Avatar.remove(self.class.to_s, id)
  748. ::UserDevice.remove(id)
  749. ::StatsStore.where(stats_storable: self).destroy_all
  750. end
  751. def destroy_move_dependency_ownership
  752. result = Models.references(self.class.to_s, id)
  753. 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]
  754. result.each do |class_name, references|
  755. next if class_name.blank?
  756. next if references.blank?
  757. ref_class = class_name.constantize
  758. ref_update_columns = []
  759. references.each do |column, reference_found|
  760. next if !reference_found
  761. if user_columns.include?(column)
  762. ref_update_columns << column
  763. elsif ref_class.exists?(column => id)
  764. raise "Failed deleting references! Check logic for #{class_name}->#{column}."
  765. end
  766. end
  767. next if ref_update_columns.blank?
  768. where_sql = ref_update_columns.map { |column| "#{column} = #{id}" }.join(' OR ')
  769. ref_class.where(where_sql).find_in_batches(batch_size: 1000) do |batch_list|
  770. batch_list.each do |record|
  771. ref_update_columns.each do |column|
  772. next if record[column] != id
  773. record[column] = 1
  774. end
  775. record.save!(validate: false)
  776. rescue => e
  777. Rails.logger.error e
  778. end
  779. end
  780. end
  781. true
  782. end
  783. def ensure_password
  784. return if !password_changed?
  785. self.password = ensured_password
  786. end
  787. def ensured_password
  788. # ensure unset password for blank values of new users
  789. return nil if new_record? && password.blank?
  790. # don't permit empty password update for existing users
  791. return password_was if password.blank?
  792. # don't re-hash passwords
  793. return password if PasswordHash.crypted?(password)
  794. if !PasswordPolicy::MaxLength.valid? password
  795. errors.add :password, __('is too long')
  796. return nil
  797. end
  798. # hash the plaintext password
  799. PasswordHash.crypt(password)
  800. end
  801. # reset login_failed if password is changed
  802. def reset_login_failed_after_password_change
  803. return true if !will_save_change_to_attribute?('password')
  804. self.login_failed = 0
  805. true
  806. end
  807. # When adding/removing a phone/mobile number from the User table,
  808. # update caller ID table
  809. # to adopt/orphan matching Cti::Logs accordingly
  810. # (see https://github.com/zammad/zammad/issues/2057)
  811. def update_caller_id
  812. # skip if "phone/mobile" does not change, or changes like [nil, ""]
  813. return if persisted? && previous_changes.slice(:phone, :mobile).values.flatten.none?(&:present?)
  814. return if destroyed? && phone.blank? && mobile.blank?
  815. Cti::CallerId.build(self)
  816. end
  817. end