Authenticators refactoring
[gitorious:gitorious-fiit-mainline.git] / app / models / user.rb
1 # encoding: utf-8
2 #--
3 #   Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
4 #   Copyright (C) 2007, 2008 Johan Sørensen <johan@johansorensen.com>
5 #   Copyright (C) 2008 David A. Cuadrado <krawek@gmail.com>
6 #   Copyright (C) 2008 Patrick Aljord <patcito@gmail.com>
7 #   Copyright (C) 2008 Tor Arne Vestbø <tavestbo@trolltech.com>
8 #   Copyright (C) 2009 Fabio Akita <fabio.akita@gmail.com>
9 #
10 #   This program is free software: you can redistribute it and/or modify
11 #   it under the terms of the GNU Affero General Public License as published by
12 #   the Free Software Foundation, either version 3 of the License, or
13 #   (at your option) any later version.
14 #
15 #   This program is distributed in the hope that it will be useful,
16 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
17 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 #   GNU Affero General Public License for more details.
19 #
20 #   You should have received a copy of the GNU Affero General Public License
21 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 #++
23
24 require 'digest/sha1'
25 require 'net/ldap'
26 require_dependency "event"
27
28 class User < ActiveRecord::Base
29   include UrlLinting
30
31   has_many :projects
32   has_many :memberships, :dependent => :destroy
33   has_many :groups, :through => :memberships
34   has_many :repositories, :as => :owner, :conditions => ["kind != ?", Repository::KIND_WIKI],
35     :dependent => :destroy
36   has_many :cloneable_repositories, :class_name => "Repository",
37      :conditions => ["kind != ?", Repository::KIND_TRACKING_REPO]
38   has_many :committerships, :as => :committer, :dependent => :destroy
39   has_many :commit_repositories, :through => :committerships, :source => :repository,
40   :conditions => ["repositories.kind NOT IN (?)", Repository::KINDS_INTERNAL_REPO]
41   has_many :ssh_keys, :order => "id desc", :dependent => :destroy
42   has_many :comments
43   has_many :email_aliases, :class_name => "Email", :dependent => :destroy
44   has_many :events, :order => "events.created_at asc", :dependent => :destroy
45   has_many :events_as_target, :class_name => "Event", :as => :target
46   has_many :favorites, :dependent => :destroy
47   has_many :feed_items, :foreign_key => "watcher_id"
48
49   # Virtual attribute for the unencrypted password
50   attr_accessor :password, :current_password
51
52   attr_protected :login, :is_admin
53
54   # For new users we are a little more strict than for existing ones.
55   USERNAME_FORMAT = /[a-z0-9\-_\.]+/i.freeze
56   USERNAME_FORMAT_ON_CREATE = /[a-z0-9\-]+/.freeze
57   validates_presence_of     :login, :email,               :if => :password_required?
58   validates_format_of       :login, :with => /^#{USERNAME_FORMAT_ON_CREATE}$/i, :on => :create
59   validates_format_of       :login, :with => /^#{USERNAME_FORMAT}$/i, :on => :update
60   validates_format_of       :email, :with => Email::FORMAT
61   validates_presence_of     :password,                   :if => :password_required?
62   validates_presence_of     :password_confirmation,      :if => :password_required?
63   validates_length_of       :password, :within => 4..40, :if => :password_required?
64   validates_confirmation_of :password,                   :if => :password_required?
65   validates_length_of       :login,    :within => 3..40
66   validates_length_of       :email,    :within => 3..100
67   validates_uniqueness_of   :login, :email, :case_sensitive => false
68
69   validates_acceptance_of :terms_of_use, :on => :create, :allow_nil => false
70
71   before_save :encrypt_password
72   before_create :make_activation_code
73   before_validation :lint_identity_url, :downcase_login
74   after_save :expire_avatar_email_caches_if_avatar_was_changed
75   after_destroy :expire_avatar_email_caches
76
77   state_machine :aasm_state, :initial => :pending do
78     state :terms_accepted
79
80     event :accept_terms do
81       transition :pending => :terms_accepted
82     end
83
84   end
85
86   has_many :received_messages, :class_name => "Message",
87       :foreign_key => 'recipient_id', :order => "created_at DESC" do
88     def unread
89       find(:all, :conditions => {:aasm_state => "unread"})
90     end
91
92     def top_level
93       find(:all, :conditions => {:in_reply_to_id => nil})
94     end
95
96     def unread_count
97       count(:all, :conditions => {
98         :aasm_state => "unread",
99         :archived_by_recipient => false,
100       })
101     end
102   end
103
104   def all_messages
105     Message.find(:all, :conditions => ["sender_id = ? OR recipient_id = ?", self, self])
106   end
107
108   Paperclip::Attachment.interpolations['login'] = lambda{|attachment, style|
109     attachment.instance.login.downcase
110   }
111
112   avatar_local_path = '/system/:attachment/:login/:style/:basename.:extension'
113   has_attached_file :avatar,
114     :styles => { :medium => "300x300>", :thumb => "64x64>", :tiny => "24x24>" },
115     :url => avatar_local_path,
116     :path => ":rails_root/public#{avatar_local_path}"
117
118   # Top level messages either from or to me
119   def top_level_messages
120     Message.find_by_sql(["SELECT * FROM messages
121       WHERE (has_unread_replies=? AND sender_id=?)
122       OR recipient_id=?
123       AND in_reply_to_id IS NULL
124       ORDER BY last_activity_at DESC", true,self, self])
125   end
126
127   # Top level messages, excluding message threads that have been archived by me
128   def messages_in_inbox(limit=100)
129     Message.find_by_sql(["SELECT * from messages
130         WHERE ((sender_id != :user AND archived_by_recipient = :no AND recipient_id = :user)
131         OR (has_unread_replies = :yes AND archived_by_recipient = :no AND sender_id = :user))
132         AND in_reply_to_id IS NULL
133         ORDER BY last_activity_at DESC LIMIT :limit",
134                          {:user => self.id, :yes => true, :no => false, :limit => limit}])
135   end
136
137   has_many :sent_messages, :class_name => "Message",
138       :foreign_key => "sender_id", :order => "created_at DESC" do
139     def top_level
140       find(:all, :conditions => {:in_reply_to_id => nil})
141     end
142   end
143
144   def self.human_name
145     I18n.t("activerecord.models.user")
146   end
147
148   # Encrypts some data with the salt.
149   def self.encrypt(password, salt)
150     Digest::SHA1.hexdigest("--#{salt}--#{password}--")
151   end
152
153   def self.generate_random_password(n = 12)
154     ActiveSupport::SecureRandom.hex(n)
155   end
156
157   def self.generate_reset_password_key(n = 16)
158     ActiveSupport::SecureRandom.hex(n)
159   end
160
161   def self.find_avatar_for_email(email, version)
162     Rails.cache.fetch(email_avatar_cache_key(email, version)) do
163       result = if u = find_by_email_with_aliases(email)
164         if u.avatar?
165           u.avatar.url(version)
166         end
167       end
168       result || :nil
169     end
170   end
171
172   def self.email_avatar_cache_key(email, version)
173     "avatar_for_#{Digest::SHA1.hexdigest(email)}_#{version.to_s}"
174   end
175
176   # Finds a user either by his/her primary email, or one of his/hers aliases
177   def self.find_by_email_with_aliases(email)
178     user = User.find_by_email(email)
179     unless user
180       if email_alias = Email.find_confirmed_by_address(email)
181         user = email_alias.user
182       end
183     end
184     user
185   end
186
187   def self.most_active(limit = 10, cutoff = 3)
188     Rails.cache.fetch("users:most_active_pushers:#{limit}:#{cutoff}",
189         :expires_in => 1.hour) do
190       find(:all, :select => "users.*, events.action, count(events.id) as event_count",
191         :joins => :events, :group => "users.id", :order => "event_count desc",
192         :conditions => ["events.action = ? and events.created_at > ?",
193                         Action::COMMIT, cutoff.days.ago],
194         :limit => limit)
195     end
196   end
197
198   # A Hash of repository => count of mergerequests active in the
199   # repositories that the user is a reviewer in
200   def review_repositories_with_open_merge_request_count
201     mr_repository_ids = self.committerships.reviewers.find(:all,
202       :select => "repository_id").map{|c| c.repository_id }
203     Repository.find(:all, {
204         :select => "repositories.*, count(merge_requests.id) as open_merge_request_count",
205         :conditions => ["repositories.id in (?) and merge_requests.status = ?",
206                         mr_repository_ids, MergeRequest::STATUS_OPEN],
207         :group => "repositories.id",
208         :joins => :merge_requests,
209         :limit => 5
210       })
211   end
212
213   def validate
214     if !not_openid?
215       begin
216         OpenIdAuthentication.normalize_identifier(self.identity_url)
217       rescue OpenIdAuthentication::InvalidOpenId => e
218         errors.add(:identity_url, I18n.t( "user.invalid_url" ))
219       end
220     end
221   end
222
223   # Activates the user in the database.
224   def activate
225     @activated = true
226     self.attributes = {:activated_at => Time.now.utc, :activation_code => nil}
227     save(false)
228   end
229
230   def activated?
231     # the existence of an activation code means they have not activated yet
232     activation_code.nil?
233   end
234
235   # Returns true if the user has just been activated.
236   def recently_activated?
237     @activated
238   end
239
240   # Can this user be shown in public
241   def public?
242     activated?# && !pending?
243   end
244
245   # Encrypts the password with the user salt
246   def encrypt(password)
247     self.class.encrypt(password, salt)
248   end
249
250   def password_authenticated?(password)
251     crypted_password == encrypt(password)
252   end
253
254   def breadcrumb_parent
255     nil
256   end
257
258   def remember_token?
259     remember_token_expires_at && Time.now.utc < remember_token_expires_at
260   end
261
262   # These create and unset the fields required for remembering users between browser closes
263   def remember_me
264     remember_me_for 2.weeks
265   end
266
267   def remember_me_for(time)
268     remember_me_until time.from_now.utc
269   end
270
271   def remember_me_until(time)
272     self.remember_token_expires_at = time
273     self.remember_token            = encrypt("#{email}--#{remember_token_expires_at}")
274     save(false)
275   end
276
277   def forget_me
278     self.remember_token_expires_at = nil
279     self.remember_token            = nil
280     save(false)
281   end
282
283   def reset_password!
284     generated = User.generate_random_password
285     self.password = generated
286     self.password_confirmation = generated
287     self.save!
288     generated
289   end
290
291   def forgot_password!
292     generated_key = User.generate_reset_password_key
293     self.password_key = generated_key
294     self.save!
295     generated_key
296   end
297
298   def can_write_to?(repository)
299     repository.writable_by?(self)
300   end
301
302   def to_param
303     login
304   end
305
306   def to_param_with_prefix
307     "~#{to_param}"
308   end
309
310   def to_xml(opts = {})
311     super({ :only => [:login, :created_at, :fullname, :url] }.merge(opts))
312   end
313
314   def is_openid_only?
315     self.crypted_password.nil?
316   end
317
318   def suspended?
319     !suspended_at.nil?
320   end
321
322   def site_admin?
323     is_admin
324   end
325
326   # is +a_user+ an admin within this users realm
327   # (for duck-typing repository etc access related things)
328   def admin?(a_user)
329     self == a_user
330   end
331
332   # is +a_user+ a committer within this users realm
333   # (for duck-typing repository etc access related things)
334   def committer?(a_user)
335     self == a_user
336   end
337
338   def to_grit_actor
339     Grit::Actor.new(fullname.blank? ? login : fullname, email)
340   end
341
342   def title
343     fullname.blank? ? login : fullname
344   end
345
346   def in_openid_import_phase!
347     @in_openid_import_phase = true
348   end
349
350   def in_openid_import_phase?
351     return @in_openid_import_phase
352   end
353
354   def url=(an_url)
355     self[:url] = clean_url(an_url)
356   end
357
358   def expire_avatar_email_caches_if_avatar_was_changed
359     return unless avatar_updated_at_changed?
360     expire_avatar_email_caches
361   end
362
363   def expire_avatar_email_caches
364     avatar.styles.keys.each do |style|
365       (email_aliases.map(&:address) << email).each do |email|
366         Rails.cache.delete(self.class.email_avatar_cache_key(email, style))
367       end
368     end
369   end
370
371   def watched_objects
372     favorites.find(:all, {
373       :include => :watchable,
374       :order => "id desc"
375     }).collect(&:watchable)
376   end
377
378   def paginated_events_in_watchlist(pagination_options = {})
379     key = "paginated_events_in_watchlist:#{self.id}:#{pagination_options[:page] || 1}"
380     Rails.cache.fetch(key, :expires_in => 20.minutes) do
381       watched = feed_items.paginate({
382           :order => "created_at desc",
383           :total_entries => FeedItem.per_page+(FeedItem.per_page+1)
384         }.merge(pagination_options))
385
386       total = (watched.length < watched.per_page ? watched.length : watched.total_entries)
387       items = WillPaginate::Collection.new(watched.current_page, watched.per_page, total)
388       items.replace(Event.find(watched.map(&:event_id), {:order => "created_at desc"}))
389     end
390   end
391
392   protected
393     # before filter
394     def encrypt_password
395       return if password.blank?
396       self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record?
397       self.crypted_password = encrypt(password)
398     end
399
400     def password_required?
401       not_openid? && (crypted_password.blank? || !password.blank?)
402     end
403
404     def not_openid?
405       identity_url.blank?
406     end
407
408     def make_activation_code
409       self.activation_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
410     end
411
412     def lint_identity_url
413       return if not_openid?
414       self.identity_url = OpenIdAuthentication.normalize_identifier(self.identity_url)
415     rescue OpenIdAuthentication::InvalidOpenId
416       # validate will catch it instead
417     end
418
419     def downcase_login
420       login.downcase! if login
421     end
422 end