diff --git a/configure-aspen.py b/configure-aspen.py index 133716609f..fa1094917f 100644 --- a/configure-aspen.py +++ b/configure-aspen.py @@ -9,6 +9,8 @@ gittip.wireup.canonical() gittip.wireup.db() gittip.wireup.billing() +gittip.wireup.id_restrictions(website) + website.github_client_id = os.environ['GITHUB_CLIENT_ID'].decode('ASCII') website.github_client_secret = os.environ['GITHUB_CLIENT_SECRET'].decode('ASCII') diff --git a/gittip/__init__.py b/gittip/__init__.py index 1f80193079..759acf7f99 100644 --- a/gittip/__init__.py +++ b/gittip/__init__.py @@ -35,6 +35,7 @@ def age(): OLD_AMOUNTS= [Decimal(a) for a in ('0.25',)] AMOUNTS = [Decimal(a) for a in ('0.00', '1.00', '3.00', '6.00', '12.00', '24.00')] +RESTRICTED_IDS = None # canonizer diff --git a/gittip/elsewhere/__init__.py b/gittip/elsewhere/__init__.py index 05fc0e98cb..735c31a4f9 100644 --- a/gittip/elsewhere/__init__.py +++ b/gittip/elsewhere/__init__.py @@ -1,7 +1,6 @@ -import os import random -from aspen import log, Response +from aspen import log from aspen.utils import typecheck from gittip import db from psycopg2 import IntegrityError @@ -11,39 +10,6 @@ class RunawayTrain(Exception): pass -ALLOWED_ASCII = set("0123456789" - "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - ".,-_;:@ ") - -def change_participant_id(website, old, suggested): - """Raise response return None. - - We want to be pretty loose with usernames. Unicode is allowed. So are - spaces. Control characters aren't. We also limit to 32 characters in - length. - - """ - for i, c in enumerate(suggested): - if i == 32: - raise Response(413) # Request Entity Too Large (more or less) - elif ord(c) < 128 and c not in ALLOWED_ASCII: - raise Response(400) # Yeah, no. - elif c not in ALLOWED_ASCII: - raise Response(400) # XXX Burned by an Aspen bug. :`-( - # https://github.com/whit537/aspen/issues/102 - - if website is not None and suggested in os.listdir(website.www_root): - raise Response(400) - - if suggested != old: - rec = db.fetchone( "UPDATE participants SET id=%s WHERE id=%s " \ - "RETURNING id", (suggested, old)) - # May raise IntegrityError - assert rec is not None # sanity check - assert suggested == rec['id'] # sanity check - - def get_a_participant_id(): """Return a random participant_id. @@ -214,15 +180,3 @@ def upsert(platform, user_id, username, user_info): , is_locked , rec['balance'] ) - - -def set_as_claimed(participant_id): - CLAIMED = """\ - - UPDATE participants - SET claimed_time=CURRENT_TIMESTAMP - WHERE id=%s - AND claimed_time IS NULL - - """ - db.execute(CLAIMED, (participant_id,)) diff --git a/gittip/participant.py b/gittip/participant.py index eca15e776a..2d948e650b 100644 --- a/gittip/participant.py +++ b/gittip/participant.py @@ -1,11 +1,18 @@ """Defines a Participant class. """ +from aspen import Response from decimal import Decimal import gittip from aspen.utils import typecheck +ASCII_ALLOWED_IN_PARTICIPANT_ID = set("0123456789" + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + ".,-_;:@ ") + + class NoParticipantId(StandardError): """Represent a bug where we treat an anonymous user as a participant. """ @@ -43,6 +50,11 @@ def get_details(self): return gittip.db.fetchone(SELECT, (self.id,)) + # Claiming + # ======== + # An unclaimed Participant is a stub that's created when someone pledges to + # give to an AccountElsewhere that's not been connected on Gittip yet. + @require_id def resolve_unclaimed(self): """Given a participant_id, return an URL path. @@ -58,6 +70,51 @@ def resolve_unclaimed(self): out = '/on/twitter/%s/' % rec['user_info']['screen_name'] return out + @require_id + def set_as_claimed(self): + CLAIM = """\ + + UPDATE participants + SET claimed_time=CURRENT_TIMESTAMP + WHERE id=%s + AND claimed_time IS NULL + + """ + gittip.db.execute(CLAIM, (self.id,)) + + + + @require_id + def change_id(self, suggested): + """Raise Response or return None. + + We want to be pretty loose with usernames. Unicode is allowed--XXX + aspen bug :(. So are spaces.Control characters aren't. We also limit to + 32 characters in length. + + """ + for i, c in enumerate(suggested): + if i == 32: + raise Response(413) # Request Entity Too Large (more or less) + elif ord(c) < 128 and c not in ASCII_ALLOWED_IN_PARTICIPANT_ID: + raise Response(400) # Yeah, no. + elif c not in ASCII_ALLOWED_IN_PARTICIPANT_ID: + raise Response(400) # XXX Burned by an Aspen bug. :`-( + # https://github.com/whit537/aspen/issues/102 + + if suggested in gittip.RESTRICTED_IDS: + raise Response(400) + + if suggested != self.id: + # Will raise IntegrityError if the desired participant_id is taken. + rec = gittip.db.fetchone("UPDATE participants " + "SET id=%s WHERE id=%s " + "RETURNING id", (suggested, self.id)) + + assert rec is not None # sanity check + assert suggested == rec['id'] # sanity check + self.id = suggested + @require_id def get_accounts_elsewhere(self): diff --git a/gittip/testing.py b/gittip/testing.py index 900c66d194..e89220ae9d 100644 --- a/gittip/testing.py +++ b/gittip/testing.py @@ -35,10 +35,11 @@ def create_schema(db): ] def populate_db_with_dummy_data(db): - from gittip.elsewhere import github, change_participant_id + from gittip.elsewhere import github + from gittip.participant import Participant for user_id, login in GITHUB_USERS: participant_id, a,b,c = github.upsert({"id": user_id, "login": login}) - change_participant_id(None, participant_id, login) + Participant(participant_id).change_id(login) class GittipBaseDBTest(unittest.TestCase): diff --git a/gittip/wireup.py b/gittip/wireup.py index 8bf52b572d..2c61bc9c95 100644 --- a/gittip/wireup.py +++ b/gittip/wireup.py @@ -31,3 +31,7 @@ def billing(): stripe.api_key= os.environ['STRIPE_SECRET_API_KEY'] stripe.publishable_api_key= os.environ['STRIPE_PUBLISHABLE_API_KEY'] balanced.configure(os.environ['BALANCED_API_SECRET']) + + +def id_restrictions(website): + gittip.RESTRICTED_IDS = os.listdir(website.www_root) diff --git a/www/%participant_id/participant_id.json b/www/%participant_id/participant_id.json index 7f0f5e0d47..eb967905ba 100644 --- a/www/%participant_id/participant_id.json +++ b/www/%participant_id/participant_id.json @@ -1,5 +1,4 @@ from aspen import Response -from gittip.elsewhere import change_participant_id from psycopg2 import IntegrityError # ========================================================================== ^L @@ -10,7 +9,7 @@ if user.ANON: new_participant_id = request.body['participant_id'] try: - change_participant_id(website, user.id, new_participant_id) + user.change_id(new_participant_id) response.body = {"participant_id": new_participant_id} except IntegrityError: raise Response(409) # Conflict diff --git a/www/on/github/associate b/www/on/github/associate index 79f07b1cc6..a60546e0c8 100644 --- a/www/on/github/associate +++ b/www/on/github/associate @@ -8,8 +8,8 @@ the Gittip community. """ from aspen import log, Response from gittip import db -from gittip.elsewhere import change_participant_id, github, set_as_claimed from gittip.authentication import User +from gittip.elsewhere import github from psycopg2 import IntegrityError # ========================== ^L @@ -34,14 +34,14 @@ if login is None: log(u"%s wants to %s" % (login, action)) if action == 'opt-in': # opt in participant_id, is_claimed, is_locked, balance = github.upsert(user_info) + user = User.from_id(participant_id) # give them a session if not is_claimed: + user.set_as_claimed() try: - change_participant_id(website, participant_id, login) + user.change_id(login) participant_id = login except (Response, IntegrityError): pass - set_as_claimed(participant_id) - user = User.from_id(participant_id) # give them a session else: # lock or unlock if then != login: diff --git a/www/on/twitter/associate b/www/on/twitter/associate index 71245e10f4..aabf4e68a3 100644 --- a/www/on/twitter/associate +++ b/www/on/twitter/associate @@ -10,8 +10,8 @@ import requests from oauth_hook import OAuthHook from aspen import log, Response, json from gittip import db -from gittip.elsewhere import change_participant_id, twitter, set_as_claimed from gittip.authentication import User +from gittip.elsewhere import twitter from urlparse import parse_qs from psycopg2 import IntegrityError @@ -63,14 +63,14 @@ user_info['html_url'] = "https://twitter.com/" + screen_name log(u"%s wants to %s" % (screen_name, action)) if action == 'opt-in': # opt in participant_id, is_claimed, is_locked, balance = twitter.upsert(user_info) + user = User.from_id(participant_id) # give them a session if not is_claimed: + user.set_as_claimed() try: - change_participant_id(website, participant_id, screen_name) + user.change_id(screen_name) participant_id = screen_name except (Response, IntegrityError): pass - set_as_claimed(participant_id) - user = User.from_id(participant_id) # give them a session else: # lock or unlock if then != screen_name: