user.rb 32 KB

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