Redmine 4.1.1

This commit is contained in:
Manuel Cillero 2020-11-22 21:20:06 +01:00
parent 33e7b881a5
commit 3d976f1b3b
1593 changed files with 36180 additions and 19489 deletions

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -99,9 +101,6 @@ class User < Principal
attr_accessor :last_before_login_on
attr_accessor :remote_ip
# Prevents unauthorized assignments
attr_protected :password, :password_confirmation, :hashed_password
LOGIN_LENGTH_LIMIT = 60
MAIL_LENGTH_LIMIT = 60
@ -113,6 +112,9 @@ class User < Principal
validates_length_of :firstname, :lastname, :maximum => 30
validates_length_of :identity_url, maximum: 255
validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
Setting::PASSWORD_CHAR_CLASSES.each do |k, v|
validates_format_of :password, :with => v, :message => :"must_contain_#{k}", :allow_blank => true, :if => Proc.new {Setting.password_required_char_classes.include?(k)}
end
validate :validate_password_length
validate do
if password_confirmation && password != password_confirmation
@ -242,7 +244,7 @@ class User < Principal
end
end
end
user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
user.update_last_login_on! if user && !user.new_record? && user.active?
user
rescue => text
raise text
@ -252,7 +254,7 @@ class User < Principal
def self.try_to_autologin(key)
user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
if user
user.update_column(:last_login_on, Time.now)
user.update_last_login_on!
user
end
end
@ -318,6 +320,12 @@ class User < Principal
update_attribute(:status, STATUS_LOCKED)
end
def update_last_login_on!
return if last_login_on.present? && last_login_on >= 1.minute.ago
update_column(:last_login_on, Time.now)
end
# Returns true if +clear_password+ is the correct user's password, otherwise false
def check_password?(clear_password)
if auth_source_id.present?
@ -337,8 +345,7 @@ class User < Principal
# Does the backend storage allow this user to change their password?
def change_password_allowed?
return true if auth_source.nil?
return auth_source.allow_password_changes?
auth_source.nil? ? true : auth_source.allow_password_changes?
end
# Returns true if the user password has expired
@ -357,15 +364,27 @@ class User < Principal
end
def generate_password?
generate_password == '1' || generate_password == true
ActiveRecord::Type::Boolean.new.deserialize(generate_password)
end
# Generate and set a random password on given length
def random_password(length=40)
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
chars -= %w(0 O 1 l)
password = ''
length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
chars_list = [('A'..'Z').to_a, ('a'..'z').to_a, ('0'..'9').to_a]
# auto-generated passwords contain special characters only when admins
# require users to use passwords which contains special characters
if Setting.password_required_char_classes.include?('special_chars')
chars_list << ("\x20".."\x7e").to_a.select {|c| c =~ Setting::PASSWORD_CHAR_CLASSES['special_chars']}
end
chars_list.each {|v| v.reject! {|c| %(0O1l|'"`*).include?(c)}}
password = +''
chars_list.each do |chars|
password << chars[SecureRandom.random_number(chars.size)]
length -= 1
end
chars = chars_list.flatten
length.times { password << chars[SecureRandom.random_number(chars.size)] }
password = password.split('').shuffle(random: SecureRandom).join
self.password = password
self.password_confirmation = password
self
@ -489,7 +508,7 @@ class User < Principal
user = where(:login => login).detect {|u| u.login == login}
unless user
# Fail over to case-insensitive if none was found
user = where("LOWER(login) = ?", login.downcase).first
user = find_by("LOWER(login) = ?", login.downcase)
end
user
end
@ -517,7 +536,7 @@ class User < Principal
name
end
CSS_CLASS_BY_STATUS = {
LABEL_BY_STATUS = {
STATUS_ANONYMOUS => 'anon',
STATUS_ACTIVE => 'active',
STATUS_REGISTERED => 'registered',
@ -525,7 +544,7 @@ class User < Principal
}
def css_classes
"user #{CSS_CLASS_BY_STATUS[status]}"
"user #{LABEL_BY_STATUS[status]}"
end
# Returns the current day according to user's time zone
@ -539,10 +558,14 @@ class User < Principal
# Returns the day of +time+ according to user's time zone
def time_to_date(time)
if time_zone.nil?
time.to_date
self.convert_time_to_user_timezone(time).to_date
end
def convert_time_to_user_timezone(time)
if self.time_zone
time.in_time_zone(self.time_zone)
else
time.in_time_zone(time_zone).to_date
time.utc? ? time.localtime : time
end
end
@ -607,24 +630,24 @@ class User < Principal
# eg. project.children.visible(user)
Project.unscoped do
return @project_ids_by_role if @project_ids_by_role
group_class = anonymous? ? GroupAnonymous : GroupNonMember
group_id = group_class.pluck(:id).first
members = Member.joins(:project, :member_roles).
where("#{Project.table_name}.status <> 9").
where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Member.table_name}.user_id = ?)", self.id, true, group_id).
pluck(:user_id, :role_id, :project_id)
hash = {}
members.each do |user_id, role_id, project_id|
# Ignore the roles of the builtin group if the user is a member of the project
next if user_id != id && project_ids.include?(project_id)
hash[role_id] ||= []
hash[role_id] << project_id
end
result = Hash.new([])
if hash.present?
roles = Role.where(:id => hash.keys).to_a
@ -732,7 +755,8 @@ class User < Principal
(!admin? || User.active.admin.where("id <> ?", id).exists?)
end
safe_attributes 'firstname',
safe_attributes(
'firstname',
'lastname',
'mail',
'mail_notification',
@ -740,21 +764,21 @@ class User < Principal
'language',
'custom_field_values',
'custom_fields',
'identity_url'
safe_attributes 'login',
:if => lambda {|user, current_user| user.new_record?}
safe_attributes 'status',
'identity_url')
safe_attributes(
'login',
:if => lambda {|user, current_user| user.new_record?})
safe_attributes(
'status',
'auth_source_id',
'generate_password',
'must_change_passwd',
'login',
'admin',
:if => lambda {|user, current_user| current_user.admin?}
safe_attributes 'group_ids',
:if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
:if => lambda {|user, current_user| current_user.admin?})
safe_attributes(
'group_ids',
:if => lambda {|user, current_user| current_user.admin? && !user.new_record?})
# Utility method to help check if a user should be notified about an
# event.
@ -771,9 +795,9 @@ class User < Principal
case mail_notification
when 'selected', 'only_my_events'
# user receives notifications for created/assigned issues on unselected projects
object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.previous_assignee)
when 'only_assigned'
is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.previous_assignee)
when 'only_owner'
object.author == self
end
@ -795,7 +819,7 @@ class User < Principal
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
# one anonymous user per database.
def self.anonymous
anonymous_user = AnonymousUser.unscoped.first
anonymous_user = AnonymousUser.unscoped.find_by(:lastname => 'Anonymous')
if anonymous_user.nil?
anonymous_user = AnonymousUser.unscoped.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
@ -817,6 +841,13 @@ class User < Principal
end
end
def bookmarked_project_ids
project_ids = []
bookmarked_project_ids = self.pref[:bookmarked_project_ids]
project_ids = bookmarked_project_ids.split(',') unless bookmarked_project_ids.nil?
project_ids.map(&:to_i)
end
protected
def validate_password_length
@ -845,7 +876,7 @@ class User < Principal
# This helps to keep the account secure in case the associated email account
# was compromised.
def destroy_tokens
if hashed_password_changed? || (status_changed? && !active?)
if saved_change_to_hashed_password? || (saved_change_to_status? && !active?)
tokens = ['recovery', 'autologin', 'session']
Token.where(:user_id => id, :action => tokens).delete_all
end
@ -880,14 +911,17 @@ class User < Principal
WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
end
# Return password digest
def self.hash_password(clear_password)
Digest::SHA1.hexdigest(clear_password || "")
end
# Singleton class method is public
class << self
# Return password digest
def hash_password(clear_password)
Digest::SHA1.hexdigest(clear_password || "")
end
# Returns a 128bits random salt as a hex string (32 chars long)
def self.generate_salt
Redmine::Utils.random_hex(16)
# Returns a 128bits random salt as a hex string (32 chars long)
def generate_salt
Redmine::Utils.random_hex(16)
end
end
# Send a security notification to all admins if the user has gained/lost admin privileges
@ -900,16 +934,16 @@ class User < Principal
}
deliver = false
if (admin? && id_changed? && active?) || # newly created admin
(admin? && admin_changed? && active?) || # regular user became admin
(admin? && status_changed? && active?) # locked admin became active again
if (admin? && saved_change_to_id? && active?) || # newly created admin
(admin? && saved_change_to_admin? && active?) || # regular user became admin
(admin? && saved_change_to_status? && active?) # locked admin became active again
deliver = true
options[:message] = :mail_body_security_notification_add
elsif (admin? && destroyed? && active?) || # active admin user was deleted
(!admin? && admin_changed? && active?) || # admin is no longer admin
(admin? && status_changed? && !active?) # admin was locked
(!admin? && saved_change_to_admin? && active?) || # admin is no longer admin
(admin? && saved_change_to_status? && !active?) # admin was locked
deliver = true
options[:message] = :mail_body_security_notification_remove
@ -917,7 +951,7 @@ class User < Principal
if deliver
users = User.active.where(admin: true).to_a
Mailer.security_notification(users, options).deliver
Mailer.deliver_security_notification(users, User.current, options)
end
end
end