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