Skip to content

Commit 0c63715

Browse files
committed
Major rewrite/refactor of voting code
This is a rewrite of much of the code related to voting. Some of the improvements include: - Detangling the whole process of creating and processing votes - Creating an actual Vote class to use instead of dealing with inconsistent ad-hoc voting data everywhere - More consistency with naming and other similar things like vote directions (previously had True/False/None in some places, 1/-1/0 in others, etc.) - More flexible methods in determining and applying the effects of votes - Improvement of modhash generation/validation - Removing various obsolete/unnecessary code
1 parent 440ce32 commit 0c63715

28 files changed

+777
-614
lines changed

r2/r2/controllers/api.py

+25-47
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from r2.controllers.reddit_base import (
2424
cross_domain,
25+
generate_modhash,
2526
is_trusted_origin,
2627
MinimalController,
2728
pagecache_policy,
@@ -51,7 +52,6 @@
5152
query_string,
5253
randstr,
5354
sanitize_url,
54-
timeago,
5555
timefromnow,
5656
timeuntil,
5757
tup,
@@ -110,9 +110,11 @@
110110
from r2.controllers.ipn import generate_blob, update_blob
111111
from r2.lib.lock import TimeoutExpired
112112
from r2.lib.csrf import csrf_exempt
113+
from r2.lib.voting import cast_vote
113114

114115
from r2.models import wiki
115116
from r2.models.recommend import AccountSRFeedback, FEEDBACK_ACTIONS
117+
from r2.models.vote import Vote
116118
from r2.lib.merge import ConflictException
117119

118120
import csv
@@ -124,20 +126,6 @@
124126
import urllib
125127
import urllib2
126128

127-
def reject_vote(thing):
128-
voteword = request.params.get('dir')
129-
130-
if voteword == '1':
131-
voteword = 'upvote'
132-
elif voteword == '0':
133-
voteword = '0-vote'
134-
elif voteword == '-1':
135-
voteword = 'downvote'
136-
137-
log_text ("rejected vote", "Rejected %s from %s (%s) on %s %s via %s" %
138-
(voteword, c.user.name, request.ip, thing.__class__.__name__,
139-
thing._id36, request.referer), "info")
140-
141129

142130
class ApiminimalController(MinimalController):
143131
"""
@@ -547,7 +535,6 @@ def POST_submit(self, form, jquery, url, selftext, kind, title,
547535
sendreplies=sendreplies,
548536
)
549537

550-
551538
if not is_self:
552539
ban = is_banned_domain(url)
553540
if ban:
@@ -556,9 +543,6 @@ def POST_submit(self, form, jquery, url, selftext, kind, title,
556543
hooks.get_hook('banned_domain.submit').call(item=l, url=url,
557544
ban=ban)
558545

559-
queries.queue_vote(c.user, l, dir=True, ip=request.ip,
560-
cheater=c.cheater)
561-
562546
if sr.should_ratelimit(c.user, 'link'):
563547
VRatelimit.ratelimit(rate_user=True, rate_ip = True,
564548
prefix = "rate_submit_")
@@ -607,7 +591,7 @@ def _login(self, responder, user, rem = None):
607591
self.login(user, rem = rem)
608592

609593
if request.params.get("hoist") != "cookie":
610-
responder._send_data(modhash = user.modhash())
594+
responder._send_data(modhash=generate_modhash())
611595
responder._send_data(cookie = user.make_cookie())
612596
responder._send_data(need_https=feature.is_enabled("force_https"))
613597

@@ -1882,8 +1866,7 @@ def POST_editusertext(self, form, jquery, item, text):
18821866
if item._deleted:
18831867
return abort(403, "forbidden")
18841868

1885-
if (item._date < timeago('3 minutes')
1886-
or (item._ups + item._downs > 2)):
1869+
if item._age > timedelta(minutes=3) or item.num_votes > 2:
18871870
item.editted = c.start_time
18881871

18891872
item.ignore_reports = False
@@ -2035,8 +2018,6 @@ def POST_comment(self, commentform, jquery, parent, comment):
20352018
else:
20362019
item, inbox_rel = Comment._new(c.user, link, parent_comment,
20372020
comment, request.ip)
2038-
queries.queue_vote(c.user, item, dir=True, ip=request.ip,
2039-
cheater=c.cheater)
20402021

20412022
if is_message:
20422023
queries.new_message(item, inbox_rel)
@@ -2161,12 +2142,12 @@ def POST_share(self, shareform, jquery, share_to, message, link):
21612142
@require_oauth2_scope("vote")
21622143
@noresponse(VUser(),
21632144
VModhash(),
2164-
vote_info=VVotehash('vh'),
2165-
dir=VInt('dir', min=-1, max=1, docs={"dir":
2166-
"vote direction. one of (1, 0, -1)"}),
2145+
direction=VInt("dir", min=-1, max=1,
2146+
docs={"dir": "vote direction. one of (1, 0, -1)"}
2147+
),
21672148
thing=VByName('id'))
21682149
@api_doc(api_section.links_and_comments)
2169-
def POST_vote(self, dir, thing, vote_info):
2150+
def POST_vote(self, direction, thing):
21702151
"""Cast a vote on a thing.
21712152
21722153
`id` should be the fullname of the Link or Comment to vote on.
@@ -2182,34 +2163,31 @@ def POST_vote(self, dir, thing, vote_info):
21822163
21832164
"""
21842165

2185-
user = c.user
2186-
store = True
2187-
21882166
if not thing or thing._deleted:
21892167
return self.abort404()
21902168

2191-
hooks.get_hook("vote.validate").call(thing=thing)
2169+
if not thing.is_votable:
2170+
abort(400, "That type of thing can't be voted on.")
21922171

2193-
if not isinstance(thing, (Link, Comment)):
2194-
return self.abort404()
2172+
hooks.get_hook("vote.validate").call(thing=thing)
21952173

21962174
if isinstance(thing, Link) and promote.is_promo(thing):
21972175
if not promote.is_promoted(thing):
21982176
return abort(400, "Bad Request")
21992177

2200-
if vote_info == 'rejected':
2201-
reject_vote(thing)
2202-
store = False
2203-
2204-
if thing._age > thing.subreddit_slow.archive_age:
2205-
store = False
2206-
2207-
dir = (True if dir > 0
2208-
else False if dir < 0
2209-
else None)
2210-
2211-
queries.queue_vote(user, thing, dir, request.ip, vote_info=vote_info,
2212-
store=store, cheater=c.cheater)
2178+
if thing.archived:
2179+
return abort(400,
2180+
"This thing is archived and may no longer be voted on")
2181+
2182+
# convert vote direction to enum value
2183+
if direction == 1:
2184+
direction = Vote.DIRECTIONS.up
2185+
elif direction == -1:
2186+
direction = Vote.DIRECTIONS.down
2187+
elif direction == 0:
2188+
direction = Vote.DIRECTIONS.unvote
2189+
2190+
cast_vote(c.user, thing, direction)
22132191

22142192
@require_oauth2_scope("modconfig")
22152193
@validatedForm(VSrModerator(perms='config'),

r2/r2/controllers/front.py

-4
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
from r2.lib.pages.things import hot_links_by_url_listing
4141
from r2.lib.pages import trafficpages
4242
from r2.lib.menus import *
43-
from r2.lib.admin_utils import check_cheating
4443
from r2.lib.csrf import csrf_exempt
4544
from r2.lib.utils import to36, sanitize_url, title_to_url
4645
from r2.lib.utils import query_string, UrlParser, url_links_builder
@@ -305,8 +304,6 @@ def GET_comments(
305304
infotext_class = 'locked-infobar'
306305
infotext_show_icon = True
307306

308-
check_cheating('comments')
309-
310307
if not c.user.pref_num_comments:
311308
num = g.num_comments
312309
elif c.user_is_loggedin and (c.user.gold or sr.is_moderator(c.user)):
@@ -1169,7 +1166,6 @@ def GET_search(self, query, num, reverse, after, count, sort, recent,
11691166
content = subreddits
11701167
subreddits = None
11711168

1172-
check_cheating("search")
11731169
res = SearchPage(_('search results'), query,
11741170
content=content,
11751171
subreddits=subreddits,

r2/r2/controllers/listingcontroller.py

-6
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
from r2.lib.db import queries
4343
from r2.lib.strings import Score
4444
from r2.lib.template_helpers import add_sr
45-
from r2.lib.admin_utils import check_cheating
4645
from r2.lib.csrf import csrf_exempt
4746
from r2.lib.utils import (
4847
extract_user_mentions,
@@ -239,7 +238,6 @@ def rightbox(self):
239238
@require_oauth2_scope("read")
240239
@base_listing
241240
def GET_listing(self, **env):
242-
check_cheating('site')
243241
if self.can_send_referrer():
244242
c.referrer_policy = "always"
245243
return self.build_listing(**env)
@@ -927,8 +925,6 @@ def GET_listing(self, where, vuser, sort, time, show, **env):
927925
self.savedsr = sr
928926
self.savedcategory = category
929927

930-
check_cheating('user')
931-
932928
self.vuser = vuser
933929
self.render_params = {'user' : vuser}
934930
c.profilepage = True
@@ -1692,7 +1688,6 @@ def GET_user_prefs(self, where, **kw):
16921688
abort(404)
16931689

16941690
kw['num'] = 0
1695-
check_cheating('site')
16961691
return self.build_listing(**kw)
16971692

16981693

@@ -1774,7 +1769,6 @@ def GET_listing(self, where, user=None, **kw):
17741769
if not self.paginated:
17751770
kw['num'] = 0
17761771

1777-
check_cheating('site')
17781772
return self.build_listing(**kw)
17791773

17801774

r2/r2/controllers/oauth2.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def _error_response(self, state, redirect_uri, as_fragment=False):
8989
resp["error"] = "unauthorized_client"
9090
elif (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors:
9191
resp["error"] = "access_denied"
92-
elif (errors.BAD_HASH, None) in c.errors:
92+
elif (errors.INVALID_MODHASH, None) in c.errors:
9393
resp["error"] = "access_denied"
9494
elif (errors.INVALID_OPTION, "response_type") in c.errors:
9595
resp["error"] = "unsupported_response_type"

r2/r2/controllers/post.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,11 @@ def GET_quarantine(self, dest):
109109
request.environ['usable_error_content'] = errpage.render()
110110
self.abort403()
111111

112-
@validate(VModhash(fatal=False),
113-
over18 = nop('over18'),
114-
dest = VDestination(default = '/'))
112+
@csrf_exempt
113+
@validate(
114+
over18=nop('over18'),
115+
dest=VDestination(default='/'),
116+
)
115117
def POST_over18(self, over18, dest):
116118
if over18 == 'yes':
117119
if c.user_is_loggedin and not c.errors:

r2/r2/controllers/reddit_base.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,6 @@ def _subscribe(self, sr):
231231
def _unsubscribe(self, sr):
232232
pass
233233

234-
def valid_hash(self, hash):
235-
return False
236-
237234
def _commit(self):
238235
if self._dirty:
239236
for k, (oldv, newv) in self._dirties.iteritems():
@@ -724,6 +721,20 @@ def make_url_https(url):
724721
return new_url.unparse()
725722

726723

724+
def generate_modhash():
725+
# OAuth clients should never receive a modhash of any kind as they could
726+
# use it in a CSRF attack to bypass their permitted OAuth scopes
727+
if c.oauth_user:
728+
return None
729+
730+
modhash = hooks.get_hook("modhash.generate").call_until_return()
731+
if modhash is not None:
732+
return modhash
733+
734+
# if no plugins generate a modhash, just use the user name
735+
return c.user.name
736+
737+
727738
def enforce_https():
728739
"""Enforce policy for forced usage of HTTPS."""
729740

@@ -1463,7 +1474,7 @@ def pre(self):
14631474

14641475
if c.user_is_loggedin:
14651476
self.set_up_user_context()
1466-
c.modhash = c.user.modhash()
1477+
c.modhash = generate_modhash()
14671478
c.user_is_admin = maybe_admin and c.user.name in g.admins
14681479
c.user_is_sponsor = c.user_is_admin or c.user.name in g.sponsors
14691480
c.otp_cached = is_otpcookie_valid

r2/r2/lib/admin_utils.py

-43
This file was deleted.

r2/r2/lib/automoderator.py

-2
Original file line numberDiff line numberDiff line change
@@ -1401,8 +1401,6 @@ def perform_actions(self, item, data):
14011401
new_comment.distinguished = "yes"
14021402
new_comment.sendreplies = False
14031403
new_comment._commit()
1404-
queries.queue_vote(ACCOUNT, new_comment, dir=True, ip=None,
1405-
send_event=False)
14061404
queries.new_comment(new_comment, inbox_rel)
14071405

14081406
g.stats.simple_event("automoderator.comment")

r2/r2/lib/count.py

-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@
3434
def incr_counts(wrapped):
3535
pass
3636

37-
def incr_sr_count(sr):
38-
pass
39-
4037
def get_link_counts(period = count_period):
4138
links = Link._query(Link.c._date >= utils.timeago(period),
4239
limit=50, data = True)

0 commit comments

Comments
 (0)