-
Notifications
You must be signed in to change notification settings - Fork 16
/
user.rb
503 lines (423 loc) · 15.9 KB
/
user.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
require 'digest/sha1'
require 'tzinfo'
require 'authlogic/crypto_providers/bcrypt'
=begin rdoc
There are two special users in each site :
[anon] Anonymous user. Used to set defaults for newly created users.
[su] This user has full access to all the content in zena. He/she can read/write/destroy
about anything. Even private content can be read/edited/removed by su. <em>This user should
only be used for emergency purpose</em>. This is why an ugly warning is shown on all pages when
logged in as su.
If you want to give administrative rights to a user, simply put him/her into the _admin_ group.
Users have access rights defined by the groups they belong to. They also have a 'status' indicating the kind of
things they can/cannot do :
+:admin+:: (60): can read/write/publish in any group, can create users/groups
+:user+:: (50): can read/write/publish
+:commentator+:: (40): can write comments
+:moderated+:: (30): can write moderated comments
+:reader+:: (20): can only read
+:deleted+:: ( 0): cannot login
TODO: when a user is 'destroyed', pass everything he owns to another user or just mark the user as 'deleted'...
=end
class User < ActiveRecord::Base
attr_accessor :zafu_cache, :asset_host
include Property
RESCUE_SKIN_ID = -1
ANY_SKIN_ID = 0
property do |p|
# nil ==> no dev mode
# -1 ==> rescue skin
# 0 ==> dev mode with normal skin
# xx ==> fixed skin
p.integer :dev_skin_id
end
acts_as_authentic do |c|
#c.transition_from_crypto_providers = Zena::InitialCryptoProvider
#c.crypto_provider = Authlogic::CryptoProviders::BCrypt
c.crypto_provider = Zena::CryptoProvider::Initial
c.validate_email_field = false
c.validate_login_field = false
c.require_password_confirmation = false
c.validate_password_field = false
end
# Dynamic resolution of the author class from the prototype
def self.node_user_proc
Proc.new do |h, r, s|
res = {:method => 'node', :nil => true}
if prototype = visitor.prototype
res[:class] = prototype.vclass
else
res[:class] = VirtualClass['Node']
end
res
end
end
include RubyLess
safe_attribute :login, :time_zone, :created_at, :updated_at, :lang, :id
safe_method :status => Number, :status_name => String,
:is_anon? => Boolean, :is_admin? => Boolean, :user? => Boolean, :commentator? => Boolean,
:moderated? => Boolean, :asset_host? => Boolean, [:in_group?, String] => Boolean,
:group_names => [String], :site => {:class => Site}
safe_context :node => node_user_proc,
:to_publish => ['Version'], :redactions => ['Version'], :proposed => ['Version'],
:comments_to_publish => ['Comment']
attr_accessible :login, :lang, :node, :time_zone, :status, :group_ids, :site_ids, :crypted_password, :password, :dev_skin_id, :node_attributes,
:login_attempt_count
attr_accessor :visited_node_ids
attr_accessor :ip
belongs_to :site
belongs_to :node, :dependent => :destroy # Do we want this ? (won't work if there are sub-nodes...)
has_and_belongs_to_many :groups
has_many :nodes
has_many :versions
before_validation :user_before_validation
validate :valid_groups
validate :valid_user
validate :valid_node
validates_uniqueness_of :login, :scope => :site_id
before_destroy :dont_destroy_protected_users
validates_presence_of :site_id
before_create :create_node
Status = {
:su => 80,
:admin => 60, # can create other users, manage site, etc
:user => 50, # can write articles + publish (depends on access rights)
:commentator => 40, # can write comments
:moderated => 30, # can write comments (moderated)
:reader => 20, # can read
:deleted => 0,
}.freeze
Num_to_status = Hash[*Status.map{|k,v| [v,k]}.flatten].freeze
class << self
# This method is used by authlogic and is only called from withing a Secure scope that
# enforces the proper site_id.
def find_allowed_user_by_login(login)
first(:conditions=>["login = ? and status > 0", login])
end
# Creates a new user without setting the defaults (used to create the first users of the site). Use
# new instead.
alias new_no_defaults new
# Creates a new user with the defaults set from the anonymous user.
def new(attrs={})
new_attrs = attrs.dup
anon = visitor.site.anon
# Set new user defaults based on the anonymous user.
[:lang, :time_zone, :status].each do |sym|
new_attrs[sym] = anon.send(sym) if attrs[sym].blank? && attrs[sym.to_s].blank?
end
super(new_attrs)
end
end
def node_with_secure
@node ||= secure(Node) { node_without_secure }
end
alias_method_chain :node, :secure
# Each time a node is found using secure (Zena::Acts::Secure or Zena::Acts::SecureNode), this method is
# called to set the visitor in the found object. This is also used to keep track of the opened nodes
# when rendering a page for the cache so we can know when to expire the cache.
def visit(obj, opts={})
if obj.kind_of? Node
obj.visitor = self #explicit visit
# keep track of the nodes connected to this visit to build the 'expire_with' list
visited_node_ids << obj[:id]
end
end
def visited_node_ids
@visited_node_ids ||= []
end
# Return the prototype user (used by Zafu and QueryBuilder to know the class
# of the visitor and during user creation)
def prototype
@prototype ||= begin
secure(Node) { Node.new_node(prototype_attributes) }
end
end
# Return a new Node to be used by a new User as base for creating the visitor Node.
def prototype_attributes
@prototype_attributes ||= begin
attrs = begin
if code = current_site.prop['usr_prototype_attributes']
hash = safe_eval(code)
if hash.kind_of?(Hash)
hash
else
{}
end
else
{}
end
rescue RubyLess::Error => err
{}
end
# This is a security feature to avoid :_parent_id set manually in usr_prototype_attributes.
attrs.stringify_keys!
unless attrs['parent_id']
attrs[:_parent_id] = current_site[:root_id]
end
attrs['klass'] ||= attrs.delete('class') || 'Node'
attrs
end
end
def node_attributes=(node_attrs)
node_attrs.stringify_keys!
if !node_attrs['id'].blank?
@node = secure!(Node) { Node.find_node_by_pseudo(node_attrs.delete('id')) }
self[:node_id] = @node.id
elsif self[:node_id]
@node = secure!(Node) { node_without_secure }
else
@node = secure(Node) { Node.new_node(prototype_attributes) }
end
@node.attributes = node_attrs || {}
end
def status_name
Num_to_status[status].to_s
end
# Return true if the user is in the admin group or if the user is the super user.
def is_admin?
status.to_i >= User::Status[:admin]
end
# Return true if the user is the anonymous user for the current visited site
def is_anon?
# tested in site_test
user_site.anon_id == self[:id] && (!new_record? || self[:login].nil?) # (when creating a new site, anon_id == nil)
end
# This is set on the user during login.
alias asset_host? asset_host
def group_names
@group_names ||= begin
groups.all(:order=>'name').map(&:name)
end
end
def in_group?(name)
group_names.include?(name)
end
# Return true if the user's status is high enough to start editing nodes.
def user?
status >= User::Status[:user]
end
# Return true if the user's status is high enough to write comments.
def commentator?
status >= User::Status[:moderated]
end
# Return true if the user's comments should be moderated.
def moderated?
status < User::Status[:commentator]
end
# Return true if the user's status is high enough to read. This is basically the same as
# not deleted?.
# TODO: test
def reader?
status >= User::Status[:reader]
end
# Return true if the user is deleted and should not be allowed to login.
# TODO: test
def deleted?
status == User::Status[:deleted]
end
# Return true if the visitor is allowed API acces
def api_authorized?
group_ids.include?(current_site.api_group_id)
end
# Returns a list of the group ids separated by commas for the user (this is used mainly in SQL clauses).
# TODO: Performance
#
# Zena::Db.fetch_ids("SELECT id FROM groups WHERE site_id = #{current_site.id} ORDER BY name ASC")
# Zena::Db.fetch_ids("SELECT group_id FROM groups_users WHERE user_id = #{id} ORDER BY name ASC", 'group_id')
def group_ids
@group_ids ||= if is_admin?
site.groups.map(&:id)
else
groups.all(:order=>'name').map(&:id)
end
end
# Return all groups (used in forms) managed by the user.
def all_groups
if is_admin?
site.groups
else
groups.all(:order=>'name').map(&:id)
end
end
# Define the groups the user belongs to.
def group_ids=(list)
# We have to do our own method to avoid rails loading groups which will not be secured.
@defined_group_ids = list
end
def time_zone=(tz)
self[:time_zone] = tz.blank? ? nil : tz
@tz = nil
end
#TODO: test
# return only the ids of the groups really set (not all groups for admin or the like)
def group_set_ids
@group_set_ids ||= groups.map{|g| g[:id]}
end
# TODO: test
def tz
@tz ||= TZInfo::Timezone.get(self[:time_zone] || "UTC")
rescue TZInfo::InvalidTimezoneIdentifier
@tz = TZInfo::Timezone.get("UTC")
end
def comments_to_publish
secure(Comment) { Comment.find(:all, :select=>'comments.*', :from=>'comments, nodes, discussions',
:conditions => ['comments.status = ? AND discussions.node_id = nodes.id AND comments.discussion_id = discussions.id AND nodes.dgroup_id IN (?)', Zena::Status::Prop, visitor.group_ids]) }
end
# List all versions proposed for publication that the user has the right to publish.
def to_publish
secure(Version) { Version.find(:all, :conditions => ['status = ? AND nodes.dgroup_id IN (?)', Zena::Status::Prop, visitor.group_ids]) }
end
# List all versions owned that are currently being written (status= +red+)
def redactions
secure(Version) { Version.find(:all, :conditions => ['status = ? AND versions.user_id = ?', Zena::Status::Red, self.id]) }
end
# List all versions owned that are currently being proposed (status= +prop+)
def proposed
secure(Version) { Version.find(:all, :conditions => ['status = ? AND versions.user_id = ?', Zena::Status::Prop, self.id]) }
end
def get_skin(node)
skin_zip = is_admin? ? dev_skin_id.to_i : 0
case skin_zip
when RESCUE_SKIN_ID
# rescue skin
nil
when ANY_SKIN_ID
# normal skin
current_site.skin || node.skin || (node.parent ? node.parent.skin : nil)
else
# find skin from zip
secure(Skin) { Skin.find_by_zip(skin_zip)}
end
end
def find_node(path, zip, name, request, need_write = false)
secure!(Node) do
if name =~ /^\d+$/
Node.find_by_zip(name)
elsif name
basepath = (path[0..-2] + [name]).map {|p| String.from_url_name(p) }.join('/')
Node.find_by_path(basepath) ||
Node.find_by_path(basepath, current_site.root_id, true)
else
Node.find_by_zip(zip)
end
end
end
def dev_mode?
!dev_skin_id.blank?
end
private
def user_site
self.site || visitor.site # site when User is new
end
def create_node
# do not try to create a node if the root node is not created yet
return unless visitor.site.root_id
@node.version.status = Zena::Status::Pub
unless @node.save
# What do we do with this error ?
raise Zena::InvalidRecord, "Could not create contact node for user #{self.id} in site #{site_id} (#{@node.errors.map{|k,v| [k,v]}.join(', ')})"
end
unless @node.publish_from
raise Zena::InvalidRecord, "Could not publish contact node for user #{user_id} in site #{site_id} (#{@node.errors.map{|k,v| [k,v]}.join(', ')})"
end
self.node_id = @node.id
end
# Set user defaults.
def user_before_validation
return true if current_site.being_created?
self[:site_id] = visitor.site[:id]
if new_record?
self.status = site.anon.status if status.blank?
self.lang = site.anon.lang if lang.blank?
elsif status.blank?
self.status = site.anon.status
end
if login.blank? && !is_anon?
self.login = name
end
if !is_admin?
# Make sure we remove dev_skin settings if user is not an admin.
self.dev_skin_id = nil
end
end
# Validates that anon user does not have a login, that other users have a password
# and that the login is unique for the sites the user belongs to.
def valid_user
self[:site_id] = visitor.site[:id]
if !site.being_created? && !visitor.is_admin? && visitor[:id] != self[:id]
errors.add('base', 'You do not have the rights to do this.')
return false
end
errors.add('lang', 'not available') unless site.lang_list.include?(lang)
if is_anon?
# Anonymous user *must* have an empty login
self[:login] = nil
self[:crypted_password] = nil
else
if new_record?
# Refuse to add a user in a site if already a user with same login.
errors.add(:password, "can't be blank") if self[:crypted_password].nil? || self[:crypted_password] == ""
else
# get old password
old = User.find(self[:id])
self[:crypted_password] = old[:crypted_password] if self[:crypted_password].nil? || self[:crypted_password] == ""
errors.add(:login, "can't be blank") if self[:login].blank?
errors.add(:status, 'You do not have the rights to do this.') if self[:id] == visitor[:id] && old.is_admin? && self.status.to_i != old.status
end
end
if self[:time_zone]
begin
TZInfo::Timezone.get(self[:time_zone])
rescue
errors.add(:time_zone, 'invalid')
end
end
if @password_too_short
errors.add(:password, 'too short')
remove_instance_variable :@password_too_short
end
end
def valid_node
return unless visitor.site[:root_id] # do not validate node if the root node is not created yet
return if !new_record? && !@node
if !@node
# force creation of node, even if it is a plain copy of the prototype
self.node_attributes = {'title' => login}
else
@node.title ||= login
end
if @node.valid?
# ok
else
@node.errors.each_error do |err, msg|
errors.add("node[#{err}]", msg)
end
end
end
# Make sure all users are in the _public_ and _site_ groups. Make sure
# the user only belongs to groups in the same site.
def valid_groups #:doc:
g_ids = @defined_group_ids || (new_record? ? [] : group_set_ids)
g_ids.reject! { |g| g.blank? }
g_ids << site.public_group_id
g_ids << site.site_group_id unless is_anon?
g_ids.uniq!
g_ids.compact!
self.groups = []
g_ids.each do |id|
group = Group.find(:first, :conditions => {:id => id})
unless group && (group.site_id == self.site_id || site.being_created?)
errors.add('group', 'not found')
next
end
self.groups << group
end
end
# Do not allow destruction of the site's special users.
def dont_destroy_protected_users #:doc:
raise Zena::AccessViolation, "su and Anonymous users cannot be destroyed !" if site.protected_user_ids.include?(id)
end
def old
@old ||= self.class.find(self[:id])
end
end