Skip to content
This repository
Newer
Older
100644 892 lines (724 sloc) 30.11 kb
df971c39 »
2008-09-17 fix the licenses so they stop polluting all the other diffs
1 # The contents of this file are subject to the Common Public Attribution
4778b17e »
2008-06-17 initial checkin
2 # License Version 1.0. (the "License"); you may not use this file except in
3 # compliance with the License. You may obtain a copy of the License at
4 # http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
5 # License Version 1.1, but Sections 14 and 15 have been added to cover use of
6 # software over a computer network and provide for limited attribution for the
7 # Original Developer. In addition, Exhibit A has been modified to be consistent
8 # with Exhibit B.
2869eaf8 »
2010-05-03 New features:
9 #
4778b17e »
2008-06-17 initial checkin
10 # Software distributed under the License is distributed on an "AS IS" basis,
11 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
12 # the specific language governing rights and limitations under the License.
2869eaf8 »
2010-05-03 New features:
13 #
914b9492 »
2012-06-19 Update / add license headers.
14 # The Original Code is reddit.
2869eaf8 »
2010-05-03 New features:
15 #
914b9492 »
2012-06-19 Update / add license headers.
16 # The Original Developer is the Initial Developer. The Initial Developer of
17 # the Original Code is reddit Inc.
2869eaf8 »
2010-05-03 New features:
18 #
8af41547 »
2013-03-13 Update and fix license headers for 2013.
19 # All portions of the code written by reddit are Copyright (c) 2006-2013 reddit
914b9492 »
2012-06-19 Update / add license headers.
20 # Inc. All Rights Reserved.
21 ###############################################################################
22
4778b17e »
2008-06-17 initial checkin
23 from r2.lib.db.thing import Thing, Relation, NotFound
24 from r2.lib.db.operators import lower
25 from r2.lib.db.userrel import UserRel
2fe83ed7 »
2012-08-12 Add AccountActivityBySR.
26 from r2.lib.db import tdb_cassandra
e35b5196 »
2009-03-04 * removed references to clear_memo
27 from r2.lib.memoize import memoize
d251ba75 »
2010-05-20 * Improvements to the email verification system
28 from r2.lib.utils import modhash, valid_hash, randstr, timefromnow
2243af73 »
2012-06-11 Clean out dead last-modified rel code.
29 from r2.lib.utils import UrlParser
76594cde »
2012-12-21 Refactor and reorganize email canonicalization; add some tests.
30 from r2.lib.utils import constant_time_compare, canonicalize_email
2869eaf8 »
2010-05-03 New features:
31 from r2.lib.cache import sgm
34377138 »
2013-09-18 Email: Use non-hardcache ban fetching system
32 from r2.lib import filters, hooks
37e2ba98 »
2010-10-18 * Combine cassandra clusters into a single one
33 from r2.lib.log import log_text
555c02ad »
2012-05-24 Dual-write last visit timestamp.
34 from r2.models.last_modified import LastModified
d2ccc407 »
2014-02-10 Automatically delete password hashes of deleted accounts.
35 from r2.models.trylater import TryLater
4778b17e »
2008-06-17 initial checkin
36
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
37 from pylons import c, g, request
88d191ee »
2011-12-24 Gold feature: personal karma breakdown by subreddit.
38 from pylons.i18n import _
c9c65dd3 »
2012-06-15 Replace references to deprecated sha module with hashlib.
39 import time
40 import hashlib
4778b17e »
2008-06-17 initial checkin
41 from copy import copy
bf9f43cc »
2009-12-01 Messaging/commenting
42 from datetime import datetime, timedelta
a311805c »
2011-10-20 Switch to bcrypt for password hashing.
43 import bcrypt
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
44 import hmac
45 import hashlib
34377138 »
2013-09-18 Email: Use non-hardcache ban fetching system
46 import itertools
2fe83ed7 »
2012-08-12 Add AccountActivityBySR.
47 from pycassa.system_manager import ASCII_TYPE
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
48
4778b17e »
2008-06-17 initial checkin
49
d2ccc407 »
2014-02-10 Automatically delete password hashes of deleted accounts.
50 trylater_hooks = hooks.HookRegistrar()
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
51 COOKIE_TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S'
52
53
4778b17e »
2008-06-17 initial checkin
54 class AccountExists(Exception): pass
55
56 class Account(Thing):
57 _data_int_props = Thing._data_int_props + ('link_karma', 'comment_karma',
58 'report_made', 'report_correct',
59 'report_ignored', 'spammer',
68a06c56 »
2011-04-14 April 2011 Merge
60 'reported', 'gold_creddits', )
4778b17e »
2008-06-17 initial checkin
61 _int_prop_suffix = '_karma'
9a4271f6 »
2010-06-15 Upgrade Instructions
62 _essentials = ('name', )
4778b17e »
2008-06-17 initial checkin
63 _defaults = dict(pref_numsites = 25,
64 pref_frame = False,
e7eb9e63 »
2009-05-30 comments panel off by default
65 pref_frame_commentspanel = False,
4778b17e »
2008-06-17 initial checkin
66 pref_newwindow = False,
ae9eaf55 »
2009-06-03 add preference to remove recently clicked widget
67 pref_clickgadget = 5,
ff002a4b »
2013-10-24 Add backend for new gold feature: "remember my visits".
68 pref_store_visits = False,
4778b17e »
2008-06-17 initial checkin
69 pref_public_votes = False,
1b030c31 »
2011-06-15 Added option to hide user profile from robots.
70 pref_hide_from_robots = False,
37e2ba98 »
2010-10-18 * Combine cassandra clusters into a single one
71 pref_research = False,
4778b17e »
2008-06-17 initial checkin
72 pref_hide_ups = False,
4440ccfc »
2009-01-26 overhaul of JS and form handling code, not based on jQuery
73 pref_hide_downs = False,
4778b17e »
2008-06-17 initial checkin
74 pref_min_link_score = -4,
75 pref_min_comment_score = -4,
76 pref_num_comments = g.num_comments,
0745f5bf »
2009-05-13 make 1/2 of the default reddits English reddits if the user hasn't se…
77 pref_lang = g.lang,
78 pref_content_langs = (g.lang,),
4778b17e »
2008-06-17 initial checkin
79 pref_over_18 = False,
80 pref_compress = False,
7e6cbc47 »
2013-05-29 Domain area: add new user pref to show extra info
81 pref_domain_details = False,
4778b17e »
2008-06-17 initial checkin
82 pref_organic = True,
08c431bd »
2010-05-28 * Comply with the spec on 304 errors so Chrome won't barf download.g…
83 pref_no_profanity = True,
5ef76b96 »
2010-05-03 New features:
84 pref_label_nsfw = True,
6bcef003 »
2008-08-26 1. Allow a reddit to have a cname, like www.proggit.com, that renders
85 pref_show_stylesheets = True,
4194ced6 »
2011-10-25 Add a user preference for whether to show flair.
86 pref_show_flair = True,
13e191b6 »
2012-04-17 Add a user preference for showing link flair.
87 pref_show_link_flair = True,
5ef76b96 »
2010-05-03 New features:
88 pref_mark_messages_read = True,
2869eaf8 »
2010-05-03 New features:
89 pref_threaded_messages = True,
90 pref_collapse_read_messages = False,
a402d48d »
2010-05-03 New features:
91 pref_private_feeds = True,
f397ffc1 »
2011-08-08 Add pref to load jQuery locally instead of from the Google CDN.
92 pref_local_js = False,
0ae8f2fb »
2010-07-21 21 Jul 2010 merge
93 pref_show_adbox = True,
7fff900b »
2011-02-23 February 2011 Merge
94 pref_show_sponsors = True, # sponsored links
95 pref_show_sponsorships = True,
a4537589 »
2014-04-10 Add a preference for trending on the front page
96 pref_show_trending=True,
37e2ba98 »
2010-10-18 * Combine cassandra clusters into a single one
97 pref_highlight_new_comments = True,
e0572789 »
2013-05-10 Gold Feature: "The Butler". Username monitoring in comments.
98 pref_monitor_mentions=True,
07366ac8 »
2013-08-30 Move left bar collapsing to server-side preference.
99 pref_collapse_left_bar=False,
8c9b31d2 »
2013-10-08 ServerSecondsBar can be made public.
100 pref_public_server_seconds=False,
9a4271f6 »
2010-06-15 Upgrade Instructions
101 mobile_compress = False,
102 mobile_thumbnail = True,
e6838895 »
2010-05-17 New Features
103 trusted_sponsor = False,
4778b17e »
2008-06-17 initial checkin
104 reported = 0,
105 report_made = 0,
106 report_correct = 0,
107 report_ignored = 0,
108 spammer = 0,
109 sort_options = {},
950971c6 »
2008-06-28 added last modified to profile/comment pages
110 has_subscribed = False,
0c04094a »
2008-08-28 updates to RSS feed to link to permalink page instead of goto. Also a…
111 pref_media = 'subreddit',
7ce107f2 »
2008-07-17 sharing
112 share = {},
603ec0e0 »
2012-11-21 wiki: Fix bug with wiki permissions.
113 wiki_override = None,
bf9f43cc »
2009-12-01 Messaging/commenting
114 email = "",
e87f520d »
2010-05-04 New Features:
115 email_verified = False,
bf9f43cc »
2009-12-01 Messaging/commenting
116 ignorereports = False,
0ae8f2fb »
2010-07-21 21 Jul 2010 merge
117 pref_show_promote = None,
118 gold = False,
37e2ba98 »
2010-10-18 * Combine cassandra clusters into a single one
119 gold_charter = False,
7fff900b »
2011-02-23 February 2011 Merge
120 gold_creddits = 0,
121 gold_creddit_escrow = 0,
acd485e1 »
2013-09-19 Add default for new cake system.
122 cake_expiration=None,
8dfd73b1 »
2012-07-22 Add framework for RFC-6238: Time-Based One Time Password Algorithm.
123 otp_secret=None,
65b14677 »
2012-10-16 Backend support for bans.
124 state=0,
5e249f47 »
2014-01-08 Make all moderators have a modmsgtime attribute.
125 modmsgtime=None,
4778b17e »
2008-06-17 initial checkin
126 )
c413a2e2 »
2014-03-27 Add PATCH /api/v1/me/prefs endpoint
127 _preference_attrs = tuple(k for k in _defaults.keys()
128 if k.startswith("pref_"))
129
130 def preferences(self):
131 return {pref: getattr(self, pref) for pref in self._preference_attrs}
950971c6 »
2008-06-28 added last modified to profile/comment pages
132
b06dc9b8 »
2012-09-14 Add equality implementation for Account model.
133 def __eq__(self, other):
134 if type(self) != type(other):
135 return False
136
137 return self._id == other._id
138
139 def __ne__(self, other):
140 return not self.__eq__(other)
141
e8e751ad »
2012-04-20 Don't spam ban messages for users who don't care.
142 def has_interacted_with(self, sr):
143 if not sr:
144 return False
145
146 for type in ('link', 'comment'):
147 if hasattr(self, "%s_%s_karma" % (sr.name, type)):
148 return True
149
150 if sr.is_subscriber(self):
151 return True
152
153 return False
154
4778b17e »
2008-06-17 initial checkin
155 def karma(self, kind, sr = None):
156 suffix = '_' + kind + '_karma'
e87f520d »
2010-05-04 New Features:
157
4778b17e »
2008-06-17 initial checkin
158 #if no sr, return the sum
159 if sr is None:
160 total = 0
161 for k, v in self._t.iteritems():
162 if k.endswith(suffix):
163 total += v
164 return total
165 else:
166 try:
167 return getattr(self, sr.name + suffix)
168 except AttributeError:
169 #if positive karma elsewhere, you get min_up_karma
170 if self.karma(kind) > 0:
171 return g.MIN_UP_KARMA
172 else:
173 return 0
174
175 def incr_karma(self, kind, sr, amt):
37e2ba98 »
2010-10-18 * Combine cassandra clusters into a single one
176 if sr.name.startswith('_'):
177 g.log.info("Ignoring karma increase for subreddit %r" % (sr.name,))
178 return
179
4778b17e »
2008-06-17 initial checkin
180 prop = '%s_%s_karma' % (sr.name, kind)
181 if hasattr(self, prop):
182 return self._incr(prop, amt)
183 else:
184 default_val = self.karma(kind, sr)
185 setattr(self, prop, default_val + amt)
186 self._commit()
187
188 @property
189 def link_karma(self):
190 return self.karma('link')
191
192 @property
193 def comment_karma(self):
194 return self.karma('comment')
195
196 @property
197 def safe_karma(self):
198 karma = self.link_karma
199 return max(karma, 1) if karma > -1000 else karma
200
a635c9cf »
2014-04-03 karma: Add karma list api endpoint.
201 def all_karmas(self, include_old=True):
88d191ee »
2011-12-24 Gold feature: personal karma breakdown by subreddit.
202 """returns a list of tuples in the form (name, hover-text, link_karma,
4778b17e »
2008-06-17 initial checkin
203 comment_karma)"""
204 link_suffix = '_link_karma'
205 comment_suffix = '_comment_karma'
206 karmas = []
207 sr_names = set()
208 for k in self._t.keys():
209 if k.endswith(link_suffix):
210 sr_names.add(k[:-len(link_suffix)])
211 elif k.endswith(comment_suffix):
212 sr_names.add(k[:-len(comment_suffix)])
213 for sr_name in sr_names:
88d191ee »
2011-12-24 Gold feature: personal karma breakdown by subreddit.
214 karmas.append((sr_name, None,
4778b17e »
2008-06-17 initial checkin
215 self._t.get(sr_name + link_suffix, 0),
216 self._t.get(sr_name + comment_suffix, 0)))
9c5dea3a »
2009-05-11 Added more_karmas link, changed karmas sort order, plus some minor cl…
217
903a9e77 »
2013-01-24 Move negative karma to bottom of per-subreddit karma list
218 karmas.sort(key = lambda x: x[2] + x[3], reverse=True)
4778b17e »
2008-06-17 initial checkin
219
88d191ee »
2011-12-24 Gold feature: personal karma breakdown by subreddit.
220 old_link_karma = self._t.get('link_karma', 0)
221 old_comment_karma = self._t.get('comment_karma', 0)
a635c9cf »
2014-04-03 karma: Add karma list api endpoint.
222 if include_old and (old_link_karma or old_comment_karma):
88d191ee »
2011-12-24 Gold feature: personal karma breakdown by subreddit.
223 karmas.append((_('ancient history'),
224 _('really obscure karma from before it was cool to track per-subreddit'),
225 old_link_karma, old_comment_karma))
4778b17e »
2008-06-17 initial checkin
226
227 return karmas
bf9f43cc »
2009-12-01 Messaging/commenting
228
229 def update_last_visit(self, current_time):
230 from admintools import apply_updates
231
232 apply_updates(self)
233
14cb34c4 »
2012-06-06 LastModified: cut reads for Last Visit over to new schema.
234 prev_visit = LastModified.get(self._fullname, "Visit")
235 if prev_visit and current_time - prev_visit < timedelta(days=1):
bf9f43cc »
2009-12-01 Messaging/commenting
236 return
237
9a4271f6 »
2010-06-15 Upgrade Instructions
238 g.log.debug ("Updating last visit for %s from %s to %s" %
239 (self.name, prev_visit, current_time))
bf9f43cc »
2009-12-01 Messaging/commenting
240
555c02ad »
2012-05-24 Dual-write last visit timestamp.
241 LastModified.touch(self._fullname, "Visit")
242
b71d8bf7 »
2012-08-10 Store a last_visit time on the Account thing.
243 self.last_visit = int(time.time())
244 self._commit()
245
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
246 def make_cookie(self, timestr=None):
4778b17e »
2008-06-17 initial checkin
247 if not self._loaded:
248 self._load()
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
249 timestr = timestr or time.strftime(COOKIE_TIMESTAMP_FORMAT)
4778b17e »
2008-06-17 initial checkin
250 id_time = str(self._id) + ',' + timestr
33660836 »
2013-11-15 Create a vault for secret tokens and move some into it.
251 to_hash = ','.join((id_time, self.password, g.secrets["SECRET"]))
c9c65dd3 »
2012-06-15 Replace references to deprecated sha module with hashlib.
252 return id_time + ',' + hashlib.sha1(to_hash).hexdigest()
4778b17e »
2008-06-17 initial checkin
253
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
254 def make_admin_cookie(self, first_login=None, last_request=None):
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
255 if not self._loaded:
256 self._load()
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
257 first_login = first_login or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)
258 last_request = last_request or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)
259 hashable = ','.join((first_login, last_request, request.ip, request.user_agent, self.password))
33660836 »
2013-11-15 Create a vault for secret tokens and move some into it.
260 mac = hmac.new(g.secrets["SECRET"], hashable, hashlib.sha1).hexdigest()
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
261 return ','.join((first_login, last_request, mac))
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
262
8dfd73b1 »
2012-07-22 Add framework for RFC-6238: Time-Based One Time Password Algorithm.
263 def make_otp_cookie(self, timestamp=None):
264 if not self._loaded:
265 self._load()
266
267 timestamp = timestamp or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)
268 secrets = [request.user_agent, self.otp_secret, self.password]
33660836 »
2013-11-15 Create a vault for secret tokens and move some into it.
269 signature = hmac.new(g.secrets["SECRET"], ','.join([timestamp] + secrets), hashlib.sha1).hexdigest()
8dfd73b1 »
2012-07-22 Add framework for RFC-6238: Time-Based One Time Password Algorithm.
270
271 return ",".join((timestamp, signature))
272
4778b17e »
2008-06-17 initial checkin
273 def needs_captcha(self):
7e014097 »
2011-12-06 Make disable_captcha work properly with unlogged users.
274 return not g.disable_captcha and self.link_karma < 1
4778b17e »
2008-06-17 initial checkin
275
276 def modhash(self, rand=None, test=False):
d6848c8d »
2014-04-14 [OAuth2] Don't send unnecessary modhash to OAuth clients
277 if c.oauth_user:
278 # OAuth clients should never receive a modhash of any kind
279 # as they could use it in a CSRF attack to bypass their
280 # permitted OAuth scopes.
281 return None
4778b17e »
2008-06-17 initial checkin
282 return modhash(self, rand = rand, test = test)
283
284 def valid_hash(self, hash):
deff9405 »
2012-03-14 Add OAuth2 handling to the main APIController.
285 if self == c.oauth_user:
286 # OAuth authenticated requests do not require CSRF protection.
287 return True
288 else:
289 return valid_hash(self, hash)
4778b17e »
2008-06-17 initial checkin
290
291 @classmethod
292 @memoize('account._by_name')
293 def _by_name_cache(cls, name, allow_deleted = False):
294 #relower name here, just in case
295 deleted = (True, False) if allow_deleted else False
296 q = cls._query(lower(Account.c.name) == name.lower(),
297 Account.c._spam == (True, False),
298 Account.c._deleted == deleted)
299
300 q._limit = 1
301 l = list(q)
302 if l:
303 return l[0]._id
304
305 @classmethod
e35b5196 »
2009-03-04 * removed references to clear_memo
306 def _by_name(cls, name, allow_deleted = False, _update = False):
4778b17e »
2008-06-17 initial checkin
307 #lower name here so there is only one cache
e35b5196 »
2009-03-04 * removed references to clear_memo
308 uid = cls._by_name_cache(name.lower(), allow_deleted, _update = _update)
4778b17e »
2008-06-17 initial checkin
309 if uid:
310 return cls._byID(uid, True)
311 else:
312 raise NotFound, 'Account %s' % name
313
bf9f43cc »
2009-12-01 Messaging/commenting
314 # Admins only, since it's not memoized
315 @classmethod
316 def _by_name_multiple(cls, name):
317 q = cls._query(lower(Account.c.name) == name.lower(),
318 Account.c._spam == (True, False),
319 Account.c._deleted == (True, False))
320 return list(q)
321
4778b17e »
2008-06-17 initial checkin
322 @property
323 def friends(self):
324 return self.friend_ids()
325
4d7a2fa4 »
2011-06-24 Allow users to block users that harass them
326 @property
327 def enemies(self):
328 return self.enemy_ids()
329
5e249f47 »
2014-01-08 Make all moderators have a modmsgtime attribute.
330 @property
331 def is_moderator_somewhere(self):
332 # modmsgtime can be:
333 # - a date: the user is a mod somewhere and has unread modmail
334 # - False: the user is a mod somewhere and has no unread modmail
335 # - None: (the default) the user is not a mod anywhere
336 return self.modmsgtime is not None
337
0ae8f2fb »
2010-07-21 21 Jul 2010 merge
338 # Used on the goldmember version of /prefs/friends
339 @memoize('account.friend_rels')
340 def friend_rels_cache(self):
341 q = Friend._query(Friend.c._thing1_id == self._id,
342 Friend.c._name == 'friend')
343 return list(f._id for f in q)
344
345 def friend_rels(self, _update = False):
346 rel_ids = self.friend_rels_cache(_update=_update)
37e2ba98 »
2010-10-18 * Combine cassandra clusters into a single one
347 try:
348 rels = Friend._byID_rel(rel_ids, return_dict=False,
349 eager_load = True, data = True,
350 thing_data = True)
351 rels = list(rels)
352 except NotFound:
353 if _update:
354 raise
355 else:
356 log_text("friend-rels-bandaid 1",
357 "Had to recalc friend_rels (1) for %s" % self.name,
358 "warning")
359 return self.friend_rels(_update=True)
360
361 if not _update:
362 sorted_1 = sorted([r._thing2_id for r in rels])
363 sorted_2 = sorted(list(self.friends))
364 if sorted_1 != sorted_2:
365 g.log.error("FR1: %r" % sorted_1)
366 g.log.error("FR2: %r" % sorted_2)
367 log_text("friend-rels-bandaid 2",
368 "Had to recalc friend_rels (2) for %s" % self.name,
369 "warning")
370 self.friend_ids(_update=True)
371 return self.friend_rels(_update=True)
0ae8f2fb »
2010-07-21 21 Jul 2010 merge
372 return dict((r._thing2_id, r) for r in rels)
373
374 def add_friend_note(self, friend, note):
375 rels = self.friend_rels()
376 rel = rels[friend._id]
377 rel.note = note
378 rel._commit()
379
ea1fb6f9 »
2011-11-04 Redesign the account deletion page.
380 def delete(self, delete_message=None):
381 self.delete_message = delete_message
f1db8eff »
2013-04-26 API: Record account deletion time
382 self.delete_time = datetime.now(g.tz)
4778b17e »
2008-06-17 initial checkin
383 self._deleted = True
384 self._commit()
e35b5196 »
2009-03-04 * removed references to clear_memo
385
386 #update caches
387 Account._by_name(self.name, allow_deleted = True, _update = True)
388 #we need to catch an exception here since it will have been
389 #recently deleted
390 try:
391 Account._by_name(self.name, _update = True)
392 except NotFound:
393 pass
4778b17e »
2008-06-17 initial checkin
394
395 #remove from friends lists
396 q = Friend._query(Friend.c._thing2_id == self._id,
397 Friend.c._name == 'friend',
398 eager_load = True)
399 for f in q:
400 f._thing1.remove_friend(f._thing2)
401
4d7a2fa4 »
2011-06-24 Allow users to block users that harass them
402 q = Friend._query(Friend.c._thing2_id == self._id,
403 Friend.c._name == 'enemy',
404 eager_load=True)
405 for f in q:
406 f._thing1.remove_enemy(f._thing2)
407
d2ccc407 »
2014-02-10 Automatically delete password hashes of deleted accounts.
408 # wipe out stored password data after a recovery period
409 TryLater.schedule("account_deletion", self._id36,
410 delay=timedelta(days=90))
411
57655724 »
2012-03-14 Tweaks to OAuth2 models.
412 # Remove OAuth2Client developer permissions. This will delete any
413 # clients for which this account is the sole developer.
d4d22145 »
2012-08-22 Fix ImportError in Account.delete (oauth2 -> token).
414 from r2.models.token import OAuth2Client
57655724 »
2012-03-14 Tweaks to OAuth2 models.
415 for client in OAuth2Client._by_developer(self):
416 client.remove_developer(self)
417
65b14677 »
2012-10-16 Backend support for bans.
418 # 'State' bitfield properties
419 @property
420 def _banned(self):
421 return self.state & 1
422
423 @_banned.setter
424 def _banned(self, value):
425 if value and not self._banned:
426 self.state |= 1
427 # Invalidate all cookies by changing the password
428 # First back up the password so we can reverse this
429 self.backup_password = self.password
430 # New PW doesn't matter, they can't log in with it anyway.
431 # Even if their PW /was/ 'banned' for some reason, this
432 # will change the salt and thus invalidate the cookies
433 change_password(self, 'banned')
434
435 # deauthorize all access tokens
436 from r2.models.token import OAuth2AccessToken
437 from r2.models.token import OAuth2RefreshToken
438
439 OAuth2AccessToken.revoke_all_by_user(self)
440 OAuth2RefreshToken.revoke_all_by_user(self)
441 elif not value and self._banned:
442 self.state &= ~1
443
444 # Undo the password thing so they can log in
445 self.password = self.backup_password
446
447 # They're on their own for OAuth tokens, though.
448
449 self._commit()
450
4778b17e »
2008-06-17 initial checkin
451 @property
452 def subreddits(self):
453 from subreddit import Subreddit
454 return Subreddit.user_subreddits(self)
455
7ce107f2 »
2008-07-17 sharing
456 def recent_share_emails(self):
457 return self.share.get('recent', set([]))
458
459 def add_share_emails(self, emails):
460 if not emails:
461 return
462
463 if not isinstance(emails, set):
464 emails = set(emails)
465
466 self.share.setdefault('emails', {})
467 share = self.share.copy()
468
469 share_emails = share['emails']
470 for e in emails:
471 share_emails[e] = share_emails.get(e, 0) +1
2869eaf8 »
2010-05-03 New features:
472
7ce107f2 »
2008-07-17 sharing
473 share['recent'] = emails
474
475 self.share = share
bf9f43cc »
2009-12-01 Messaging/commenting
476
37350339 »
2011-09-20 Add support for special user distingushes.
477 def special_distinguish(self):
478 if self._t.get("special_distinguish_name"):
479 return dict((k, self._t.get("special_distinguish_"+k, None))
480 for k in ("name", "kind", "symbol", "cssclass", "label", "link"))
481 else:
482 return None
483
e87f520d »
2010-05-04 New Features:
484 def quota_key(self, kind):
485 return "user_%s_quotas-%s" % (kind, self.name)
486
487 def clog_quota(self, kind, item):
488 key = self.quota_key(kind)
489 fnames = g.hardcache.get(key, [])
490 fnames.append(item._fullname)
491 g.hardcache.set(key, fnames, 86400 * 30)
492
493 def quota_baskets(self, kind):
494 from r2.models.admintools import filter_quotas
495 key = self.quota_key(kind)
496 fnames = g.hardcache.get(key)
497
498 if not fnames:
499 return None
500
501 unfiltered = Thing._by_fullname(fnames, data=True, return_dict=False)
502
503 baskets, new_quotas = filter_quotas(unfiltered)
504
505 if new_quotas is None:
506 pass
507 elif new_quotas == []:
508 g.hardcache.delete(key)
509 else:
510 g.hardcache.set(key, new_quotas, 86400 * 30)
511
512 return baskets
513
1d9b9fe7 »
2010-05-21 * transparency updates to some of the pngs thanks to ytknows
514 # Needs to take the *canonicalized* version of each email
515 # When true, returns the reason
516 @classmethod
517 def which_emails_are_banned(cls, canons):
34377138 »
2013-09-18 Email: Use non-hardcache ban fetching system
518 banned = hooks.get_hook('email.get_banned').call(canons=canons)
1d9b9fe7 »
2010-05-21 * transparency updates to some of the pngs thanks to ytknows
519
34377138 »
2013-09-18 Email: Use non-hardcache ban fetching system
520 # Create a dictionary like:
1d9b9fe7 »
2010-05-21 * transparency updates to some of the pngs thanks to ytknows
521 # d["abc.def.com"] = [ "bob@abc.def.com", "sue@abc.def.com" ]
522 rv = {}
523 canons_by_domain = {}
34377138 »
2013-09-18 Email: Use non-hardcache ban fetching system
524
525 # email.get_banned will return a list of lists (one layer from the
526 # hooks system, the second from the function itself); chain them
527 # together for easy processing
528 for canon in itertools.chain(*banned):
1d9b9fe7 »
2010-05-21 * transparency updates to some of the pngs thanks to ytknows
529 rv[canon] = None
530
531 at_sign = canon.find("@")
532 domain = canon[at_sign+1:]
533 canons_by_domain.setdefault(domain, [])
534 canons_by_domain[domain].append(canon)
535
48dfdfc3 »
2013-07-19 Domainban: Stop depending on zookeeper
536 # Hand off to the domain ban system; it knows in the case of
537 # abc@foo.bar.com to check foo.bar.com, bar.com, and .com
538 from r2.models.admintools import bans_for_domain_parts
539
1d9b9fe7 »
2010-05-21 * transparency updates to some of the pngs thanks to ytknows
540 for domain, canons in canons_by_domain.iteritems():
48dfdfc3 »
2013-07-19 Domainban: Stop depending on zookeeper
541 for d in bans_for_domain_parts(domain):
542 if d.no_email:
1d9b9fe7 »
2010-05-21 * transparency updates to some of the pngs thanks to ytknows
543 rv[canon] = "domain"
544
545 return rv
546
547 def has_banned_email(self):
548 canon = self.canonical_email()
549 which = self.which_emails_are_banned((canon,))
550 return which.get(canon, None)
551
d251ba75 »
2010-05-20 * Improvements to the email verification system
552 def canonical_email(self):
76594cde »
2012-12-21 Refactor and reorganize email canonicalization; add some tests.
553 return canonicalize_email(self.email)
d251ba75 »
2010-05-20 * Improvements to the email verification system
554
555 def cromulent(self):
556 """Return whether the user has validated their email address and
557 passes some rudimentary 'not evil' checks."""
558
559 if not self.email_verified:
560 return False
561
1d9b9fe7 »
2010-05-21 * transparency updates to some of the pngs thanks to ytknows
562 if self.has_banned_email():
d251ba75 »
2010-05-20 * Improvements to the email verification system
563 return False
564
565 # Otherwise, congratulations; you're cromulent!
566 return True
567
e87f520d »
2010-05-04 New Features:
568 def quota_limits(self, kind):
569 if kind != 'link':
570 raise NotImplementedError
571
d251ba75 »
2010-05-20 * Improvements to the email verification system
572 if self.cromulent():
e87f520d »
2010-05-04 New Features:
573 return dict(hour=3, day=10, week=50, month=150)
574 else:
575 return dict(hour=1, day=3, week=5, month=5)
576
577 def quota_full(self, kind):
578 limits = self.quota_limits(kind)
579 baskets = self.quota_baskets(kind)
580
581 if baskets is None:
582 return None
583
584 total = 0
585 filled_quota = None
586 for key in ('hour', 'day', 'week', 'month'):
587 total += len(baskets[key])
588 if total >= limits[key]:
589 filled_quota = key
590
591 return filled_quota
592
2869eaf8 »
2010-05-03 New features:
593 @classmethod
e87f520d »
2010-05-04 New Features:
594 def system_user(cls):
595 try:
596 return cls._by_name(g.system_user)
52da3221 »
2010-06-25 Bugfixes:
597 except (NotFound, AttributeError):
e87f520d »
2010-05-04 New Features:
598 return None
a402d48d »
2010-05-03 New features:
599
46317514 »
2011-07-06 User option to block flair, and layout fixes.
600 def flair_enabled_in_sr(self, sr_id):
30c91347 »
2013-03-09 Tweak flair preference lookup to not break on multi ids.
601 return getattr(self, 'flair_%s_enabled' % sr_id, True)
46317514 »
2011-07-06 User option to block flair, and layout fixes.
602
7e225282 »
2012-08-12 Add Account method to update AccountActivityBySR.
603 def update_sr_activity(self, sr):
604 if not self._spam:
1520ddf0 »
2012-08-13 Rename AccountActivityBySR to AccountsActiveBySR.
605 AccountsActiveBySR.touch(self, sr)
7e225282 »
2012-08-12 Add Account method to update AccountActivityBySR.
606
fc9abd1d »
2013-03-08 Award-claiming via one-time links.
607 def get_trophy_id(self, uid):
608 '''Return the ID of the Trophy associated with the given "uid"
609
610 `uid` - The unique identifier for the Trophy to look up
611
612 '''
613 return getattr(self, 'received_trophy_%s' % uid, None)
614
615 def set_trophy_id(self, uid, trophy_id):
616 '''Recored that a user has received a Trophy with "uid"
617
618 `uid` - The trophy "type" that the user should only have one of
619 `trophy_id` - The ID of the corresponding Trophy object
620
621 '''
622 return setattr(self, 'received_trophy_%s' % uid, trophy_id)
623
75da6170 »
2013-08-08 account: Add employee property.
624 @property
625 def employee(self):
626 """Return if the user is an employee.
627
628 Being an employee grants them various special privileges.
629
630 """
dd925f4a »
2013-08-09 account: Protect employees against logged out users.
631 return (hasattr(self, 'name') and
632 (self.name in g.admins or
633 self.name in g.sponsors or
634 self.name in g.employees))
75da6170 »
2013-08-08 account: Add employee property.
635
e3945794 »
2013-09-18 Per user CPM overrides.
636 @property
637 def cpm_selfserve_pennies(self):
5f063a09 »
2014-03-24 Make it possible to unset Account.cpm_selfserve_pennies_override.
638 override_price = getattr(self, 'cpm_selfserve_pennies_override', None)
639 if override_price is not None:
640 return override_price
641 else:
642 return g.cpm_selfserve.pennies
e3945794 »
2013-09-18 Per user CPM overrides.
643
19d99684 »
2013-10-24 Don't PM users with gold subscription about expiration.
644 @property
645 def has_gold_subscription(self):
210dd1b3 »
2013-11-16 Store stripe customer id in Account.gold_subscr_id.
646 return bool(getattr(self, 'gold_subscr_id', None))
647
648 @property
649 def has_paypal_subscription(self):
650 return (self.has_gold_subscription and
651 not self.gold_subscr_id.startswith('cus_'))
652
653 @property
654 def has_stripe_subscription(self):
655 return (self.has_gold_subscription and
656 self.gold_subscr_id.startswith('cus_'))
19d99684 »
2013-10-24 Don't PM users with gold subscription about expiration.
657
e3945794 »
2013-09-18 Per user CPM overrides.
658
4778b17e »
2008-06-17 initial checkin
659 class FakeAccount(Account):
660 _nodb = True
5ef76b96 »
2010-05-03 New features:
661 pref_no_profanity = True
4778b17e »
2008-06-17 initial checkin
662
b06dc9b8 »
2012-09-14 Add equality implementation for Account model.
663 def __eq__(self, other):
664 return self is other
4778b17e »
2008-06-17 initial checkin
665
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
666 def valid_admin_cookie(cookie):
667 if g.read_only_mode:
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
668 return (False, None)
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
669
670 # parse the cookie
671 try:
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
672 first_login, last_request, hash = cookie.split(',')
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
673 except ValueError:
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
674 return (False, None)
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
675
676 # make sure it's a recent cookie
677 try:
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
678 first_login_time = datetime.strptime(first_login, COOKIE_TIMESTAMP_FORMAT)
679 last_request_time = datetime.strptime(last_request, COOKIE_TIMESTAMP_FORMAT)
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
680 except ValueError:
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
681 return (False, None)
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
682
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
683 cookie_age = datetime.utcnow() - first_login_time
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
684 if cookie_age.total_seconds() > g.ADMIN_COOKIE_TTL:
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
685 return (False, None)
686
687 idle_time = datetime.utcnow() - last_request_time
688 if idle_time.total_seconds() > g.ADMIN_COOKIE_MAX_IDLE:
689 return (False, None)
33b15bc2 »
2012-03-12 Split the admin cookie out from the session cookie.
690
691 # validate
a42505c5 »
2012-03-14 Keep admin cookie around if actively used.
692 expected_cookie = c.user.make_admin_cookie(first_login, last_request)
693 return (constant_time_compare(cookie, expected_cookie),
694 first_login)
4778b17e »
2008-06-17 initial checkin
695
696
8dfd73b1 »
2012-07-22 Add framework for RFC-6238: Time-Based One Time Password Algorithm.
697 def valid_otp_cookie(cookie):
698 if g.read_only_mode:
699 return False
700
701 # parse the cookie
702 try:
703 remembered_at, signature = cookie.split(",")
704 except ValueError:
705 return False
706
707 # make sure it hasn't expired
708 try:
709 remembered_at_time = datetime.strptime(remembered_at, COOKIE_TIMESTAMP_FORMAT)
710 except ValueError:
711 return False
712
713 age = datetime.utcnow() - remembered_at_time
714 if age.total_seconds() > g.OTP_COOKIE_TTL:
715 return False
716
717 # validate
718 expected_cookie = c.user.make_otp_cookie(remembered_at)
719 return constant_time_compare(cookie, expected_cookie)
720
721
a402d48d »
2010-05-03 New features:
722 def valid_feed(name, feedhash, path):
723 if name and feedhash and path:
724 from r2.lib.template_helpers import add_sr
725 path = add_sr(path)
726 try:
727 user = Account._by_name(name)
728 if (user.pref_private_feeds and
83058d48 »
2011-07-11 Use constant-time string comparison for auth.
729 constant_time_compare(feedhash, make_feedhash(user, path))):
a402d48d »
2010-05-03 New features:
730 return user
731 except NotFound:
732 pass
733
734 def make_feedhash(user, path):
33660836 »
2013-11-15 Create a vault for secret tokens and move some into it.
735 return hashlib.sha1("".join([user.name, user.password,
736 g.secrets["FEEDSECRET"]])
a402d48d »
2010-05-03 New features:
737 ).hexdigest()
738
739 def make_feedurl(user, path, ext = "rss"):
740 u = UrlParser(path)
741 u.update_query(user = user.name,
742 feed = make_feedhash(user, path))
743 u.set_extension(ext)
744 return u.unparse()
745
4778b17e »
2008-06-17 initial checkin
746 def valid_login(name, password):
747 try:
748 a = Account._by_name(name)
749 except NotFound:
750 return False
751
752 if not a._loaded: a._load()
18624371 »
2013-12-18 Signup/login: Enable account spotchecks
753
754 hooks.get_hook("account.spotcheck").call(account=a)
755
65b14677 »
2012-10-16 Backend support for bans.
756 if a._banned:
757 return False
4778b17e »
2008-06-17 initial checkin
758 return valid_password(a, password)
759
760 def valid_password(a, password):
a311805c »
2011-10-20 Switch to bcrypt for password hashing.
761 # bail out early if the account or password's invalid
762 if not hasattr(a, 'name') or not hasattr(a, 'password') or not password:
763 return False
764
765 # standardize on utf-8 encoding
766 password = filters._force_utf8(password)
767
768 if a.password.startswith('$2a$'):
a44f6f4c »
2012-08-12 Upgrade passwords on log in when bcrypt work factor changed.
769 # it's bcrypt.
a311805c »
2011-10-20 Switch to bcrypt for password hashing.
770 expected_hash = bcrypt.hashpw(password, a.password)
a44f6f4c »
2012-08-12 Upgrade passwords on log in when bcrypt work factor changed.
771 if not constant_time_compare(a.password, expected_hash):
772 return False
a311805c »
2011-10-20 Switch to bcrypt for password hashing.
773
a44f6f4c »
2012-08-12 Upgrade passwords on log in when bcrypt work factor changed.
774 # if it's using the current work factor, we're done, but if it's not
775 # we'll have to rehash.
776 # the format is $2a$workfactor$salt+hash
777 work_factor = int(a.password.split("$")[2])
778 if work_factor == g.bcrypt_work_factor:
779 return a
780 else:
781 # alright, so it's not bcrypt. how old is it?
782 # if the length of the stored hash is 43 bytes, the sha-1 hash has a salt
783 # otherwise it's sha-1 with no salt.
784 salt = ''
785 if len(a.password) == 43:
786 salt = a.password[:3]
787 expected_hash = passhash(a.name, password, salt)
788
789 if not constant_time_compare(a.password, expected_hash):
790 return False
a311805c »
2011-10-20 Switch to bcrypt for password hashing.
791
792 # since we got this far, it's a valid password but in an old format
793 # let's upgrade it
794 a.password = bcrypt_password(password)
795 a._commit()
796 return a
797
798 def bcrypt_password(password):
799 salt = bcrypt.gensalt(log_rounds=g.bcrypt_work_factor)
800 return bcrypt.hashpw(password, salt)
4778b17e »
2008-06-17 initial checkin
801
802 def passhash(username, password, salt = ''):
803 if salt is True:
804 salt = randstr(3)
805 tohash = '%s%s %s' % (salt, username, password)
c9c65dd3 »
2012-06-15 Replace references to deprecated sha module with hashlib.
806 return salt + hashlib.sha1(tohash).hexdigest()
4778b17e »
2008-06-17 initial checkin
807
c8529534 »
2008-06-26 fixed recover password
808 def change_password(user, newpassword):
a311805c »
2011-10-20 Switch to bcrypt for password hashing.
809 user.password = bcrypt_password(newpassword)
c8529534 »
2008-06-26 fixed recover password
810 user._commit()
811 return True
4778b17e »
2008-06-17 initial checkin
812
813 #TODO reset the cache
b55934ab »
2012-10-25 Account: ensure all non-defaulted attributes are atomically created.
814 def register(name, password, registration_ip):
4778b17e »
2008-06-17 initial checkin
815 try:
816 a = Account._by_name(name)
817 raise AccountExists
818 except NotFound:
819 a = Account(name = name,
a311805c »
2011-10-20 Switch to bcrypt for password hashing.
820 password = bcrypt_password(password))
5ef76b96 »
2010-05-03 New features:
821 # new accounts keep the profanity filter settings until opting out
822 a.pref_no_profanity = True
b55934ab »
2012-10-25 Account: ensure all non-defaulted attributes are atomically created.
823 a.registration_ip = registration_ip
4778b17e »
2008-06-17 initial checkin
824 a._commit()
e35b5196 »
2009-03-04 * removed references to clear_memo
825
826 #clear the caches
827 Account._by_name(name, _update = True)
828 Account._by_name(name, allow_deleted = True, _update = True)
4778b17e »
2008-06-17 initial checkin
829 return a
830
831 class Friend(Relation(Account, Account)): pass
bf9f43cc »
2009-12-01 Messaging/commenting
832
4d7a2fa4 »
2011-06-24 Allow users to block users that harass them
833 Account.__bases__ += (UserRel('friend', Friend, disable_reverse_ids_fn=True),
6c100914 »
2011-07-12 Reverse lookup of blocked user
834 UserRel('enemy', Friend, disable_reverse_ids_fn=False))
4440ccfc »
2009-01-26 overhaul of JS and form handling code, not based on jQuery
835
836 class DeletedUser(FakeAccount):
837 @property
838 def name(self):
839 return '[deleted]'
840
a402d48d »
2010-05-03 New features:
841 @property
842 def _deleted(self):
843 return True
844
4440ccfc »
2009-01-26 overhaul of JS and form handling code, not based on jQuery
845 def _fullname(self):
846 raise NotImplementedError
847
848 def _id(self):
849 raise NotImplementedError
a402d48d »
2010-05-03 New features:
850
851 def __setattr__(self, attr, val):
852 if attr == '_deleted':
853 pass
854 else:
855 object.__setattr__(self, attr, val)
2fe83ed7 »
2012-08-12 Add AccountActivityBySR.
856
1520ddf0 »
2012-08-13 Rename AccountActivityBySR to AccountsActiveBySR.
857 class AccountsActiveBySR(tdb_cassandra.View):
2fe83ed7 »
2012-08-12 Add AccountActivityBySR.
858 _use_db = True
859 _connection_pool = 'main'
6bfa5523 »
2012-09-25 tdb_cassandra: Magic up the _ttl attribute for ThingMeta users
860 _ttl = timedelta(minutes=15)
2fe83ed7 »
2012-08-12 Add AccountActivityBySR.
861
862 _extra_schema_creation_args = dict(key_validation_class=ASCII_TYPE)
863
864 _read_consistency_level = tdb_cassandra.CL.ONE
865 _write_consistency_level = tdb_cassandra.CL.ANY
866
867 @classmethod
868 def touch(cls, account, sr):
869 cls._set_values(sr._id36,
870 {account._id36: ''})
871
872 @classmethod
49607b72 »
2012-08-16 Add a memoized method of get_count, and use it by default.
873 def get_count(cls, sr, cached=True):
874 return cls.get_count_cached(sr._id36, _update=not cached)
875
876 @classmethod
877 @memoize('accounts_active', time=60)
878 def get_count_cached(cls, sr_id):
879 return cls._cf.get_count(sr_id)
d2ccc407 »
2014-02-10 Automatically delete password hashes of deleted accounts.
880
881
882 @trylater_hooks.on("trylater.account_deletion")
883 def on_account_deletion(mature_items):
884 for account_id36 in mature_items.itervalues():
885 account = Account._byID36(account_id36, data=True)
886
887 if not account._deleted:
888 continue
889
890 account.password = ""
891 account._commit()
Something went wrong with that request. Please try again.