Skip to content

HTTPS clone URL

Subversion checkout URL

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