Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 1081 lines (881 sloc) 37.166 kb
df971c3 fix the licenses so they stop polluting all the other diffs
spez authored
1 # The contents of this file are subject to the Common Public Attribution
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
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.
2869eaf @ketralnis New features:
ketralnis authored
9 #
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
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.
2869eaf @ketralnis New features:
ketralnis authored
13 #
914b949 @spladug Update / add license headers.
spladug authored
14 # The Original Code is reddit.
2869eaf @ketralnis New features:
ketralnis authored
15 #
914b949 @spladug Update / add license headers.
spladug authored
16 # The Original Developer is the Initial Developer. The Initial Developer of
17 # the Original Code is reddit Inc.
2869eaf @ketralnis New features:
ketralnis authored
18 #
af09fa8 @spladug Update license headers to 2015.
spladug authored
19 # All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
914b949 @spladug Update / add license headers.
spladug authored
20 # Inc. All Rights Reserved.
21 ###############################################################################
22
25a890d @JordanMilne Replace `disable_require_employee_https` with a feature flag
JordanMilne authored
23 from r2.config import feature
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
24 from r2.lib.db.thing import Thing, Relation, NotFound
25 from r2.lib.db.operators import lower
26 from r2.lib.db.userrel import UserRel
2fe83ed @alienth Add AccountActivityBySR.
alienth authored
27 from r2.lib.db import tdb_cassandra
e35b519 * removed references to clear_memo
spez authored
28 from r2.lib.memoize import memoize
8a5f685 @chromakode Move private-shadowed utils into separate module.
chromakode authored
29 from r2.lib.admin_utils import modhash, valid_hash
30 from r2.lib.utils import randstr, timefromnow
2243af7 @spladug Clean out dead last-modified rel code.
spladug authored
31 from r2.lib.utils import UrlParser
76594cd @spladug Refactor and reorganize email canonicalization; add some tests.
spladug authored
32 from r2.lib.utils import constant_time_compare, canonicalize_email
0bceab8 @JordanMilne Add queue to scrub deleted accounts of potential leaks
JordanMilne authored
33 from r2.lib import amqp, filters, hooks
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
34 from r2.lib.log import log_text
555c02a @spladug Dual-write last visit timestamp.
spladug authored
35 from r2.models.last_modified import LastModified
b24ba98 @Deimos Refactor flair API endpoints
Deimos authored
36 from r2.models.modaction import ModAction
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
37 from r2.models.trylater import TryLater
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
38
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
39 from pylons import c, g, request
88d191e @spladug Gold feature: personal karma breakdown by subreddit.
spladug authored
40 from pylons.i18n import _
c9c65dd @spladug Replace references to deprecated sha module with hashlib.
spladug authored
41 import time
42 import hashlib
b8d9981 @Deimos Refactor ProfileBar and Account.all_karmas
Deimos authored
43 from collections import Counter, OrderedDict
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
44 from copy import copy
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
45 from datetime import datetime, timedelta
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
46 import bcrypt
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
47 import hmac
48 import hashlib
3437713 Email: Use non-hardcache ban fetching system
Roger Ostrander authored
49 import itertools
2fe83ed @alienth Add AccountActivityBySR.
alienth authored
50 from pycassa.system_manager import ASCII_TYPE
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
51
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
52
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
53 trylater_hooks = hooks.HookRegistrar()
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
54 COOKIE_TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S'
55
56
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
57 class AccountExists(Exception): pass
58
59 class Account(Thing):
60 _data_int_props = Thing._data_int_props + ('link_karma', 'comment_karma',
61 'report_made', 'report_correct',
62 'report_ignored', 'spammer',
09c98d6 @umbrae Inbox counts: Add dark unread counts badge, start writing to inbox_count
umbrae authored
63 'reported', 'gold_creddits',
64 'inbox_count',
b76322f @bsimpson63 Add alerts for suspicious selfserve advertising payments.
bsimpson63 authored
65 'num_payment_methods',
66 'num_failed_payments',
7d91d02 @MelissaCole Add num_gildings to keep track of gildings count
MelissaCole authored
67 'num_gildings',
35e3997 @weffey Takedown tool
weffey authored
68 'admin_takedown_strikes',
09c98d6 @umbrae Inbox counts: Add dark unread counts badge, start writing to inbox_count
umbrae authored
69 )
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
70 _int_prop_suffix = '_karma'
9a4271f @KeyserSosa Upgrade Instructions
KeyserSosa authored
71 _essentials = ('name', )
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
72 _defaults = dict(pref_numsites = 25,
73 pref_frame = False,
e7eb9e6 comments panel off by default
spez authored
74 pref_frame_commentspanel = False,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
75 pref_newwindow = False,
ae9eaf5 add preference to remove recently clicked widget
spez authored
76 pref_clickgadget = 5,
ff002a4 @ketralnis Add backend for new gold feature: "remember my visits".
ketralnis authored
77 pref_store_visits = False,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
78 pref_public_votes = False,
1b030c3 @raugturi Added option to hide user profile from robots.
raugturi authored
79 pref_hide_from_robots = False,
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
80 pref_research = False,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
81 pref_hide_ups = False,
4440ccf @KeyserSosa overhaul of JS and form handling code, not based on jQuery
KeyserSosa authored
82 pref_hide_downs = False,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
83 pref_min_link_score = -4,
84 pref_min_comment_score = -4,
85 pref_num_comments = g.num_comments,
c47c697 @umbrae Add optional controversial indicator to comment scores
umbrae authored
86 pref_highlight_controversial=False,
52d419f @xiongchiamiov Comment sort preference: dual-write to migrate
xiongchiamiov authored
87 pref_default_comment_sort = None,
0745f5b @KeyserSosa make 1/2 of the default reddits English reddits if the user hasn't se…
KeyserSosa authored
88 pref_lang = g.lang,
89 pref_content_langs = (g.lang,),
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
90 pref_over_18 = False,
91 pref_compress = False,
7e6cbc4 @Deimos Domain area: add new user pref to show extra info
Deimos authored
92 pref_domain_details = False,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
93 pref_organic = True,
08c431b @KeyserSosa * Comply with the spec on 304 errors so Chrome won't barf download.g…
KeyserSosa authored
94 pref_no_profanity = True,
5ef76b9 @KeyserSosa New features:
KeyserSosa authored
95 pref_label_nsfw = True,
6bcef00 @ketralnis 1. Allow a reddit to have a cname, like www.proggit.com, that renders
ketralnis authored
96 pref_show_stylesheets = True,
49912e6 @MelissaCole Pull reddit themes in pref from wiki
MelissaCole authored
97 pref_enable_default_themes=False,
98 pref_default_theme_sr=None,
4194ced Add a user preference for whether to show flair.
Logan Hanks authored
99 pref_show_flair = True,
13e191b Add a user preference for showing link flair.
Logan Hanks authored
100 pref_show_link_flair = True,
5ef76b9 @KeyserSosa New features:
KeyserSosa authored
101 pref_mark_messages_read = True,
2869eaf @ketralnis New features:
ketralnis authored
102 pref_threaded_messages = True,
103 pref_collapse_read_messages = False,
9c6aa71 @xiongchiamiov Add option for sending orangereds as emails
xiongchiamiov authored
104 pref_email_messages = False,
a402d48 New features:
Mike authored
105 pref_private_feeds = True,
b70556a @JordanMilne Add support for forced HTTPS with HSTS grants
JordanMilne authored
106 pref_force_https = False,
a30cc40 @MelissaCole Consolidate gold ad preferences into one
MelissaCole authored
107 pref_hide_ads = False,
a453758 @umbrae Add a preference for trending on the front page
umbrae authored
108 pref_show_trending=True,
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
109 pref_highlight_new_comments = True,
e057278 @spladug Gold Feature: "The Butler". Username monitoring in comments.
spladug authored
110 pref_monitor_mentions=True,
07366ac Move left bar collapsing to server-side preference.
Max Goodman authored
111 pref_collapse_left_bar=False,
8c9b31d @bsimpson63 ServerSecondsBar can be made public.
bsimpson63 authored
112 pref_public_server_seconds=False,
cc648c7 @umbrae Suggested sort: Add preference to ignore suggested sorts
umbrae authored
113 pref_ignore_suggested_sort=False,
03bc77b @umbrae Beta mode: Add preference and subreddit callouts
umbrae authored
114 pref_beta=False,
9a4271f @KeyserSosa Upgrade Instructions
KeyserSosa authored
115 mobile_compress = False,
116 mobile_thumbnail = True,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
117 reported = 0,
118 report_made = 0,
119 report_correct = 0,
120 report_ignored = 0,
121 spammer = 0,
122 sort_options = {},
950971c added last modified to profile/comment pages
shuffman authored
123 has_subscribed = False,
0c04094 @KeyserSosa updates to RSS feed to link to permalink page instead of goto. Also …
KeyserSosa authored
124 pref_media = 'subreddit',
603ec0e @rram wiki: Fix bug with wiki permissions.
rram authored
125 wiki_override = None,
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
126 email = "",
e87f520 @KeyserSosa New Features:
KeyserSosa authored
127 email_verified = False,
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
128 ignorereports = False,
0ae8f2f @ketralnis 21 Jul 2010 merge
ketralnis authored
129 pref_show_promote = None,
130 gold = False,
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
131 gold_charter = False,
7fff900 @ketralnis February 2011 Merge
ketralnis authored
132 gold_creddits = 0,
7d91d02 @MelissaCole Add num_gildings to keep track of gildings count
MelissaCole authored
133 num_gildings=0,
acd485e @spladug Add default for new cake system.
spladug authored
134 cake_expiration=None,
8dfd73b @spladug Add framework for RFC-6238: Time-Based One Time Password Algorithm.
spladug authored
135 otp_secret=None,
65b1467 Backend support for bans.
Roger Ostrander authored
136 state=0,
5e249f4 @spladug Make all moderators have a modmsgtime attribute.
spladug authored
137 modmsgtime=None,
09c98d6 @umbrae Inbox counts: Add dark unread counts badge, start writing to inbox_count
umbrae authored
138 inbox_count=0,
594b6bf Listingcontroller: Optionally display banned userpages
Roger Ostrander authored
139 banned_profile_visible=False,
ac83646 @bsimpson63 Add a notice when localized default subreddits are used.
bsimpson63 authored
140 pref_use_global_defaults=False,
141 pref_hide_locationbar=False,
82b870d @Deimos Gold: add preference to auto-renew with a creddit
Deimos authored
142 pref_creddit_autorenew=False,
f963cb7 @Deimos Support certain accounts never adding to sent
Deimos authored
143 update_sent_messages=True,
0055f9a @MelissaCole Can reveal identity or write a message in gildings
MelissaCole authored
144 num_payment_methods=0,
145 num_failed_payments=0,
146 pref_show_snoovatar=False,
147 gild_reveal_username=False,
dbb7935 @bsimpson63 Enable per-user global selfserve cpm override.
bsimpson63 authored
148 selfserve_cpm_override_pennies=None,
55c22f9 @MelissaCole Display gold expiration for users
MelissaCole authored
149 pref_show_gold_expiration=False,
35e3997 @weffey Takedown tool
weffey authored
150 admin_takedown_strikes=0,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
151 )
c413a2e @kemitche Add PATCH /api/v1/me/prefs endpoint
kemitche authored
152 _preference_attrs = tuple(k for k in _defaults.keys()
153 if k.startswith("pref_"))
154
155 def preferences(self):
156 return {pref: getattr(self, pref) for pref in self._preference_attrs}
950971c added last modified to profile/comment pages
shuffman authored
157
b06dc9b Add equality implementation for Account model.
Max Goodman authored
158 def __eq__(self, other):
159 if type(self) != type(other):
160 return False
161
162 return self._id == other._id
163
164 def __ne__(self, other):
165 return not self.__eq__(other)
166
e8e751a @spladug Don't spam ban messages for users who don't care.
spladug authored
167 def has_interacted_with(self, sr):
168 if not sr:
169 return False
170
171 for type in ('link', 'comment'):
172 if hasattr(self, "%s_%s_karma" % (sr.name, type)):
173 return True
174
175 if sr.is_subscriber(self):
176 return True
177
178 return False
179
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
180 def karma(self, kind, sr = None):
181 suffix = '_' + kind + '_karma'
e87f520 @KeyserSosa New Features:
KeyserSosa authored
182
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
183 #if no sr, return the sum
184 if sr is None:
185 total = 0
186 for k, v in self._t.iteritems():
187 if k.endswith(suffix):
188 total += v
189 return total
190 else:
191 try:
192 return getattr(self, sr.name + suffix)
193 except AttributeError:
194 #if positive karma elsewhere, you get min_up_karma
195 if self.karma(kind) > 0:
196 return g.MIN_UP_KARMA
197 else:
198 return 0
199
200 def incr_karma(self, kind, sr, amt):
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
201 if sr.name.startswith('_'):
202 g.log.info("Ignoring karma increase for subreddit %r" % (sr.name,))
203 return
204
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
205 prop = '%s_%s_karma' % (sr.name, kind)
206 if hasattr(self, prop):
207 return self._incr(prop, amt)
208 else:
209 default_val = self.karma(kind, sr)
210 setattr(self, prop, default_val + amt)
211 self._commit()
212
213 @property
214 def link_karma(self):
215 return self.karma('link')
216
217 @property
218 def comment_karma(self):
219 return self.karma('comment')
220
a635c9c @andre-d karma: Add karma list api endpoint.
andre-d authored
221 def all_karmas(self, include_old=True):
b8d9981 @Deimos Refactor ProfileBar and Account.all_karmas
Deimos authored
222 """Get all of the user's subreddit-specific karma totals.
223
224 Returns an OrderedDict keyed on subreddit name and containing
225 (link_karma, comment_karma) tuples, ordered by the combined total
226 descending.
227 """
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
228 link_suffix = '_link_karma'
229 comment_suffix = '_comment_karma'
b8d9981 @Deimos Refactor ProfileBar and Account.all_karmas
Deimos authored
230
231 comment_karmas = Counter()
232 link_karmas = Counter()
233 combined_karmas = Counter()
234
235 for key, value in self._t.iteritems():
236 if key.endswith(link_suffix):
237 sr_name = key[:-len(link_suffix)]
238 link_karmas[sr_name] = value
239 elif key.endswith(comment_suffix):
240 sr_name = key[:-len(comment_suffix)]
241 comment_karmas[sr_name] = value
242 else:
243 continue
244
245 combined_karmas[sr_name] += value
246
247 all_karmas = OrderedDict()
248 for sr_name, total in combined_karmas.most_common():
249 all_karmas[sr_name] = (link_karmas[sr_name],
250 comment_karmas[sr_name])
251
252 if include_old:
253 old_link_karma = self._t.get('link_karma', 0)
254 old_comment_karma = self._t.get('comment_karma', 0)
255 if old_link_karma or old_comment_karma:
256 all_karmas['ancient history'] = (old_link_karma,
257 old_comment_karma)
258
259 return all_karmas
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
260
261 def update_last_visit(self, current_time):
262 from admintools import apply_updates
263
264 apply_updates(self)
265
14cb34c @spladug LastModified: cut reads for Last Visit over to new schema.
spladug authored
266 prev_visit = LastModified.get(self._fullname, "Visit")
267 if prev_visit and current_time - prev_visit < timedelta(days=1):
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
268 return
269
9a4271f @KeyserSosa Upgrade Instructions
KeyserSosa authored
270 g.log.debug ("Updating last visit for %s from %s to %s" %
271 (self.name, prev_visit, current_time))
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
272
555c02a @spladug Dual-write last visit timestamp.
spladug authored
273 LastModified.touch(self._fullname, "Visit")
274
b71d8bf @alienth Store a last_visit time on the Account thing.
alienth authored
275 self.last_visit = int(time.time())
276 self._commit()
277
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
278 def make_cookie(self, timestr=None):
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
279 if not self._loaded:
280 self._load()
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
281 timestr = timestr or time.strftime(COOKIE_TIMESTAMP_FORMAT)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
282 id_time = str(self._id) + ',' + timestr
3366083 @spladug Create a vault for secret tokens and move some into it.
spladug authored
283 to_hash = ','.join((id_time, self.password, g.secrets["SECRET"]))
c9c65dd @spladug Replace references to deprecated sha module with hashlib.
spladug authored
284 return id_time + ',' + hashlib.sha1(to_hash).hexdigest()
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
285
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
286 def make_admin_cookie(self, first_login=None, last_request=None):
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
287 if not self._loaded:
288 self._load()
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
289 first_login = first_login or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)
290 last_request = last_request or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)
291 hashable = ','.join((first_login, last_request, request.ip, request.user_agent, self.password))
3366083 @spladug Create a vault for secret tokens and move some into it.
spladug authored
292 mac = hmac.new(g.secrets["SECRET"], hashable, hashlib.sha1).hexdigest()
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
293 return ','.join((first_login, last_request, mac))
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
294
8dfd73b @spladug Add framework for RFC-6238: Time-Based One Time Password Algorithm.
spladug authored
295 def make_otp_cookie(self, timestamp=None):
296 if not self._loaded:
297 self._load()
298
299 timestamp = timestamp or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)
300 secrets = [request.user_agent, self.otp_secret, self.password]
3366083 @spladug Create a vault for secret tokens and move some into it.
spladug authored
301 signature = hmac.new(g.secrets["SECRET"], ','.join([timestamp] + secrets), hashlib.sha1).hexdigest()
8dfd73b @spladug Add framework for RFC-6238: Time-Based One Time Password Algorithm.
spladug authored
302
303 return ",".join((timestamp, signature))
304
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
305 def needs_captcha(self):
9c8be78 @Deimos Account.needs_captcha: add hook, use live_config
Deimos authored
306 if g.disable_captcha:
307 return False
308
309 hook = hooks.get_hook("account.is_captcha_exempt")
310 captcha_exempt = hook.call_until_return(account=self)
311 if captcha_exempt:
312 return False
313
314 if self.link_karma >= g.live_config["captcha_exempt_link_karma"]:
315 return False
316
317 if self.comment_karma >= g.live_config["captcha_exempt_comment_karma"]:
318 return False
319
320 return True
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
321
165ca78 @Deimos Create subreddit: add support for min age/karma
Deimos authored
322 @property
323 def can_create_subreddit(self):
324 hook = hooks.get_hook("account.can_create_subreddit")
325 can_create = hook.call_until_return(account=self)
326 if can_create is not None:
327 return can_create
328
329 min_age = timedelta(days=g.live_config["create_sr_account_age_days"])
330 if self._age < min_age:
331 return False
332
333 if (self.link_karma < g.live_config["create_sr_link_karma"] and
334 self.comment_karma < g.live_config["create_sr_comment_karma"]):
335 return False
336
337 return True
338
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
339 def modhash(self, rand=None, test=False):
d6848c8 @kemitche [OAuth2] Don't send unnecessary modhash to OAuth clients
kemitche authored
340 if c.oauth_user:
341 # OAuth clients should never receive a modhash of any kind
342 # as they could use it in a CSRF attack to bypass their
343 # permitted OAuth scopes.
344 return None
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
345 return modhash(self, rand = rand, test = test)
346
347 def valid_hash(self, hash):
deff940 Add OAuth2 handling to the main APIController.
Max Goodman authored
348 if self == c.oauth_user:
349 # OAuth authenticated requests do not require CSRF protection.
350 return True
351 else:
352 return valid_hash(self, hash)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
353
354 @classmethod
355 @memoize('account._by_name')
356 def _by_name_cache(cls, name, allow_deleted = False):
357 #relower name here, just in case
358 deleted = (True, False) if allow_deleted else False
359 q = cls._query(lower(Account.c.name) == name.lower(),
360 Account.c._spam == (True, False),
361 Account.c._deleted == deleted)
362
363 q._limit = 1
364 l = list(q)
365 if l:
366 return l[0]._id
367
368 @classmethod
e35b519 * removed references to clear_memo
spez authored
369 def _by_name(cls, name, allow_deleted = False, _update = False):
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
370 #lower name here so there is only one cache
e35b519 * removed references to clear_memo
spez authored
371 uid = cls._by_name_cache(name.lower(), allow_deleted, _update = _update)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
372 if uid:
373 return cls._byID(uid, True)
374 else:
375 raise NotFound, 'Account %s' % name
376
ee5ea8c @umbrae Inbox_counts: corrections on deletes, spams, edits
umbrae authored
377 @classmethod
378 def _names_to_ids(cls, names, ignore_missing=False, allow_deleted=False,
379 _update=False):
380 for name in names:
381 uid = cls._by_name_cache(name.lower(), allow_deleted, _update=_update)
382 if not uid:
383 if ignore_missing:
384 continue
385 raise NotFound('Account %s' % name)
386 yield uid
387
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
388 # Admins only, since it's not memoized
389 @classmethod
390 def _by_name_multiple(cls, name):
391 q = cls._query(lower(Account.c.name) == name.lower(),
392 Account.c._spam == (True, False),
393 Account.c._deleted == (True, False))
394 return list(q)
395
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
396 @property
397 def friends(self):
398 return self.friend_ids()
399
4d7a2fa @kemitche Allow users to block users that harass them
kemitche authored
400 @property
401 def enemies(self):
402 return self.enemy_ids()
403
5e249f4 @spladug Make all moderators have a modmsgtime attribute.
spladug authored
404 @property
405 def is_moderator_somewhere(self):
406 # modmsgtime can be:
407 # - a date: the user is a mod somewhere and has unread modmail
408 # - False: the user is a mod somewhere and has no unread modmail
409 # - None: (the default) the user is not a mod anywhere
410 return self.modmsgtime is not None
411
0ae8f2f @ketralnis 21 Jul 2010 merge
ketralnis authored
412 # Used on the goldmember version of /prefs/friends
413 @memoize('account.friend_rels')
414 def friend_rels_cache(self):
415 q = Friend._query(Friend.c._thing1_id == self._id,
416 Friend.c._name == 'friend')
417 return list(f._id for f in q)
418
419 def friend_rels(self, _update = False):
420 rel_ids = self.friend_rels_cache(_update=_update)
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
421 try:
422 rels = Friend._byID_rel(rel_ids, return_dict=False,
423 eager_load = True, data = True,
424 thing_data = True)
425 rels = list(rels)
426 except NotFound:
427 if _update:
428 raise
429 else:
430 log_text("friend-rels-bandaid 1",
431 "Had to recalc friend_rels (1) for %s" % self.name,
432 "warning")
433 return self.friend_rels(_update=True)
434
435 if not _update:
436 sorted_1 = sorted([r._thing2_id for r in rels])
437 sorted_2 = sorted(list(self.friends))
438 if sorted_1 != sorted_2:
439 g.log.error("FR1: %r" % sorted_1)
440 g.log.error("FR2: %r" % sorted_2)
441 log_text("friend-rels-bandaid 2",
442 "Had to recalc friend_rels (2) for %s" % self.name,
443 "warning")
444 self.friend_ids(_update=True)
445 return self.friend_rels(_update=True)
0ae8f2f @ketralnis 21 Jul 2010 merge
ketralnis authored
446 return dict((r._thing2_id, r) for r in rels)
447
448 def add_friend_note(self, friend, note):
449 rels = self.friend_rels()
450 rel = rels[friend._id]
451 rel.note = note
452 rel._commit()
453
ea1fb6f Redesign the account deletion page.
Max Goodman authored
454 def delete(self, delete_message=None):
455 self.delete_message = delete_message
f1db8ef API: Record account deletion time
Roger Ostrander authored
456 self.delete_time = datetime.now(g.tz)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
457 self._deleted = True
458 self._commit()
e35b519 * removed references to clear_memo
spez authored
459
460 #update caches
461 Account._by_name(self.name, allow_deleted = True, _update = True)
462 #we need to catch an exception here since it will have been
463 #recently deleted
464 try:
465 Account._by_name(self.name, _update = True)
466 except NotFound:
467 pass
0bceab8 @JordanMilne Add queue to scrub deleted accounts of potential leaks
JordanMilne authored
468
469 # Mark this account for scrubbing
470 amqp.add_item('account_deleted', self._fullname)
471
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
472 #remove from friends lists
473 q = Friend._query(Friend.c._thing2_id == self._id,
474 Friend.c._name == 'friend',
475 eager_load = True)
476 for f in q:
477 f._thing1.remove_friend(f._thing2)
478
4d7a2fa @kemitche Allow users to block users that harass them
kemitche authored
479 q = Friend._query(Friend.c._thing2_id == self._id,
480 Friend.c._name == 'enemy',
481 eager_load=True)
482 for f in q:
483 f._thing1.remove_enemy(f._thing2)
484
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
485 # wipe out stored password data after a recovery period
486 TryLater.schedule("account_deletion", self._id36,
487 delay=timedelta(days=90))
488
5765572 @dpifke Tweaks to OAuth2 models.
dpifke authored
489 # Remove OAuth2Client developer permissions. This will delete any
490 # clients for which this account is the sole developer.
d4d2214 Fix ImportError in Account.delete (oauth2 -> token).
Logan Hanks authored
491 from r2.models.token import OAuth2Client
5765572 @dpifke Tweaks to OAuth2 models.
dpifke authored
492 for client in OAuth2Client._by_developer(self):
493 client.remove_developer(self)
494
65b1467 Backend support for bans.
Roger Ostrander authored
495 # 'State' bitfield properties
496 @property
497 def _banned(self):
498 return self.state & 1
499
500 @_banned.setter
501 def _banned(self, value):
502 if value and not self._banned:
503 self.state |= 1
504 # Invalidate all cookies by changing the password
505 # First back up the password so we can reverse this
506 self.backup_password = self.password
507 # New PW doesn't matter, they can't log in with it anyway.
508 # Even if their PW /was/ 'banned' for some reason, this
509 # will change the salt and thus invalidate the cookies
510 change_password(self, 'banned')
511
512 # deauthorize all access tokens
513 from r2.models.token import OAuth2AccessToken
514 from r2.models.token import OAuth2RefreshToken
515
516 OAuth2AccessToken.revoke_all_by_user(self)
517 OAuth2RefreshToken.revoke_all_by_user(self)
518 elif not value and self._banned:
519 self.state &= ~1
520
521 # Undo the password thing so they can log in
522 self.password = self.backup_password
523
524 # They're on their own for OAuth tokens, though.
525
526 self._commit()
527
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
528 @property
529 def subreddits(self):
530 from subreddit import Subreddit
531 return Subreddit.user_subreddits(self)
532
3735033 Add support for special user distingushes.
Max Goodman authored
533 def special_distinguish(self):
534 if self._t.get("special_distinguish_name"):
535 return dict((k, self._t.get("special_distinguish_"+k, None))
536 for k in ("name", "kind", "symbol", "cssclass", "label", "link"))
537 else:
538 return None
539
e87f520 @KeyserSosa New Features:
KeyserSosa authored
540 def quota_key(self, kind):
541 return "user_%s_quotas-%s" % (kind, self.name)
542
543 def clog_quota(self, kind, item):
544 key = self.quota_key(kind)
545 fnames = g.hardcache.get(key, [])
546 fnames.append(item._fullname)
547 g.hardcache.set(key, fnames, 86400 * 30)
548
549 def quota_baskets(self, kind):
550 from r2.models.admintools import filter_quotas
551 key = self.quota_key(kind)
552 fnames = g.hardcache.get(key)
553
554 if not fnames:
555 return None
556
557 unfiltered = Thing._by_fullname(fnames, data=True, return_dict=False)
558
559 baskets, new_quotas = filter_quotas(unfiltered)
560
561 if new_quotas is None:
562 pass
563 elif new_quotas == []:
564 g.hardcache.delete(key)
565 else:
566 g.hardcache.set(key, new_quotas, 86400 * 30)
567
568 return baskets
569
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
570 # Needs to take the *canonicalized* version of each email
571 # When true, returns the reason
572 @classmethod
573 def which_emails_are_banned(cls, canons):
3437713 Email: Use non-hardcache ban fetching system
Roger Ostrander authored
574 banned = hooks.get_hook('email.get_banned').call(canons=canons)
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
575
3437713 Email: Use non-hardcache ban fetching system
Roger Ostrander authored
576 # Create a dictionary like:
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
577 # d["abc.def.com"] = [ "bob@abc.def.com", "sue@abc.def.com" ]
578 rv = {}
579 canons_by_domain = {}
3437713 Email: Use non-hardcache ban fetching system
Roger Ostrander authored
580
581 # email.get_banned will return a list of lists (one layer from the
582 # hooks system, the second from the function itself); chain them
583 # together for easy processing
584 for canon in itertools.chain(*banned):
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
585 rv[canon] = None
586
587 at_sign = canon.find("@")
588 domain = canon[at_sign+1:]
589 canons_by_domain.setdefault(domain, [])
590 canons_by_domain[domain].append(canon)
591
48dfdfc Domainban: Stop depending on zookeeper
Roger Ostrander authored
592 # Hand off to the domain ban system; it knows in the case of
593 # abc@foo.bar.com to check foo.bar.com, bar.com, and .com
594 from r2.models.admintools import bans_for_domain_parts
595
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
596 for domain, canons in canons_by_domain.iteritems():
48dfdfc Domainban: Stop depending on zookeeper
Roger Ostrander authored
597 for d in bans_for_domain_parts(domain):
598 if d.no_email:
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
599 rv[canon] = "domain"
600
601 return rv
602
003ca6e @bsimpson63 Add AccountsByCanonicalEmail.
bsimpson63 authored
603 def set_email(self, email):
604 old_email = self.email
605 self.email = email
606 self._commit()
607 AccountsByCanonicalEmail.update_email(self, old_email, email)
608
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
609 def has_banned_email(self):
610 canon = self.canonical_email()
611 which = self.which_emails_are_banned((canon,))
612 return which.get(canon, None)
613
d251ba7 @KeyserSosa * Improvements to the email verification system
KeyserSosa authored
614 def canonical_email(self):
76594cd @spladug Refactor and reorganize email canonicalization; add some tests.
spladug authored
615 return canonicalize_email(self.email)
d251ba7 @KeyserSosa * Improvements to the email verification system
KeyserSosa authored
616
617 def cromulent(self):
618 """Return whether the user has validated their email address and
619 passes some rudimentary 'not evil' checks."""
620
621 if not self.email_verified:
622 return False
623
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
624 if self.has_banned_email():
d251ba7 @KeyserSosa * Improvements to the email verification system
KeyserSosa authored
625 return False
626
627 # Otherwise, congratulations; you're cromulent!
628 return True
629
e87f520 @KeyserSosa New Features:
KeyserSosa authored
630 def quota_limits(self, kind):
631 if kind != 'link':
632 raise NotImplementedError
633
d251ba7 @KeyserSosa * Improvements to the email verification system
KeyserSosa authored
634 if self.cromulent():
e87f520 @KeyserSosa New Features:
KeyserSosa authored
635 return dict(hour=3, day=10, week=50, month=150)
636 else:
637 return dict(hour=1, day=3, week=5, month=5)
638
639 def quota_full(self, kind):
640 limits = self.quota_limits(kind)
641 baskets = self.quota_baskets(kind)
642
643 if baskets is None:
644 return None
645
646 total = 0
647 filled_quota = None
648 for key in ('hour', 'day', 'week', 'month'):
649 total += len(baskets[key])
650 if total >= limits[key]:
651 filled_quota = key
652
653 return filled_quota
654
2869eaf @ketralnis New features:
ketralnis authored
655 @classmethod
e87f520 @KeyserSosa New Features:
KeyserSosa authored
656 def system_user(cls):
657 try:
658 return cls._by_name(g.system_user)
52da322 @KeyserSosa Bugfixes:
KeyserSosa authored
659 except (NotFound, AttributeError):
e87f520 @KeyserSosa New Features:
KeyserSosa authored
660 return None
a402d48 New features:
Mike authored
661
7f1a728 @MelissaCole Gold: Add ability to override individual sr styles
MelissaCole authored
662 def use_subreddit_style(self, sr):
663 """Return whether to show subreddit stylesheet depending on
664 individual selection if available, else use pref_show_stylesheets"""
665 # if FakeSubreddit, there is no stylesheet
666 if not hasattr(sr, '_id'):
667 return False
668 if not feature.is_enabled('stylesheets_everywhere'):
669 return self.pref_show_stylesheets
670 # if stylesheet isn't individually enabled/disabled, use global pref
671 return bool(getattr(self, "sr_style_%s_enabled" % sr._id,
672 self.pref_show_stylesheets))
673
674 def set_subreddit_style(self, sr, use_style):
675 if hasattr(sr, '_id'):
676 setattr(self, "sr_style_%s_enabled" % sr._id, use_style)
677 self._commit()
678
4631751 User option to block flair, and layout fixes.
Logan Hanks authored
679 def flair_enabled_in_sr(self, sr_id):
30c9134 Tweak flair preference lookup to not break on multi ids.
Max Goodman authored
680 return getattr(self, 'flair_%s_enabled' % sr_id, True)
4631751 User option to block flair, and layout fixes.
Logan Hanks authored
681
bedf7e9 @Deimos Account flair methods: allow obeying pref
Deimos authored
682 def flair_text(self, sr_id, obey_disabled=False):
683 if obey_disabled and not self.flair_enabled_in_sr(sr_id):
684 return None
d6f2574 @bsimpson63 Cleanup wrapped_flair.
bsimpson63 authored
685 return getattr(self, 'flair_%s_text' % sr_id, None)
686
bedf7e9 @Deimos Account flair methods: allow obeying pref
Deimos authored
687 def flair_css_class(self, sr_id, obey_disabled=False):
688 if obey_disabled and not self.flair_enabled_in_sr(sr_id):
689 return None
d6f2574 @bsimpson63 Cleanup wrapped_flair.
bsimpson63 authored
690 return getattr(self, 'flair_%s_css_class' % sr_id, None)
691
b24ba98 @Deimos Refactor flair API endpoints
Deimos authored
692 def can_flair_in_sr(self, user, sr):
693 """Return whether a user can set this one's flair in a subreddit."""
694 can_assign_own = self._id == user._id and sr.flair_self_assign_enabled
695
696 return can_assign_own or sr.is_moderator_with_perms(user, "flair")
697
698 def set_flair(self, subreddit, text=None, css_class=None, set_by=None,
699 log_details="edit"):
700 log_details = "flair_%s" % log_details
701 if not text and not css_class:
702 # set to None instead of potentially empty strings
703 text = css_class = None
704 subreddit.remove_flair(self)
705 log_details = "flair_delete"
706 elif not subreddit.is_flair(self):
707 subreddit.add_flair(self)
708
709 setattr(self, 'flair_%s_text' % subreddit._id, text)
710 setattr(self, 'flair_%s_css_class' % subreddit._id, css_class)
711 self._commit()
712
713 if set_by and set_by != self:
714 ModAction.create(subreddit, set_by, action='editflair',
715 target=self, details=log_details)
716
7e22528 @alienth Add Account method to update AccountActivityBySR.
alienth authored
717 def update_sr_activity(self, sr):
718 if not self._spam:
1520ddf @alienth Rename AccountActivityBySR to AccountsActiveBySR.
alienth authored
719 AccountsActiveBySR.touch(self, sr)
7e22528 @alienth Add Account method to update AccountActivityBySR.
alienth authored
720
fc9abd1 @kemitche Award-claiming via one-time links.
kemitche authored
721 def get_trophy_id(self, uid):
722 '''Return the ID of the Trophy associated with the given "uid"
723
724 `uid` - The unique identifier for the Trophy to look up
725
726 '''
727 return getattr(self, 'received_trophy_%s' % uid, None)
728
729 def set_trophy_id(self, uid, trophy_id):
730 '''Recored that a user has received a Trophy with "uid"
731
732 `uid` - The trophy "type" that the user should only have one of
733 `trophy_id` - The ID of the corresponding Trophy object
734
735 '''
736 return setattr(self, 'received_trophy_%s' % uid, trophy_id)
737
75da617 @rram account: Add employee property.
rram authored
738 @property
739 def employee(self):
740 """Return if the user is an employee.
741
742 Being an employee grants them various special privileges.
743
744 """
dd925f4 @rram account: Protect employees against logged out users.
rram authored
745 return (hasattr(self, 'name') and
746 (self.name in g.admins or
747 self.name in g.sponsors or
748 self.name in g.employees))
75da617 @rram account: Add employee property.
rram authored
749
e394579 @bsimpson63 Per user CPM overrides.
bsimpson63 authored
750 @property
9e20cc1 @JordanMilne Allow forcing HTTPS upon employees
JordanMilne authored
751 def https_forced(self):
752 """Return whether this account may only be used via HTTPS."""
25a890d @JordanMilne Replace `disable_require_employee_https` with a feature flag
JordanMilne authored
753 if feature.is_enabled_for("require_https", self):
9e20cc1 @JordanMilne Allow forcing HTTPS upon employees
JordanMilne authored
754 return True
755 return self.pref_force_https
756
757 @property
8aa2312 @JordanMilne Disable toolbar when using forcing HTTPS
JordanMilne authored
758 def uses_toolbar(self):
759 return not self.https_forced and self.pref_frame
760
761 @property
19d9968 @bsimpson63 Don't PM users with gold subscription about expiration.
bsimpson63 authored
762 def has_gold_subscription(self):
210dd1b @bsimpson63 Store stripe customer id in Account.gold_subscr_id.
bsimpson63 authored
763 return bool(getattr(self, 'gold_subscr_id', None))
764
765 @property
766 def has_paypal_subscription(self):
767 return (self.has_gold_subscription and
768 not self.gold_subscr_id.startswith('cus_'))
769
770 @property
771 def has_stripe_subscription(self):
772 return (self.has_gold_subscription and
773 self.gold_subscr_id.startswith('cus_'))
19d9968 @bsimpson63 Don't PM users with gold subscription about expiration.
bsimpson63 authored
774
82b870d @Deimos Gold: add preference to auto-renew with a creddit
Deimos authored
775 @property
776 def gold_will_autorenew(self):
777 return (self.has_gold_subscription or
778 (self.pref_creddit_autorenew and self.gold_creddits > 0))
779
35e3997 @weffey Takedown tool
weffey authored
780 def incr_admin_takedown_strikes(self, amt=1):
781 return self._incr('admin_takedown_strikes', amt)
782
52d419f @xiongchiamiov Comment sort preference: dual-write to migrate
xiongchiamiov authored
783 @property
784 def default_comment_sort(self):
785 if self.pref_default_comment_sort:
786 return self.pref_default_comment_sort
787
788 old_sort_pref = self.sort_options.get('front_sort')
789 if old_sort_pref:
790 return old_sort_pref
791
792 return 'confidence'
793
e394579 @bsimpson63 Per user CPM overrides.
bsimpson63 authored
794
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
795 class FakeAccount(Account):
796 _nodb = True
5ef76b9 @KeyserSosa New features:
KeyserSosa authored
797 pref_no_profanity = True
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
798
b06dc9b Add equality implementation for Account model.
Max Goodman authored
799 def __eq__(self, other):
800 return self is other
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
801
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
802 def valid_admin_cookie(cookie):
803 if g.read_only_mode:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
804 return (False, None)
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
805
806 # parse the cookie
807 try:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
808 first_login, last_request, hash = cookie.split(',')
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
809 except ValueError:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
810 return (False, None)
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
811
812 # make sure it's a recent cookie
813 try:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
814 first_login_time = datetime.strptime(first_login, COOKIE_TIMESTAMP_FORMAT)
815 last_request_time = datetime.strptime(last_request, COOKIE_TIMESTAMP_FORMAT)
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
816 except ValueError:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
817 return (False, None)
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
818
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
819 cookie_age = datetime.utcnow() - first_login_time
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
820 if cookie_age.total_seconds() > g.ADMIN_COOKIE_TTL:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
821 return (False, None)
822
823 idle_time = datetime.utcnow() - last_request_time
824 if idle_time.total_seconds() > g.ADMIN_COOKIE_MAX_IDLE:
825 return (False, None)
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
826
827 # validate
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
828 expected_cookie = c.user.make_admin_cookie(first_login, last_request)
829 return (constant_time_compare(cookie, expected_cookie),
830 first_login)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
831
832
8dfd73b @spladug Add framework for RFC-6238: Time-Based One Time Password Algorithm.
spladug authored
833 def valid_otp_cookie(cookie):
834 if g.read_only_mode:
835 return False
836
837 # parse the cookie
838 try:
839 remembered_at, signature = cookie.split(",")
840 except ValueError:
841 return False
842
843 # make sure it hasn't expired
844 try:
845 remembered_at_time = datetime.strptime(remembered_at, COOKIE_TIMESTAMP_FORMAT)
846 except ValueError:
847 return False
848
849 age = datetime.utcnow() - remembered_at_time
850 if age.total_seconds() > g.OTP_COOKIE_TTL:
851 return False
852
853 # validate
854 expected_cookie = c.user.make_otp_cookie(remembered_at)
855 return constant_time_compare(cookie, expected_cookie)
856
857
a402d48 New features:
Mike authored
858 def valid_feed(name, feedhash, path):
859 if name and feedhash and path:
860 from r2.lib.template_helpers import add_sr
861 path = add_sr(path)
862 try:
863 user = Account._by_name(name)
864 if (user.pref_private_feeds and
83058d4 @ekarulf Use constant-time string comparison for auth.
ekarulf authored
865 constant_time_compare(feedhash, make_feedhash(user, path))):
a402d48 New features:
Mike authored
866 return user
867 except NotFound:
868 pass
869
870 def make_feedhash(user, path):
3366083 @spladug Create a vault for secret tokens and move some into it.
spladug authored
871 return hashlib.sha1("".join([user.name, user.password,
872 g.secrets["FEEDSECRET"]])
a402d48 New features:
Mike authored
873 ).hexdigest()
874
875 def make_feedurl(user, path, ext = "rss"):
876 u = UrlParser(path)
877 u.update_query(user = user.name,
878 feed = make_feedhash(user, path))
879 u.set_extension(ext)
880 return u.unparse()
881
722fbf2 Passwords: Enable checks against other passwords
Roger Ostrander authored
882 def valid_password(a, password, compare_password=None):
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
883 # bail out early if the account or password's invalid
884 if not hasattr(a, 'name') or not hasattr(a, 'password') or not password:
885 return False
886
722fbf2 Passwords: Enable checks against other passwords
Roger Ostrander authored
887 convert_password = False
888 if compare_password is None:
889 convert_password = True
890 compare_password = a.password
891
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
892 # standardize on utf-8 encoding
893 password = filters._force_utf8(password)
894
722fbf2 Passwords: Enable checks against other passwords
Roger Ostrander authored
895 if compare_password.startswith('$2a$'):
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
896 # it's bcrypt.
722fbf2 Passwords: Enable checks against other passwords
Roger Ostrander authored
897 expected_hash = bcrypt.hashpw(password, compare_password)
898 if not constant_time_compare(compare_password, expected_hash):
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
899 return False
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
900
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
901 # if it's using the current work factor, we're done, but if it's not
902 # we'll have to rehash.
903 # the format is $2a$workfactor$salt+hash
722fbf2 Passwords: Enable checks against other passwords
Roger Ostrander authored
904 work_factor = int(compare_password.split("$")[2])
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
905 if work_factor == g.bcrypt_work_factor:
906 return a
907 else:
908 # alright, so it's not bcrypt. how old is it?
909 # if the length of the stored hash is 43 bytes, the sha-1 hash has a salt
910 # otherwise it's sha-1 with no salt.
911 salt = ''
722fbf2 Passwords: Enable checks against other passwords
Roger Ostrander authored
912 if len(compare_password) == 43:
913 salt = compare_password[:3]
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
914 expected_hash = passhash(a.name, password, salt)
915
722fbf2 Passwords: Enable checks against other passwords
Roger Ostrander authored
916 if not constant_time_compare(compare_password, expected_hash):
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
917 return False
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
918
919 # since we got this far, it's a valid password but in an old format
920 # let's upgrade it
722fbf2 Passwords: Enable checks against other passwords
Roger Ostrander authored
921 if convert_password:
922 a.password = bcrypt_password(password)
923 a._commit()
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
924 return a
925
926 def bcrypt_password(password):
927 salt = bcrypt.gensalt(log_rounds=g.bcrypt_work_factor)
928 return bcrypt.hashpw(password, salt)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
929
930 def passhash(username, password, salt = ''):
931 if salt is True:
932 salt = randstr(3)
933 tohash = '%s%s %s' % (salt, username, password)
c9c65dd @spladug Replace references to deprecated sha module with hashlib.
spladug authored
934 return salt + hashlib.sha1(tohash).hexdigest()
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
935
c852953 fixed recover password
shuffman authored
936 def change_password(user, newpassword):
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
937 user.password = bcrypt_password(newpassword)
c852953 fixed recover password
shuffman authored
938 user._commit()
abf6f6e @alienth Store a LastModified update on account password change.
alienth authored
939 LastModified.touch(user._fullname, 'Password')
c852953 fixed recover password
shuffman authored
940 return True
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
941
942 #TODO reset the cache
b55934a @spladug Account: ensure all non-defaulted attributes are atomically created.
spladug authored
943 def register(name, password, registration_ip):
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
944 try:
945 a = Account._by_name(name)
946 raise AccountExists
947 except NotFound:
948 a = Account(name = name,
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
949 password = bcrypt_password(password))
5ef76b9 @KeyserSosa New features:
KeyserSosa authored
950 # new accounts keep the profanity filter settings until opting out
951 a.pref_no_profanity = True
b55934a @spladug Account: ensure all non-defaulted attributes are atomically created.
spladug authored
952 a.registration_ip = registration_ip
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
953 a._commit()
e35b519 * removed references to clear_memo
spez authored
954
955 #clear the caches
956 Account._by_name(name, _update = True)
957 Account._by_name(name, allow_deleted = True, _update = True)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
958 return a
959
960 class Friend(Relation(Account, Account)): pass
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
961
4d7a2fa @kemitche Allow users to block users that harass them
kemitche authored
962 Account.__bases__ += (UserRel('friend', Friend, disable_reverse_ids_fn=True),
6c10091 @kemitche Reverse lookup of blocked user
kemitche authored
963 UserRel('enemy', Friend, disable_reverse_ids_fn=False))
4440ccf @KeyserSosa overhaul of JS and form handling code, not based on jQuery
KeyserSosa authored
964
965 class DeletedUser(FakeAccount):
966 @property
967 def name(self):
968 return '[deleted]'
969
a402d48 New features:
Mike authored
970 @property
971 def _deleted(self):
972 return True
973
4440ccf @KeyserSosa overhaul of JS and form handling code, not based on jQuery
KeyserSosa authored
974 def _fullname(self):
975 raise NotImplementedError
976
977 def _id(self):
978 raise NotImplementedError
a402d48 New features:
Mike authored
979
980 def __setattr__(self, attr, val):
981 if attr == '_deleted':
982 pass
983 else:
984 object.__setattr__(self, attr, val)
2fe83ed @alienth Add AccountActivityBySR.
alienth authored
985
1520ddf @alienth Rename AccountActivityBySR to AccountsActiveBySR.
alienth authored
986 class AccountsActiveBySR(tdb_cassandra.View):
2fe83ed @alienth Add AccountActivityBySR.
alienth authored
987 _use_db = True
988 _connection_pool = 'main'
6bfa552 @kemitche tdb_cassandra: Magic up the _ttl attribute for ThingMeta users
kemitche authored
989 _ttl = timedelta(minutes=15)
2fe83ed @alienth Add AccountActivityBySR.
alienth authored
990
991 _extra_schema_creation_args = dict(key_validation_class=ASCII_TYPE)
992
993 _read_consistency_level = tdb_cassandra.CL.ONE
994 _write_consistency_level = tdb_cassandra.CL.ANY
995
996 @classmethod
997 def touch(cls, account, sr):
998 cls._set_values(sr._id36,
999 {account._id36: ''})
1000
1001 @classmethod
49607b7 @alienth Add a memoized method of get_count, and use it by default.
alienth authored
1002 def get_count(cls, sr, cached=True):
1003 return cls.get_count_cached(sr._id36, _update=not cached)
1004
1005 @classmethod
1006 @memoize('accounts_active', time=60)
1007 def get_count_cached(cls, sr_id):
1008 return cls._cf.get_count(sr_id)
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
1009
1010
ff5e5e0 @bsimpson63 Let users block messages from subreddits.
bsimpson63 authored
1011 class BlockedSubredditsByAccount(tdb_cassandra.DenormalizedRelation):
1012 _use_db = True
1013 _last_modified_name = 'block_subreddit'
1014 _read_consistency_level = tdb_cassandra.CL.QUORUM
1015 _write_consistency_level = tdb_cassandra.CL.QUORUM
1016 _connection_pool = 'main'
1017 _views = []
1018
1019 @classmethod
1020 def value_for(cls, thing1, thing2):
1021 return ''
1022
1023 @classmethod
1024 def block(cls, user, sr):
1025 cls.create(user, sr)
1026
1027 @classmethod
1028 def unblock(cls, user, sr):
1029 cls.destroy(user, sr)
1030
1031 @classmethod
1032 def is_blocked(cls, user, sr):
1033 try:
df77799 @bsimpson63 BlockedSubredditsByAccount: fix is_blocked method.
bsimpson63 authored
1034 r = cls.fast_query(user, [sr])
ff5e5e0 @bsimpson63 Let users block messages from subreddits.
bsimpson63 authored
1035 except tdb_cassandra.NotFound:
1036 return False
df77799 @bsimpson63 BlockedSubredditsByAccount: fix is_blocked method.
bsimpson63 authored
1037 return (user, sr) in r
ff5e5e0 @bsimpson63 Let users block messages from subreddits.
bsimpson63 authored
1038
1039
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
1040 @trylater_hooks.on("trylater.account_deletion")
fb507e5 @xiongchiamiov TryLater: use more generic parameter name
xiongchiamiov authored
1041 def on_account_deletion(data):
1042 for account_id36 in data.itervalues():
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
1043 account = Account._byID36(account_id36, data=True)
1044
1045 if not account._deleted:
1046 continue
1047
1048 account.password = ""
1049 account._commit()
003ca6e @bsimpson63 Add AccountsByCanonicalEmail.
bsimpson63 authored
1050
1051
1052 class AccountsByCanonicalEmail(tdb_cassandra.View):
1053 __metaclass__ = tdb_cassandra.ThingMeta
1054
1055 _use_db = True
1056 _compare_with = tdb_cassandra.UTF8_TYPE
1057 _extra_schema_creation_args = dict(
1058 key_validation_class=tdb_cassandra.UTF8_TYPE,
1059 )
1060
1061 @classmethod
1062 def update_email(cls, account, old, new):
1063 old, new = map(canonicalize_email, (old, new))
1064
1065 if old == new:
1066 return
1067
1068 with cls._cf.batch() as b:
1069 if old:
1070 b.remove(old, {account._id36: ""})
1071 if new:
1072 b.insert(new, {account._id36: ""})
1073
1074 @classmethod
1075 def get_accounts(cls, email_address):
1076 canonical = canonicalize_email(email_address)
1077 if not canonical:
1078 return []
1079 account_id36s = cls.get_time_sorted_columns(canonical).keys()
1080 return Account._byID36(account_id36s, data=True, return_dict=False)
Something went wrong with that request. Please try again.