##// END OF EJS Templates
Speeds up Project.allowed_to_condition for users who belong to hundreds of projects....
Jean-Philippe Lang -
r15742:98f8a17851d5
parent child
Show More
@@ -1,1095 +1,1095
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 include Redmine::NestedSet::ProjectNestedSet
21 21
22 22 # Project statuses
23 23 STATUS_ACTIVE = 1
24 24 STATUS_CLOSED = 5
25 25 STATUS_ARCHIVED = 9
26 26
27 27 # Maximum length for project identifiers
28 28 IDENTIFIER_MAX_LENGTH = 100
29 29
30 30 # Specific overridden Activities
31 31 has_many :time_entry_activities
32 32 has_many :memberships, :class_name => 'Member', :inverse_of => :project
33 33 # Memberships of active users only
34 34 has_many :members,
35 35 lambda { joins(:principal).where(:users => {:type => 'User', :status => Principal::STATUS_ACTIVE}) }
36 36 has_many :enabled_modules, :dependent => :delete_all
37 37 has_and_belongs_to_many :trackers, lambda {order(:position)}
38 38 has_many :issues, :dependent => :destroy
39 39 has_many :issue_changes, :through => :issues, :source => :journals
40 40 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
41 41 belongs_to :default_version, :class_name => 'Version'
42 42 has_many :time_entries, :dependent => :destroy
43 43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 44 has_many :documents, :dependent => :destroy
45 45 has_many :news, lambda {includes(:author)}, :dependent => :destroy
46 46 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
47 47 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
48 48 has_one :repository, lambda {where(["is_default = ?", true])}
49 49 has_many :repositories, :dependent => :destroy
50 50 has_many :changesets, :through => :repository
51 51 has_one :wiki, :dependent => :destroy
52 52 # Custom field for the project issues
53 53 has_and_belongs_to_many :issue_custom_fields,
54 54 lambda {order("#{CustomField.table_name}.position")},
55 55 :class_name => 'IssueCustomField',
56 56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 57 :association_foreign_key => 'custom_field_id'
58 58
59 59 acts_as_attachable :view_permission => :view_files,
60 60 :edit_permission => :manage_files,
61 61 :delete_permission => :manage_files
62 62
63 63 acts_as_customizable
64 64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
65 65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 67 :author => nil
68 68
69 69 attr_protected :status
70 70
71 71 validates_presence_of :name, :identifier
72 72 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
73 73 validates_length_of :name, :maximum => 255
74 74 validates_length_of :homepage, :maximum => 255
75 75 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH
76 76 # downcase letters, digits, dashes but not digits only
77 77 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
78 78 # reserved words
79 79 validates_exclusion_of :identifier, :in => %w( new )
80 80 validate :validate_parent
81 81
82 82 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
83 83 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
84 84 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
85 85 before_destroy :delete_all_members
86 86
87 87 scope :has_module, lambda {|mod|
88 88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 89 }
90 90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 92 scope :all_public, lambda { where(:is_public => true) }
93 93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 94 scope :allowed_to, lambda {|*args|
95 95 user = User.current
96 96 permission = nil
97 97 if args.first.is_a?(Symbol)
98 98 permission = args.shift
99 99 else
100 100 user = args.shift
101 101 permission = args.shift
102 102 end
103 103 where(Project.allowed_to_condition(user, permission, *args))
104 104 }
105 105 scope :like, lambda {|arg|
106 106 if arg.blank?
107 107 where(nil)
108 108 else
109 109 pattern = "%#{arg.to_s.strip.downcase}%"
110 110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 111 end
112 112 }
113 113 scope :sorted, lambda {order(:lft)}
114 114 scope :having_trackers, lambda {
115 115 where("#{Project.table_name}.id IN (SELECT DISTINCT project_id FROM #{table_name_prefix}projects_trackers#{table_name_suffix})")
116 116 }
117 117
118 118 def initialize(attributes=nil, *args)
119 119 super
120 120
121 121 initialized = (attributes || {}).stringify_keys
122 122 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
123 123 self.identifier = Project.next_identifier
124 124 end
125 125 if !initialized.key?('is_public')
126 126 self.is_public = Setting.default_projects_public?
127 127 end
128 128 if !initialized.key?('enabled_module_names')
129 129 self.enabled_module_names = Setting.default_projects_modules
130 130 end
131 131 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
132 132 default = Setting.default_projects_tracker_ids
133 133 if default.is_a?(Array)
134 134 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
135 135 else
136 136 self.trackers = Tracker.sorted.to_a
137 137 end
138 138 end
139 139 end
140 140
141 141 def identifier=(identifier)
142 142 super unless identifier_frozen?
143 143 end
144 144
145 145 def identifier_frozen?
146 146 errors[:identifier].blank? && !(new_record? || identifier.blank?)
147 147 end
148 148
149 149 # returns latest created projects
150 150 # non public projects will be returned only if user is a member of those
151 151 def self.latest(user=nil, count=5)
152 152 visible(user).limit(count).
153 153 order(:created_on => :desc).
154 154 where("#{table_name}.created_on >= ?", 30.days.ago).
155 155 to_a
156 156 end
157 157
158 158 # Returns true if the project is visible to +user+ or to the current user.
159 159 def visible?(user=User.current)
160 160 user.allowed_to?(:view_project, self)
161 161 end
162 162
163 163 # Returns a SQL conditions string used to find all projects visible by the specified user.
164 164 #
165 165 # Examples:
166 166 # Project.visible_condition(admin) => "projects.status = 1"
167 167 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
168 168 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
169 169 def self.visible_condition(user, options={})
170 170 allowed_to_condition(user, :view_project, options)
171 171 end
172 172
173 173 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
174 174 #
175 175 # Valid options:
176 176 # * :project => limit the condition to project
177 177 # * :with_subprojects => limit the condition to project and its subprojects
178 178 # * :member => limit the condition to the user projects
179 179 def self.allowed_to_condition(user, permission, options={})
180 180 perm = Redmine::AccessControl.permission(permission)
181 181 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
182 182 if perm && perm.project_module
183 183 # If the permission belongs to a project module, make sure the module is enabled
184 184 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
185 185 end
186 186 if project = options[:project]
187 187 project_statement = project.project_condition(options[:with_subprojects])
188 188 base_statement = "(#{project_statement}) AND (#{base_statement})"
189 189 end
190 190
191 191 if user.admin?
192 192 base_statement
193 193 else
194 194 statement_by_role = {}
195 195 unless options[:member]
196 196 role = user.builtin_role
197 197 if role.allowed_to?(permission)
198 198 s = "#{Project.table_name}.is_public = #{connection.quoted_true}"
199 199 if user.id
200 200 group = role.anonymous? ? Group.anonymous : Group.non_member
201 201 principal_ids = [user.id, group.id].compact
202 202 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id IN (#{principal_ids.join(',')})))"
203 203 end
204 204 statement_by_role[role] = s
205 205 end
206 206 end
207 user.projects_by_role.each do |role, projects|
208 if role.allowed_to?(permission) && projects.any?
209 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
207 user.project_ids_by_role.each do |role, project_ids|
208 if role.allowed_to?(permission) && project_ids.any?
209 statement_by_role[role] = "#{Project.table_name}.id IN (#{project_ids.join(',')})"
210 210 end
211 211 end
212 212 if statement_by_role.empty?
213 213 "1=0"
214 214 else
215 215 if block_given?
216 216 statement_by_role.each do |role, statement|
217 217 if s = yield(role, user)
218 218 statement_by_role[role] = "(#{statement} AND (#{s}))"
219 219 end
220 220 end
221 221 end
222 222 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
223 223 end
224 224 end
225 225 end
226 226
227 227 def override_roles(role)
228 228 @override_members ||= memberships.
229 229 joins(:principal).
230 230 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
231 231
232 232 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
233 233 member = @override_members.detect {|m| m.principal.is_a? group_class}
234 234 member ? member.roles.to_a : [role]
235 235 end
236 236
237 237 def principals
238 238 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
239 239 end
240 240
241 241 def users
242 242 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
243 243 end
244 244
245 245 # Returns the Systemwide and project specific activities
246 246 def activities(include_inactive=false)
247 247 t = TimeEntryActivity.table_name
248 248 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
249 249
250 250 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
251 251 if overridden_activity_ids.any?
252 252 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
253 253 end
254 254 unless include_inactive
255 255 scope = scope.active
256 256 end
257 257 scope
258 258 end
259 259
260 260 # Will create a new Project specific Activity or update an existing one
261 261 #
262 262 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
263 263 # does not successfully save.
264 264 def update_or_create_time_entry_activity(id, activity_hash)
265 265 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
266 266 self.create_time_entry_activity_if_needed(activity_hash)
267 267 else
268 268 activity = project.time_entry_activities.find_by_id(id.to_i)
269 269 activity.update_attributes(activity_hash) if activity
270 270 end
271 271 end
272 272
273 273 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
274 274 #
275 275 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
276 276 # does not successfully save.
277 277 def create_time_entry_activity_if_needed(activity)
278 278 if activity['parent_id']
279 279 parent_activity = TimeEntryActivity.find(activity['parent_id'])
280 280 activity['name'] = parent_activity.name
281 281 activity['position'] = parent_activity.position
282 282 if Enumeration.overriding_change?(activity, parent_activity)
283 283 project_activity = self.time_entry_activities.create(activity)
284 284 if project_activity.new_record?
285 285 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
286 286 else
287 287 self.time_entries.
288 288 where(:activity_id => parent_activity.id).
289 289 update_all(:activity_id => project_activity.id)
290 290 end
291 291 end
292 292 end
293 293 end
294 294
295 295 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
296 296 #
297 297 # Examples:
298 298 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
299 299 # project.project_condition(false) => "projects.id = 1"
300 300 def project_condition(with_subprojects)
301 301 cond = "#{Project.table_name}.id = #{id}"
302 302 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
303 303 cond
304 304 end
305 305
306 306 def self.find(*args)
307 307 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
308 308 project = find_by_identifier(*args)
309 309 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
310 310 project
311 311 else
312 312 super
313 313 end
314 314 end
315 315
316 316 def self.find_by_param(*args)
317 317 self.find(*args)
318 318 end
319 319
320 320 alias :base_reload :reload
321 321 def reload(*args)
322 322 @principals = nil
323 323 @users = nil
324 324 @shared_versions = nil
325 325 @rolled_up_versions = nil
326 326 @rolled_up_trackers = nil
327 327 @all_issue_custom_fields = nil
328 328 @all_time_entry_custom_fields = nil
329 329 @to_param = nil
330 330 @allowed_parents = nil
331 331 @allowed_permissions = nil
332 332 @actions_allowed = nil
333 333 @start_date = nil
334 334 @due_date = nil
335 335 @override_members = nil
336 336 @assignable_users = nil
337 337 base_reload(*args)
338 338 end
339 339
340 340 def to_param
341 341 if new_record?
342 342 nil
343 343 else
344 344 # id is used for projects with a numeric identifier (compatibility)
345 345 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
346 346 end
347 347 end
348 348
349 349 def active?
350 350 self.status == STATUS_ACTIVE
351 351 end
352 352
353 353 def closed?
354 354 self.status == STATUS_CLOSED
355 355 end
356 356
357 357 def archived?
358 358 self.status == STATUS_ARCHIVED
359 359 end
360 360
361 361 # Archives the project and its descendants
362 362 def archive
363 363 # Check that there is no issue of a non descendant project that is assigned
364 364 # to one of the project or descendant versions
365 365 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
366 366
367 367 if version_ids.any? &&
368 368 Issue.
369 369 includes(:project).
370 370 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
371 371 where(:fixed_version_id => version_ids).
372 372 exists?
373 373 return false
374 374 end
375 375 Project.transaction do
376 376 archive!
377 377 end
378 378 true
379 379 end
380 380
381 381 # Unarchives the project
382 382 # All its ancestors must be active
383 383 def unarchive
384 384 return false if ancestors.detect {|a| a.archived?}
385 385 new_status = STATUS_ACTIVE
386 386 if parent
387 387 new_status = parent.status
388 388 end
389 389 update_attribute :status, new_status
390 390 end
391 391
392 392 def close
393 393 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
394 394 end
395 395
396 396 def reopen
397 397 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
398 398 end
399 399
400 400 # Returns an array of projects the project can be moved to
401 401 # by the current user
402 402 def allowed_parents(user=User.current)
403 403 return @allowed_parents if @allowed_parents
404 404 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
405 405 @allowed_parents = @allowed_parents - self_and_descendants
406 406 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
407 407 @allowed_parents << nil
408 408 end
409 409 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
410 410 @allowed_parents << parent
411 411 end
412 412 @allowed_parents
413 413 end
414 414
415 415 # Sets the parent of the project with authorization check
416 416 def set_allowed_parent!(p)
417 417 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
418 418 p = p.id if p.is_a?(Project)
419 419 send :safe_attributes, {:project_id => p}
420 420 save
421 421 end
422 422
423 423 # Sets the parent of the project and saves the project
424 424 # Argument can be either a Project, a String, a Fixnum or nil
425 425 def set_parent!(p)
426 426 if p.is_a?(Project)
427 427 self.parent = p
428 428 else
429 429 self.parent_id = p
430 430 end
431 431 save
432 432 end
433 433
434 434 # Returns a scope of the trackers used by the project and its active sub projects
435 435 def rolled_up_trackers(include_subprojects=true)
436 436 if include_subprojects
437 437 @rolled_up_trackers ||= rolled_up_trackers_base_scope.
438 438 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
439 439 else
440 440 rolled_up_trackers_base_scope.
441 441 where(:projects => {:id => id})
442 442 end
443 443 end
444 444
445 445 def rolled_up_trackers_base_scope
446 446 Tracker.
447 447 joins(projects: :enabled_modules).
448 448 where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
449 449 where(:enabled_modules => {:name => 'issue_tracking'}).
450 450 distinct.
451 451 sorted
452 452 end
453 453
454 454 # Closes open and locked project versions that are completed
455 455 def close_completed_versions
456 456 Version.transaction do
457 457 versions.where(:status => %w(open locked)).each do |version|
458 458 if version.completed?
459 459 version.update_attribute(:status, 'closed')
460 460 end
461 461 end
462 462 end
463 463 end
464 464
465 465 # Returns a scope of the Versions on subprojects
466 466 def rolled_up_versions
467 467 @rolled_up_versions ||=
468 468 Version.
469 469 joins(:project).
470 470 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
471 471 end
472 472
473 473 # Returns a scope of the Versions used by the project
474 474 def shared_versions
475 475 if new_record?
476 476 Version.
477 477 joins(:project).
478 478 preload(:project).
479 479 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
480 480 else
481 481 @shared_versions ||= begin
482 482 r = root? ? self : root
483 483 Version.
484 484 joins(:project).
485 485 preload(:project).
486 486 where("#{Project.table_name}.id = #{id}" +
487 487 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
488 488 " #{Version.table_name}.sharing = 'system'" +
489 489 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
490 490 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
491 491 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
492 492 "))")
493 493 end
494 494 end
495 495 end
496 496
497 497 # Returns a hash of project users grouped by role
498 498 def users_by_role
499 499 members.includes(:user, :roles).inject({}) do |h, m|
500 500 m.roles.each do |r|
501 501 h[r] ||= []
502 502 h[r] << m.user
503 503 end
504 504 h
505 505 end
506 506 end
507 507
508 508 # Adds user as a project member with the default role
509 509 # Used for when a non-admin user creates a project
510 510 def add_default_member(user)
511 511 role = self.class.default_member_role
512 512 member = Member.new(:project => self, :principal => user, :roles => [role])
513 513 self.members << member
514 514 member
515 515 end
516 516
517 517 # Default role that is given to non-admin users that
518 518 # create a project
519 519 def self.default_member_role
520 520 Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
521 521 end
522 522
523 523 # Deletes all project's members
524 524 def delete_all_members
525 525 me, mr = Member.table_name, MemberRole.table_name
526 526 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
527 527 Member.where(:project_id => id).delete_all
528 528 end
529 529
530 530 # Return a Principal scope of users/groups issues can be assigned to
531 531 def assignable_users(tracker=nil)
532 532 return @assignable_users[tracker] if @assignable_users && @assignable_users[tracker]
533 533
534 534 types = ['User']
535 535 types << 'Group' if Setting.issue_group_assignment?
536 536
537 537 scope = Principal.
538 538 active.
539 539 joins(:members => :roles).
540 540 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
541 541 distinct.
542 542 sorted
543 543
544 544 if tracker
545 545 # Rejects users that cannot the view the tracker
546 546 roles = Role.where(:assignable => true).select {|role| role.permissions_tracker?(:view_issues, tracker)}
547 547 scope = scope.where(:roles => {:id => roles.map(&:id)})
548 548 end
549 549
550 550 @assignable_users ||= {}
551 551 @assignable_users[tracker] = scope
552 552 end
553 553
554 554 # Returns the mail addresses of users that should be always notified on project events
555 555 def recipients
556 556 notified_users.collect {|user| user.mail}
557 557 end
558 558
559 559 # Returns the users that should be notified on project events
560 560 def notified_users
561 561 # TODO: User part should be extracted to User#notify_about?
562 562 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
563 563 end
564 564
565 565 # Returns a scope of all custom fields enabled for project issues
566 566 # (explicitly associated custom fields and custom fields enabled for all projects)
567 567 def all_issue_custom_fields
568 568 if new_record?
569 569 @all_issue_custom_fields ||= IssueCustomField.
570 570 sorted.
571 571 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
572 572 else
573 573 @all_issue_custom_fields ||= IssueCustomField.
574 574 sorted.
575 575 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
576 576 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
577 577 " WHERE cfp.project_id = ?)", true, id)
578 578 end
579 579 end
580 580
581 581 def project
582 582 self
583 583 end
584 584
585 585 def <=>(project)
586 586 name.casecmp(project.name)
587 587 end
588 588
589 589 def to_s
590 590 name
591 591 end
592 592
593 593 # Returns a short description of the projects (first lines)
594 594 def short_description(length = 255)
595 595 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
596 596 end
597 597
598 598 def css_classes
599 599 s = 'project'
600 600 s << ' root' if root?
601 601 s << ' child' if child?
602 602 s << (leaf? ? ' leaf' : ' parent')
603 603 unless active?
604 604 if archived?
605 605 s << ' archived'
606 606 else
607 607 s << ' closed'
608 608 end
609 609 end
610 610 s
611 611 end
612 612
613 613 # The earliest start date of a project, based on it's issues and versions
614 614 def start_date
615 615 @start_date ||= [
616 616 issues.minimum('start_date'),
617 617 shared_versions.minimum('effective_date'),
618 618 Issue.fixed_version(shared_versions).minimum('start_date')
619 619 ].compact.min
620 620 end
621 621
622 622 # The latest due date of an issue or version
623 623 def due_date
624 624 @due_date ||= [
625 625 issues.maximum('due_date'),
626 626 shared_versions.maximum('effective_date'),
627 627 Issue.fixed_version(shared_versions).maximum('due_date')
628 628 ].compact.max
629 629 end
630 630
631 631 def overdue?
632 632 active? && !due_date.nil? && (due_date < User.current.today)
633 633 end
634 634
635 635 # Returns the percent completed for this project, based on the
636 636 # progress on it's versions.
637 637 def completed_percent(options={:include_subprojects => false})
638 638 if options.delete(:include_subprojects)
639 639 total = self_and_descendants.collect(&:completed_percent).sum
640 640
641 641 total / self_and_descendants.count
642 642 else
643 643 if versions.count > 0
644 644 total = versions.collect(&:completed_percent).sum
645 645
646 646 total / versions.count
647 647 else
648 648 100
649 649 end
650 650 end
651 651 end
652 652
653 653 # Return true if this project allows to do the specified action.
654 654 # action can be:
655 655 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
656 656 # * a permission Symbol (eg. :edit_project)
657 657 def allows_to?(action)
658 658 if archived?
659 659 # No action allowed on archived projects
660 660 return false
661 661 end
662 662 unless active? || Redmine::AccessControl.read_action?(action)
663 663 # No write action allowed on closed projects
664 664 return false
665 665 end
666 666 # No action allowed on disabled modules
667 667 if action.is_a? Hash
668 668 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
669 669 else
670 670 allowed_permissions.include? action
671 671 end
672 672 end
673 673
674 674 # Return the enabled module with the given name
675 675 # or nil if the module is not enabled for the project
676 676 def enabled_module(name)
677 677 name = name.to_s
678 678 enabled_modules.detect {|m| m.name == name}
679 679 end
680 680
681 681 # Return true if the module with the given name is enabled
682 682 def module_enabled?(name)
683 683 enabled_module(name).present?
684 684 end
685 685
686 686 def enabled_module_names=(module_names)
687 687 if module_names && module_names.is_a?(Array)
688 688 module_names = module_names.collect(&:to_s).reject(&:blank?)
689 689 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
690 690 else
691 691 enabled_modules.clear
692 692 end
693 693 end
694 694
695 695 # Returns an array of the enabled modules names
696 696 def enabled_module_names
697 697 enabled_modules.collect(&:name)
698 698 end
699 699
700 700 # Enable a specific module
701 701 #
702 702 # Examples:
703 703 # project.enable_module!(:issue_tracking)
704 704 # project.enable_module!("issue_tracking")
705 705 def enable_module!(name)
706 706 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
707 707 end
708 708
709 709 # Disable a module if it exists
710 710 #
711 711 # Examples:
712 712 # project.disable_module!(:issue_tracking)
713 713 # project.disable_module!("issue_tracking")
714 714 # project.disable_module!(project.enabled_modules.first)
715 715 def disable_module!(target)
716 716 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
717 717 target.destroy unless target.blank?
718 718 end
719 719
720 720 safe_attributes 'name',
721 721 'description',
722 722 'homepage',
723 723 'is_public',
724 724 'identifier',
725 725 'custom_field_values',
726 726 'custom_fields',
727 727 'tracker_ids',
728 728 'issue_custom_field_ids',
729 729 'parent_id',
730 730 'default_version_id'
731 731
732 732 safe_attributes 'enabled_module_names',
733 733 :if => lambda {|project, user|
734 734 if project.new_record?
735 735 if user.admin?
736 736 true
737 737 else
738 738 default_member_role.has_permission?(:select_project_modules)
739 739 end
740 740 else
741 741 user.allowed_to?(:select_project_modules, project)
742 742 end
743 743 }
744 744
745 745 safe_attributes 'inherit_members',
746 746 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
747 747
748 748 def safe_attributes=(attrs, user=User.current)
749 749 return unless attrs.is_a?(Hash)
750 750 attrs = attrs.deep_dup
751 751
752 752 @unallowed_parent_id = nil
753 753 if new_record? || attrs.key?('parent_id')
754 754 parent_id_param = attrs['parent_id'].to_s
755 755 if new_record? || parent_id_param != parent_id.to_s
756 756 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
757 757 unless allowed_parents(user).include?(p)
758 758 attrs.delete('parent_id')
759 759 @unallowed_parent_id = true
760 760 end
761 761 end
762 762 end
763 763
764 764 super(attrs, user)
765 765 end
766 766
767 767 # Returns an auto-generated project identifier based on the last identifier used
768 768 def self.next_identifier
769 769 p = Project.order('id DESC').first
770 770 p.nil? ? nil : p.identifier.to_s.succ
771 771 end
772 772
773 773 # Copies and saves the Project instance based on the +project+.
774 774 # Duplicates the source project's:
775 775 # * Wiki
776 776 # * Versions
777 777 # * Categories
778 778 # * Issues
779 779 # * Members
780 780 # * Queries
781 781 #
782 782 # Accepts an +options+ argument to specify what to copy
783 783 #
784 784 # Examples:
785 785 # project.copy(1) # => copies everything
786 786 # project.copy(1, :only => 'members') # => copies members only
787 787 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
788 788 def copy(project, options={})
789 789 project = project.is_a?(Project) ? project : Project.find(project)
790 790
791 791 to_be_copied = %w(members wiki versions issue_categories issues queries boards)
792 792 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
793 793
794 794 Project.transaction do
795 795 if save
796 796 reload
797 797 to_be_copied.each do |name|
798 798 send "copy_#{name}", project
799 799 end
800 800 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
801 801 save
802 802 else
803 803 false
804 804 end
805 805 end
806 806 end
807 807
808 808 def member_principals
809 809 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
810 810 memberships.active
811 811 end
812 812
813 813 # Returns a new unsaved Project instance with attributes copied from +project+
814 814 def self.copy_from(project)
815 815 project = project.is_a?(Project) ? project : Project.find(project)
816 816 # clear unique attributes
817 817 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
818 818 copy = Project.new(attributes)
819 819 copy.enabled_module_names = project.enabled_module_names
820 820 copy.trackers = project.trackers
821 821 copy.custom_values = project.custom_values.collect {|v| v.clone}
822 822 copy.issue_custom_fields = project.issue_custom_fields
823 823 copy
824 824 end
825 825
826 826 # Yields the given block for each project with its level in the tree
827 827 def self.project_tree(projects, options={}, &block)
828 828 ancestors = []
829 829 if options[:init_level] && projects.first
830 830 ancestors = projects.first.ancestors.to_a
831 831 end
832 832 projects.sort_by(&:lft).each do |project|
833 833 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
834 834 ancestors.pop
835 835 end
836 836 yield project, ancestors.size
837 837 ancestors << project
838 838 end
839 839 end
840 840
841 841 private
842 842
843 843 def update_inherited_members
844 844 if parent
845 845 if inherit_members? && !inherit_members_was
846 846 remove_inherited_member_roles
847 847 add_inherited_member_roles
848 848 elsif !inherit_members? && inherit_members_was
849 849 remove_inherited_member_roles
850 850 end
851 851 end
852 852 end
853 853
854 854 def remove_inherited_member_roles
855 855 member_roles = MemberRole.where(:member_id => membership_ids).to_a
856 856 member_role_ids = member_roles.map(&:id)
857 857 member_roles.each do |member_role|
858 858 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
859 859 member_role.destroy
860 860 end
861 861 end
862 862 end
863 863
864 864 def add_inherited_member_roles
865 865 if inherit_members? && parent
866 866 parent.memberships.each do |parent_member|
867 867 member = Member.find_or_new(self.id, parent_member.user_id)
868 868 parent_member.member_roles.each do |parent_member_role|
869 869 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
870 870 end
871 871 member.save!
872 872 end
873 873 memberships.reset
874 874 end
875 875 end
876 876
877 877 def update_versions_from_hierarchy_change
878 878 Issue.update_versions_from_hierarchy_change(self)
879 879 end
880 880
881 881 def validate_parent
882 882 if @unallowed_parent_id
883 883 errors.add(:parent_id, :invalid)
884 884 elsif parent_id_changed?
885 885 unless parent.nil? || (parent.active? && move_possible?(parent))
886 886 errors.add(:parent_id, :invalid)
887 887 end
888 888 end
889 889 end
890 890
891 891 # Copies wiki from +project+
892 892 def copy_wiki(project)
893 893 # Check that the source project has a wiki first
894 894 unless project.wiki.nil?
895 895 wiki = self.wiki || Wiki.new
896 896 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
897 897 wiki_pages_map = {}
898 898 project.wiki.pages.each do |page|
899 899 # Skip pages without content
900 900 next if page.content.nil?
901 901 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
902 902 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
903 903 new_wiki_page.content = new_wiki_content
904 904 wiki.pages << new_wiki_page
905 905 wiki_pages_map[page.id] = new_wiki_page
906 906 end
907 907
908 908 self.wiki = wiki
909 909 wiki.save
910 910 # Reproduce page hierarchy
911 911 project.wiki.pages.each do |page|
912 912 if page.parent_id && wiki_pages_map[page.id]
913 913 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
914 914 wiki_pages_map[page.id].save
915 915 end
916 916 end
917 917 end
918 918 end
919 919
920 920 # Copies versions from +project+
921 921 def copy_versions(project)
922 922 project.versions.each do |version|
923 923 new_version = Version.new
924 924 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
925 925 self.versions << new_version
926 926 end
927 927 end
928 928
929 929 # Copies issue categories from +project+
930 930 def copy_issue_categories(project)
931 931 project.issue_categories.each do |issue_category|
932 932 new_issue_category = IssueCategory.new
933 933 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
934 934 self.issue_categories << new_issue_category
935 935 end
936 936 end
937 937
938 938 # Copies issues from +project+
939 939 def copy_issues(project)
940 940 # Stores the source issue id as a key and the copied issues as the
941 941 # value. Used to map the two together for issue relations.
942 942 issues_map = {}
943 943
944 944 # Store status and reopen locked/closed versions
945 945 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
946 946 version_statuses.each do |version, status|
947 947 version.update_attribute :status, 'open'
948 948 end
949 949
950 950 # Get issues sorted by root_id, lft so that parent issues
951 951 # get copied before their children
952 952 project.issues.reorder('root_id, lft').each do |issue|
953 953 new_issue = Issue.new
954 954 new_issue.copy_from(issue, :subtasks => false, :link => false)
955 955 new_issue.project = self
956 956 # Changing project resets the custom field values
957 957 # TODO: handle this in Issue#project=
958 958 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
959 959 # Reassign fixed_versions by name, since names are unique per project
960 960 if issue.fixed_version && issue.fixed_version.project == project
961 961 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
962 962 end
963 963 # Reassign version custom field values
964 964 new_issue.custom_field_values.each do |custom_value|
965 965 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
966 966 versions = Version.where(:id => custom_value.value).to_a
967 967 new_value = versions.map do |version|
968 968 if version.project == project
969 969 self.versions.detect {|v| v.name == version.name}.try(:id)
970 970 else
971 971 version.id
972 972 end
973 973 end
974 974 new_value.compact!
975 975 new_value = new_value.first unless custom_value.custom_field.multiple?
976 976 custom_value.value = new_value
977 977 end
978 978 end
979 979 # Reassign the category by name, since names are unique per project
980 980 if issue.category
981 981 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
982 982 end
983 983 # Parent issue
984 984 if issue.parent_id
985 985 if copied_parent = issues_map[issue.parent_id]
986 986 new_issue.parent_issue_id = copied_parent.id
987 987 end
988 988 end
989 989
990 990 self.issues << new_issue
991 991 if new_issue.new_record?
992 992 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
993 993 else
994 994 issues_map[issue.id] = new_issue unless new_issue.new_record?
995 995 end
996 996 end
997 997
998 998 # Restore locked/closed version statuses
999 999 version_statuses.each do |version, status|
1000 1000 version.update_attribute :status, status
1001 1001 end
1002 1002
1003 1003 # Relations after in case issues related each other
1004 1004 project.issues.each do |issue|
1005 1005 new_issue = issues_map[issue.id]
1006 1006 unless new_issue
1007 1007 # Issue was not copied
1008 1008 next
1009 1009 end
1010 1010
1011 1011 # Relations
1012 1012 issue.relations_from.each do |source_relation|
1013 1013 new_issue_relation = IssueRelation.new
1014 1014 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
1015 1015 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
1016 1016 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
1017 1017 new_issue_relation.issue_to = source_relation.issue_to
1018 1018 end
1019 1019 new_issue.relations_from << new_issue_relation
1020 1020 end
1021 1021
1022 1022 issue.relations_to.each do |source_relation|
1023 1023 new_issue_relation = IssueRelation.new
1024 1024 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
1025 1025 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
1026 1026 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
1027 1027 new_issue_relation.issue_from = source_relation.issue_from
1028 1028 end
1029 1029 new_issue.relations_to << new_issue_relation
1030 1030 end
1031 1031 end
1032 1032 end
1033 1033
1034 1034 # Copies members from +project+
1035 1035 def copy_members(project)
1036 1036 # Copy users first, then groups to handle members with inherited and given roles
1037 1037 members_to_copy = []
1038 1038 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
1039 1039 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
1040 1040
1041 1041 members_to_copy.each do |member|
1042 1042 new_member = Member.new
1043 1043 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
1044 1044 # only copy non inherited roles
1045 1045 # inherited roles will be added when copying the group membership
1046 1046 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
1047 1047 next if role_ids.empty?
1048 1048 new_member.role_ids = role_ids
1049 1049 new_member.project = self
1050 1050 self.members << new_member
1051 1051 end
1052 1052 end
1053 1053
1054 1054 # Copies queries from +project+
1055 1055 def copy_queries(project)
1056 1056 project.queries.each do |query|
1057 1057 new_query = IssueQuery.new
1058 1058 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1059 1059 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1060 1060 new_query.project = self
1061 1061 new_query.user_id = query.user_id
1062 1062 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1063 1063 self.queries << new_query
1064 1064 end
1065 1065 end
1066 1066
1067 1067 # Copies boards from +project+
1068 1068 def copy_boards(project)
1069 1069 project.boards.each do |board|
1070 1070 new_board = Board.new
1071 1071 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1072 1072 new_board.project = self
1073 1073 self.boards << new_board
1074 1074 end
1075 1075 end
1076 1076
1077 1077 def allowed_permissions
1078 1078 @allowed_permissions ||= begin
1079 1079 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1080 1080 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1081 1081 end
1082 1082 end
1083 1083
1084 1084 def allowed_actions
1085 1085 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1086 1086 end
1087 1087
1088 1088 # Archives subprojects recursively
1089 1089 def archive!
1090 1090 children.each do |subproject|
1091 1091 subproject.send :archive!
1092 1092 end
1093 1093 update_attribute :status, STATUS_ARCHIVED
1094 1094 end
1095 1095 end
@@ -1,929 +1,948
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/sha1"
19 19
20 20 class User < Principal
21 21 include Redmine::SafeAttributes
22 22
23 23 # Different ways of displaying/sorting users
24 24 USER_FORMATS = {
25 25 :firstname_lastname => {
26 26 :string => '#{firstname} #{lastname}',
27 27 :order => %w(firstname lastname id),
28 28 :setting_order => 1
29 29 },
30 30 :firstname_lastinitial => {
31 31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 32 :order => %w(firstname lastname id),
33 33 :setting_order => 2
34 34 },
35 35 :firstinitial_lastname => {
36 36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 37 :order => %w(firstname lastname id),
38 38 :setting_order => 2
39 39 },
40 40 :firstname => {
41 41 :string => '#{firstname}',
42 42 :order => %w(firstname id),
43 43 :setting_order => 3
44 44 },
45 45 :lastname_firstname => {
46 46 :string => '#{lastname} #{firstname}',
47 47 :order => %w(lastname firstname id),
48 48 :setting_order => 4
49 49 },
50 50 :lastnamefirstname => {
51 51 :string => '#{lastname}#{firstname}',
52 52 :order => %w(lastname firstname id),
53 53 :setting_order => 5
54 54 },
55 55 :lastname_comma_firstname => {
56 56 :string => '#{lastname}, #{firstname}',
57 57 :order => %w(lastname firstname id),
58 58 :setting_order => 6
59 59 },
60 60 :lastname => {
61 61 :string => '#{lastname}',
62 62 :order => %w(lastname id),
63 63 :setting_order => 7
64 64 },
65 65 :username => {
66 66 :string => '#{login}',
67 67 :order => %w(login id),
68 68 :setting_order => 8
69 69 },
70 70 }
71 71
72 72 MAIL_NOTIFICATION_OPTIONS = [
73 73 ['all', :label_user_mail_option_all],
74 74 ['selected', :label_user_mail_option_selected],
75 75 ['only_my_events', :label_user_mail_option_only_my_events],
76 76 ['only_assigned', :label_user_mail_option_only_assigned],
77 77 ['only_owner', :label_user_mail_option_only_owner],
78 78 ['none', :label_user_mail_option_none]
79 79 ]
80 80
81 81 has_and_belongs_to_many :groups,
82 82 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
83 83 :after_add => Proc.new {|user, group| group.user_added(user)},
84 84 :after_remove => Proc.new {|user, group| group.user_removed(user)}
85 85 has_many :changesets, :dependent => :nullify
86 86 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
87 87 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
88 88 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
89 89 has_one :email_address, lambda {where :is_default => true}, :autosave => true
90 90 has_many :email_addresses, :dependent => :delete_all
91 91 belongs_to :auth_source
92 92
93 93 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
94 94 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
95 95
96 96 acts_as_customizable
97 97
98 98 attr_accessor :password, :password_confirmation, :generate_password
99 99 attr_accessor :last_before_login_on
100 100 attr_accessor :remote_ip
101 101
102 102 # Prevents unauthorized assignments
103 103 attr_protected :password, :password_confirmation, :hashed_password
104 104
105 105 LOGIN_LENGTH_LIMIT = 60
106 106 MAIL_LENGTH_LIMIT = 60
107 107
108 108 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
109 109 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
110 110 # Login must contain letters, numbers, underscores only
111 111 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
112 112 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
113 113 validates_length_of :firstname, :lastname, :maximum => 30
114 114 validates_length_of :identity_url, maximum: 255
115 115 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
116 116 validate :validate_password_length
117 117 validate do
118 118 if password_confirmation && password != password_confirmation
119 119 errors.add(:password, :confirmation)
120 120 end
121 121 end
122 122
123 123 self.valid_statuses = [STATUS_ACTIVE, STATUS_REGISTERED, STATUS_LOCKED]
124 124
125 125 before_validation :instantiate_email_address
126 126 before_create :set_mail_notification
127 127 before_save :generate_password_if_needed, :update_hashed_password
128 128 before_destroy :remove_references_before_destroy
129 129 after_save :update_notified_project_ids, :destroy_tokens, :deliver_security_notification
130 130 after_destroy :deliver_security_notification
131 131
132 132 scope :in_group, lambda {|group|
133 133 group_id = group.is_a?(Group) ? group.id : group.to_i
134 134 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
135 135 }
136 136 scope :not_in_group, lambda {|group|
137 137 group_id = group.is_a?(Group) ? group.id : group.to_i
138 138 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
139 139 }
140 140 scope :sorted, lambda { order(*User.fields_for_order_statement)}
141 141 scope :having_mail, lambda {|arg|
142 142 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
143 143 if addresses.any?
144 144 joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).distinct
145 145 else
146 146 none
147 147 end
148 148 }
149 149
150 150 def set_mail_notification
151 151 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
152 152 true
153 153 end
154 154
155 155 def update_hashed_password
156 156 # update hashed_password if password was set
157 157 if self.password && self.auth_source_id.blank?
158 158 salt_password(password)
159 159 end
160 160 end
161 161
162 162 alias :base_reload :reload
163 163 def reload(*args)
164 164 @name = nil
165 165 @projects_by_role = nil
166 @project_ids_by_role = nil
166 167 @membership_by_project_id = nil
167 168 @notified_projects_ids = nil
168 169 @notified_projects_ids_changed = false
169 170 @builtin_role = nil
170 171 @visible_project_ids = nil
171 172 @managed_roles = nil
172 173 base_reload(*args)
173 174 end
174 175
175 176 def mail
176 177 email_address.try(:address)
177 178 end
178 179
179 180 def mail=(arg)
180 181 email = email_address || build_email_address
181 182 email.address = arg
182 183 end
183 184
184 185 def mail_changed?
185 186 email_address.try(:address_changed?)
186 187 end
187 188
188 189 def mails
189 190 email_addresses.pluck(:address)
190 191 end
191 192
192 193 def self.find_or_initialize_by_identity_url(url)
193 194 user = where(:identity_url => url).first
194 195 unless user
195 196 user = User.new
196 197 user.identity_url = url
197 198 end
198 199 user
199 200 end
200 201
201 202 def identity_url=(url)
202 203 if url.blank?
203 204 write_attribute(:identity_url, '')
204 205 else
205 206 begin
206 207 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
207 208 rescue OpenIdAuthentication::InvalidOpenId
208 209 # Invalid url, don't save
209 210 end
210 211 end
211 212 self.read_attribute(:identity_url)
212 213 end
213 214
214 215 # Returns the user that matches provided login and password, or nil
215 216 def self.try_to_login(login, password, active_only=true)
216 217 login = login.to_s
217 218 password = password.to_s
218 219
219 220 # Make sure no one can sign in with an empty login or password
220 221 return nil if login.empty? || password.empty?
221 222 user = find_by_login(login)
222 223 if user
223 224 # user is already in local database
224 225 return nil unless user.check_password?(password)
225 226 return nil if !user.active? && active_only
226 227 else
227 228 # user is not yet registered, try to authenticate with available sources
228 229 attrs = AuthSource.authenticate(login, password)
229 230 if attrs
230 231 user = new(attrs)
231 232 user.login = login
232 233 user.language = Setting.default_language
233 234 if user.save
234 235 user.reload
235 236 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
236 237 end
237 238 end
238 239 end
239 240 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
240 241 user
241 242 rescue => text
242 243 raise text
243 244 end
244 245
245 246 # Returns the user who matches the given autologin +key+ or nil
246 247 def self.try_to_autologin(key)
247 248 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
248 249 if user
249 250 user.update_column(:last_login_on, Time.now)
250 251 user
251 252 end
252 253 end
253 254
254 255 def self.name_formatter(formatter = nil)
255 256 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
256 257 end
257 258
258 259 # Returns an array of fields names than can be used to make an order statement for users
259 260 # according to how user names are displayed
260 261 # Examples:
261 262 #
262 263 # User.fields_for_order_statement => ['users.login', 'users.id']
263 264 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
264 265 def self.fields_for_order_statement(table=nil)
265 266 table ||= table_name
266 267 name_formatter[:order].map {|field| "#{table}.#{field}"}
267 268 end
268 269
269 270 # Return user's full name for display
270 271 def name(formatter = nil)
271 272 f = self.class.name_formatter(formatter)
272 273 if formatter
273 274 eval('"' + f[:string] + '"')
274 275 else
275 276 @name ||= eval('"' + f[:string] + '"')
276 277 end
277 278 end
278 279
279 280 def active?
280 281 self.status == STATUS_ACTIVE
281 282 end
282 283
283 284 def registered?
284 285 self.status == STATUS_REGISTERED
285 286 end
286 287
287 288 def locked?
288 289 self.status == STATUS_LOCKED
289 290 end
290 291
291 292 def activate
292 293 self.status = STATUS_ACTIVE
293 294 end
294 295
295 296 def register
296 297 self.status = STATUS_REGISTERED
297 298 end
298 299
299 300 def lock
300 301 self.status = STATUS_LOCKED
301 302 end
302 303
303 304 def activate!
304 305 update_attribute(:status, STATUS_ACTIVE)
305 306 end
306 307
307 308 def register!
308 309 update_attribute(:status, STATUS_REGISTERED)
309 310 end
310 311
311 312 def lock!
312 313 update_attribute(:status, STATUS_LOCKED)
313 314 end
314 315
315 316 # Returns true if +clear_password+ is the correct user's password, otherwise false
316 317 def check_password?(clear_password)
317 318 if auth_source_id.present?
318 319 auth_source.authenticate(self.login, clear_password)
319 320 else
320 321 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
321 322 end
322 323 end
323 324
324 325 # Generates a random salt and computes hashed_password for +clear_password+
325 326 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
326 327 def salt_password(clear_password)
327 328 self.salt = User.generate_salt
328 329 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
329 330 self.passwd_changed_on = Time.now.change(:usec => 0)
330 331 end
331 332
332 333 # Does the backend storage allow this user to change their password?
333 334 def change_password_allowed?
334 335 return true if auth_source.nil?
335 336 return auth_source.allow_password_changes?
336 337 end
337 338
338 339 # Returns true if the user password has expired
339 340 def password_expired?
340 341 period = Setting.password_max_age.to_i
341 342 if period.zero?
342 343 false
343 344 else
344 345 changed_on = self.passwd_changed_on || Time.at(0)
345 346 changed_on < period.days.ago
346 347 end
347 348 end
348 349
349 350 def must_change_password?
350 351 (must_change_passwd? || password_expired?) && change_password_allowed?
351 352 end
352 353
353 354 def generate_password?
354 355 generate_password == '1' || generate_password == true
355 356 end
356 357
357 358 # Generate and set a random password on given length
358 359 def random_password(length=40)
359 360 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
360 361 chars -= %w(0 O 1 l)
361 362 password = ''
362 363 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
363 364 self.password = password
364 365 self.password_confirmation = password
365 366 self
366 367 end
367 368
368 369 def pref
369 370 self.preference ||= UserPreference.new(:user => self)
370 371 end
371 372
372 373 def time_zone
373 374 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
374 375 end
375 376
376 377 def force_default_language?
377 378 Setting.force_default_language_for_loggedin?
378 379 end
379 380
380 381 def language
381 382 if force_default_language?
382 383 Setting.default_language
383 384 else
384 385 super
385 386 end
386 387 end
387 388
388 389 def wants_comments_in_reverse_order?
389 390 self.pref[:comments_sorting] == 'desc'
390 391 end
391 392
392 393 # Return user's RSS key (a 40 chars long string), used to access feeds
393 394 def rss_key
394 395 if rss_token.nil?
395 396 create_rss_token(:action => 'feeds')
396 397 end
397 398 rss_token.value
398 399 end
399 400
400 401 # Return user's API key (a 40 chars long string), used to access the API
401 402 def api_key
402 403 if api_token.nil?
403 404 create_api_token(:action => 'api')
404 405 end
405 406 api_token.value
406 407 end
407 408
408 409 # Generates a new session token and returns its value
409 410 def generate_session_token
410 411 token = Token.create!(:user_id => id, :action => 'session')
411 412 token.value
412 413 end
413 414
414 415 # Returns true if token is a valid session token for the user whose id is user_id
415 416 def self.verify_session_token(user_id, token)
416 417 return false if user_id.blank? || token.blank?
417 418
418 419 scope = Token.where(:user_id => user_id, :value => token.to_s, :action => 'session')
419 420 if Setting.session_lifetime?
420 421 scope = scope.where("created_on > ?", Setting.session_lifetime.to_i.minutes.ago)
421 422 end
422 423 if Setting.session_timeout?
423 424 scope = scope.where("updated_on > ?", Setting.session_timeout.to_i.minutes.ago)
424 425 end
425 426 scope.update_all(:updated_on => Time.now) == 1
426 427 end
427 428
428 429 # Return an array of project ids for which the user has explicitly turned mail notifications on
429 430 def notified_projects_ids
430 431 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
431 432 end
432 433
433 434 def notified_project_ids=(ids)
434 435 @notified_projects_ids_changed = true
435 436 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
436 437 end
437 438
438 439 # Updates per project notifications (after_save callback)
439 440 def update_notified_project_ids
440 441 if @notified_projects_ids_changed
441 442 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
442 443 members.update_all(:mail_notification => false)
443 444 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
444 445 end
445 446 end
446 447 private :update_notified_project_ids
447 448
448 449 def valid_notification_options
449 450 self.class.valid_notification_options(self)
450 451 end
451 452
452 453 # Only users that belong to more than 1 project can select projects for which they are notified
453 454 def self.valid_notification_options(user=nil)
454 455 # Note that @user.membership.size would fail since AR ignores
455 456 # :include association option when doing a count
456 457 if user.nil? || user.memberships.length < 1
457 458 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
458 459 else
459 460 MAIL_NOTIFICATION_OPTIONS
460 461 end
461 462 end
462 463
463 464 # Find a user account by matching the exact login and then a case-insensitive
464 465 # version. Exact matches will be given priority.
465 466 def self.find_by_login(login)
466 467 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
467 468 if login.present?
468 469 # First look for an exact match
469 470 user = where(:login => login).detect {|u| u.login == login}
470 471 unless user
471 472 # Fail over to case-insensitive if none was found
472 473 user = where("LOWER(login) = ?", login.downcase).first
473 474 end
474 475 user
475 476 end
476 477 end
477 478
478 479 def self.find_by_rss_key(key)
479 480 Token.find_active_user('feeds', key)
480 481 end
481 482
482 483 def self.find_by_api_key(key)
483 484 Token.find_active_user('api', key)
484 485 end
485 486
486 487 # Makes find_by_mail case-insensitive
487 488 def self.find_by_mail(mail)
488 489 having_mail(mail).first
489 490 end
490 491
491 492 # Returns true if the default admin account can no longer be used
492 493 def self.default_admin_account_changed?
493 494 !User.active.find_by_login("admin").try(:check_password?, "admin")
494 495 end
495 496
496 497 def to_s
497 498 name
498 499 end
499 500
500 501 CSS_CLASS_BY_STATUS = {
501 502 STATUS_ANONYMOUS => 'anon',
502 503 STATUS_ACTIVE => 'active',
503 504 STATUS_REGISTERED => 'registered',
504 505 STATUS_LOCKED => 'locked'
505 506 }
506 507
507 508 def css_classes
508 509 "user #{CSS_CLASS_BY_STATUS[status]}"
509 510 end
510 511
511 512 # Returns the current day according to user's time zone
512 513 def today
513 514 if time_zone.nil?
514 515 Date.today
515 516 else
516 517 time_zone.today
517 518 end
518 519 end
519 520
520 521 # Returns the day of +time+ according to user's time zone
521 522 def time_to_date(time)
522 523 if time_zone.nil?
523 524 time.to_date
524 525 else
525 526 time.in_time_zone(time_zone).to_date
526 527 end
527 528 end
528 529
529 530 def logged?
530 531 true
531 532 end
532 533
533 534 def anonymous?
534 535 !logged?
535 536 end
536 537
537 538 # Returns user's membership for the given project
538 539 # or nil if the user is not a member of project
539 540 def membership(project)
540 541 project_id = project.is_a?(Project) ? project.id : project
541 542
542 543 @membership_by_project_id ||= Hash.new {|h, project_id|
543 544 h[project_id] = memberships.where(:project_id => project_id).first
544 545 }
545 546 @membership_by_project_id[project_id]
546 547 end
547 548
548 549 # Returns the user's bult-in role
549 550 def builtin_role
550 551 @builtin_role ||= Role.non_member
551 552 end
552 553
553 554 # Return user's roles for project
554 555 def roles_for_project(project)
555 556 # No role on archived projects
556 557 return [] if project.nil? || project.archived?
557 558 if membership = membership(project)
558 559 membership.roles.to_a
559 560 elsif project.is_public?
560 561 project.override_roles(builtin_role)
561 562 else
562 563 []
563 564 end
564 565 end
565 566
566 567 # Returns a hash of user's projects grouped by roles
568 # TODO: No longer used, should be deprecated
567 569 def projects_by_role
568 570 return @projects_by_role if @projects_by_role
569 571
570 hash = Hash.new([])
572 result = Hash.new([])
573 project_ids_by_role.each do |role, ids|
574 result[role] = Project.where(:id => ids).to_a
575 end
576 @projects_by_role = result
577 end
578
579 # Returns a hash of project ids grouped by roles.
580 # Includes the projects that the user is a member of and the projects
581 # that grant custom permissions to the builtin groups.
582 def project_ids_by_role
583 return @project_ids_by_role if @project_ids_by_role
571 584
572 585 group_class = anonymous? ? GroupAnonymous : GroupNonMember
573 members = Member.joins(:project, :principal).
586 group_id = group_class.pluck(:id).first
587
588 members = Member.joins(:project, :member_roles).
574 589 where("#{Project.table_name}.status <> 9").
575 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
576 preload(:project, :roles).
577 to_a
578
579 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
580 members.each do |member|
581 if member.project
582 member.roles.each do |role|
583 hash[role] = [] unless hash.key?(role)
584 hash[role] << member.project
585 end
586 end
587 end
590 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Member.table_name}.user_id = ?)", self.id, true, group_id).
591 pluck(:user_id, :role_id, :project_id)
592
593 hash = {}
594 members.each do |user_id, role_id, project_id|
595 # Ignore the roles of the builtin group if the user is a member of the project
596 next if user_id != id && project_ids.include?(project_id)
588 597
589 hash.each do |role, projects|
590 projects.uniq!
598 hash[role_id] ||= []
599 hash[role_id] << project_id
591 600 end
592 601
593 @projects_by_role = hash
602 result = Hash.new([])
603 if hash.present?
604 roles = Role.where(:id => hash.keys).to_a
605 hash.each do |role_id, proj_ids|
606 role = roles.detect {|r| r.id == role_id}
607 if role
608 result[role] = proj_ids.uniq
609 end
610 end
611 end
612 @project_ids_by_role = result
594 613 end
595 614
596 615 # Returns the ids of visible projects
597 616 def visible_project_ids
598 617 @visible_project_ids ||= Project.visible(self).pluck(:id)
599 618 end
600 619
601 620 # Returns the roles that the user is allowed to manage for the given project
602 621 def managed_roles(project)
603 622 if admin?
604 623 @managed_roles ||= Role.givable.to_a
605 624 else
606 625 membership(project).try(:managed_roles) || []
607 626 end
608 627 end
609 628
610 629 # Returns true if user is arg or belongs to arg
611 630 def is_or_belongs_to?(arg)
612 631 if arg.is_a?(User)
613 632 self == arg
614 633 elsif arg.is_a?(Group)
615 634 arg.users.include?(self)
616 635 else
617 636 false
618 637 end
619 638 end
620 639
621 640 # Return true if the user is allowed to do the specified action on a specific context
622 641 # Action can be:
623 642 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
624 643 # * a permission Symbol (eg. :edit_project)
625 644 # Context can be:
626 645 # * a project : returns true if user is allowed to do the specified action on this project
627 646 # * an array of projects : returns true if user is allowed on every project
628 647 # * nil with options[:global] set : check if user has at least one role allowed for this action,
629 648 # or falls back to Non Member / Anonymous permissions depending if the user is logged
630 649 def allowed_to?(action, context, options={}, &block)
631 650 if context && context.is_a?(Project)
632 651 return false unless context.allows_to?(action)
633 652 # Admin users are authorized for anything else
634 653 return true if admin?
635 654
636 655 roles = roles_for_project(context)
637 656 return false unless roles
638 657 roles.any? {|role|
639 658 (context.is_public? || role.member?) &&
640 659 role.allowed_to?(action) &&
641 660 (block_given? ? yield(role, self) : true)
642 661 }
643 662 elsif context && context.is_a?(Array)
644 663 if context.empty?
645 664 false
646 665 else
647 666 # Authorize if user is authorized on every element of the array
648 667 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
649 668 end
650 669 elsif context
651 670 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
652 671 elsif options[:global]
653 672 # Admin users are always authorized
654 673 return true if admin?
655 674
656 675 # authorize if user has at least one role that has this permission
657 676 roles = memberships.collect {|m| m.roles}.flatten.uniq
658 677 roles << (self.logged? ? Role.non_member : Role.anonymous)
659 678 roles.any? {|role|
660 679 role.allowed_to?(action) &&
661 680 (block_given? ? yield(role, self) : true)
662 681 }
663 682 else
664 683 false
665 684 end
666 685 end
667 686
668 687 # Is the user allowed to do the specified action on any project?
669 688 # See allowed_to? for the actions and valid options.
670 689 #
671 690 # NB: this method is not used anywhere in the core codebase as of
672 691 # 2.5.2, but it's used by many plugins so if we ever want to remove
673 692 # it it has to be carefully deprecated for a version or two.
674 693 def allowed_to_globally?(action, options={}, &block)
675 694 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
676 695 end
677 696
678 697 def allowed_to_view_all_time_entries?(context)
679 698 allowed_to?(:view_time_entries, context) do |role, user|
680 699 role.time_entries_visibility == 'all'
681 700 end
682 701 end
683 702
684 703 # Returns true if the user is allowed to delete the user's own account
685 704 def own_account_deletable?
686 705 Setting.unsubscribe? &&
687 706 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
688 707 end
689 708
690 709 safe_attributes 'firstname',
691 710 'lastname',
692 711 'mail',
693 712 'mail_notification',
694 713 'notified_project_ids',
695 714 'language',
696 715 'custom_field_values',
697 716 'custom_fields',
698 717 'identity_url'
699 718
700 719 safe_attributes 'login',
701 720 :if => lambda {|user, current_user| user.new_record?}
702 721
703 722 safe_attributes 'status',
704 723 'auth_source_id',
705 724 'generate_password',
706 725 'must_change_passwd',
707 726 'login',
708 727 'admin',
709 728 :if => lambda {|user, current_user| current_user.admin?}
710 729
711 730 safe_attributes 'group_ids',
712 731 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
713 732
714 733 # Utility method to help check if a user should be notified about an
715 734 # event.
716 735 #
717 736 # TODO: only supports Issue events currently
718 737 def notify_about?(object)
719 738 if mail_notification == 'all'
720 739 true
721 740 elsif mail_notification.blank? || mail_notification == 'none'
722 741 false
723 742 else
724 743 case object
725 744 when Issue
726 745 case mail_notification
727 746 when 'selected', 'only_my_events'
728 747 # user receives notifications for created/assigned issues on unselected projects
729 748 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
730 749 when 'only_assigned'
731 750 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
732 751 when 'only_owner'
733 752 object.author == self
734 753 end
735 754 when News
736 755 # always send to project members except when mail_notification is set to 'none'
737 756 true
738 757 end
739 758 end
740 759 end
741 760
742 761 def self.current=(user)
743 762 RequestStore.store[:current_user] = user
744 763 end
745 764
746 765 def self.current
747 766 RequestStore.store[:current_user] ||= User.anonymous
748 767 end
749 768
750 769 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
751 770 # one anonymous user per database.
752 771 def self.anonymous
753 772 anonymous_user = AnonymousUser.unscoped.first
754 773 if anonymous_user.nil?
755 774 anonymous_user = AnonymousUser.unscoped.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
756 775 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
757 776 end
758 777 anonymous_user
759 778 end
760 779
761 780 # Salts all existing unsalted passwords
762 781 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
763 782 # This method is used in the SaltPasswords migration and is to be kept as is
764 783 def self.salt_unsalted_passwords!
765 784 transaction do
766 785 User.where("salt IS NULL OR salt = ''").find_each do |user|
767 786 next if user.hashed_password.blank?
768 787 salt = User.generate_salt
769 788 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
770 789 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
771 790 end
772 791 end
773 792 end
774 793
775 794 protected
776 795
777 796 def validate_password_length
778 797 return if password.blank? && generate_password?
779 798 # Password length validation based on setting
780 799 if !password.nil? && password.size < Setting.password_min_length.to_i
781 800 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
782 801 end
783 802 end
784 803
785 804 def instantiate_email_address
786 805 email_address || build_email_address
787 806 end
788 807
789 808 private
790 809
791 810 def generate_password_if_needed
792 811 if generate_password? && auth_source.nil?
793 812 length = [Setting.password_min_length.to_i + 2, 10].max
794 813 random_password(length)
795 814 end
796 815 end
797 816
798 817 # Delete all outstanding password reset tokens on password change.
799 818 # Delete the autologin tokens on password change to prohibit session leakage.
800 819 # This helps to keep the account secure in case the associated email account
801 820 # was compromised.
802 821 def destroy_tokens
803 822 if hashed_password_changed? || (status_changed? && !active?)
804 823 tokens = ['recovery', 'autologin', 'session']
805 824 Token.where(:user_id => id, :action => tokens).delete_all
806 825 end
807 826 end
808 827
809 828 # Removes references that are not handled by associations
810 829 # Things that are not deleted are reassociated with the anonymous user
811 830 def remove_references_before_destroy
812 831 return if self.id.nil?
813 832
814 833 substitute = User.anonymous
815 834 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
816 835 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
817 836 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
818 837 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
819 838 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
820 839 JournalDetail.
821 840 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
822 841 update_all(['old_value = ?', substitute.id.to_s])
823 842 JournalDetail.
824 843 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
825 844 update_all(['value = ?', substitute.id.to_s])
826 845 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
827 846 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
828 847 # Remove private queries and keep public ones
829 848 ::Query.where('user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE).delete_all
830 849 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
831 850 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
832 851 Token.where('user_id = ?', id).delete_all
833 852 Watcher.where('user_id = ?', id).delete_all
834 853 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
835 854 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
836 855 end
837 856
838 857 # Return password digest
839 858 def self.hash_password(clear_password)
840 859 Digest::SHA1.hexdigest(clear_password || "")
841 860 end
842 861
843 862 # Returns a 128bits random salt as a hex string (32 chars long)
844 863 def self.generate_salt
845 864 Redmine::Utils.random_hex(16)
846 865 end
847 866
848 867 # Send a security notification to all admins if the user has gained/lost admin privileges
849 868 def deliver_security_notification
850 869 options = {
851 870 field: :field_admin,
852 871 value: login,
853 872 title: :label_user_plural,
854 873 url: {controller: 'users', action: 'index'}
855 874 }
856 875
857 876 deliver = false
858 877 if (admin? && id_changed? && active?) || # newly created admin
859 878 (admin? && admin_changed? && active?) || # regular user became admin
860 879 (admin? && status_changed? && active?) # locked admin became active again
861 880
862 881 deliver = true
863 882 options[:message] = :mail_body_security_notification_add
864 883
865 884 elsif (admin? && destroyed? && active?) || # active admin user was deleted
866 885 (!admin? && admin_changed? && active?) || # admin is no longer admin
867 886 (admin? && status_changed? && !active?) # admin was locked
868 887
869 888 deliver = true
870 889 options[:message] = :mail_body_security_notification_remove
871 890 end
872 891
873 892 if deliver
874 893 users = User.active.where(admin: true).to_a
875 894 Mailer.security_notification(users, options).deliver
876 895 end
877 896 end
878 897 end
879 898
880 899 class AnonymousUser < User
881 900 validate :validate_anonymous_uniqueness, :on => :create
882 901
883 902 self.valid_statuses = [STATUS_ANONYMOUS]
884 903
885 904 def validate_anonymous_uniqueness
886 905 # There should be only one AnonymousUser in the database
887 906 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
888 907 end
889 908
890 909 def available_custom_fields
891 910 []
892 911 end
893 912
894 913 # Overrides a few properties
895 914 def logged?; false end
896 915 def admin; false end
897 916 def name(*args); I18n.t(:label_user_anonymous) end
898 917 def mail=(*args); nil end
899 918 def mail; nil end
900 919 def time_zone; nil end
901 920 def rss_key; nil end
902 921
903 922 def pref
904 923 UserPreference.new(:user => self)
905 924 end
906 925
907 926 # Returns the user's bult-in role
908 927 def builtin_role
909 928 @builtin_role ||= Role.anonymous
910 929 end
911 930
912 931 def membership(*args)
913 932 nil
914 933 end
915 934
916 935 def member_of?(*args)
917 936 false
918 937 end
919 938
920 939 # Anonymous user can not be destroyed
921 940 def destroy
922 941 false
923 942 end
924 943
925 944 protected
926 945
927 946 def instantiate_email_address
928 947 end
929 948 end
General Comments 0
You need to be logged in to leave comments. Login now