def test_stale_records_get_deleted_when_function_is_called(): token_store = [] for i in range(20): user_name = "testPrepare%d" % (i, ) email_addr = "*****@*****.**" % (i, ) user_id = db_utils.create_user(email_addr=email_addr, username=user_name) form_for_request = Bag(email=email_addr, username=user_name, day=arrow.now().day, month=arrow.now().month, year=arrow.now().year) resetpassword.request(form_for_request) pw_reset_token = d.engine.scalar( "SELECT token FROM forgotpassword WHERE userid = %(id)s", id=user_id) token_store.append(pw_reset_token) # All tokens should exist at this point for i in range(20): assert resetpassword.checktoken(token_store[i]) # Set 5 tokens to be two hours old (0,5) (7200) for i in range(0, 5): d.engine.execute( "UPDATE forgotpassword SET set_time = %(time)s WHERE token = %(token)s", time=d.get_time() - 7200, token=token_store[i]) # Set 5 tokens to be 30 minutes old (5,10) (1800) for i in range(5, 10): d.engine.execute( "UPDATE forgotpassword SET set_time = %(time)s WHERE token = %(token)s", time=d.get_time() - 1800, token=token_store[i]) # Set 5 tokens to be 10 minutes old for the last visit time (10,15) (600) for i in range(10, 15): d.engine.execute( "UPDATE forgotpassword SET link_time = %(time)s WHERE token = %(token)s", time=d.get_time() - 600, token=token_store[i]) # Set 5 tokens to be 2 minutes old for the last visit time (10,15) (120) for i in range(15, 20): d.engine.execute( "UPDATE forgotpassword SET link_time = %(time)s WHERE token = %(token)s", time=d.get_time() - 120, token=token_store[i]) # This should clear all tokens >1hr old, and all tokens >5 minutes from last visit (10 total) resetpassword.prepare('foo') # This range should be cleared (set_time > 3600) for i in range(0, 5): assert not resetpassword.checktoken(token_store[i]) # This range should still be present (set_time < 3600) for i in range(5, 10): assert resetpassword.checktoken(token_store[i]) # This range should be cleared (link_time > 300) for i in range(10, 15): assert not resetpassword.checktoken(token_store[i]) # This range should still be present (link_time < 300) for i in range(15, 20): assert resetpassword.checktoken(token_store[i])
def select_query(userid, rating, otherid=None, folderid=None, backid=None, nextid=None, subcat=None, exclude=None, options=[], config=None, profile_page_filter=False, index_page_filter=False, featured_filter=False): if config is None: config = d.get_config(userid) statement = [ "FROM submission su " "INNER JOIN profile pr ON su.userid = pr.userid " "LEFT JOIN folder f USING (folderid) " "WHERE su.settings !~ 'h'"] if profile_page_filter: statement.append(" AND COALESCE(f.settings !~ 'u', true)") if index_page_filter: statement.append(" AND COALESCE(f.settings !~ 'm', true)") if featured_filter: statement.append(" AND COALESCE(f.settings ~ 'f', false)") # Logged in users will see their own submissions regardless of rating # EXCEPT if they are in SFW mode if userid and not d.is_sfw_mode(): statement.append(" AND (su.rating <= %i OR su.userid = %i)" % (rating, userid)) else: statement.append(" AND su.rating <= %i" % (rating,)) if otherid: statement.append(" AND su.userid = %i" % (otherid,)) if folderid: statement.append(" AND su.folderid = %i" % (folderid,)) if exclude: statement.append(" AND su.submitid != %i" % (exclude,)) if subcat: statement.append(" AND su.subtype >= %i AND su.subtype < %i" % (subcat, subcat + 1000)) if "critique" in options: statement.append(" AND su.settings ~ 'q' AND su.unixtime > %i" % (d.get_time() - 259200,)) if backid: statement.append(" AND su.submitid > %i" % (backid,)) elif nextid: statement.append(" AND su.submitid < %i" % (nextid,)) elif "offset" in options: statement.append(" AND su.unixtime < %i" % (d.get_time() - 1800,)) if userid: statement.append(m.MACRO_FRIENDUSER_SUBMIT % (userid, userid, userid)) if not otherid: statement.append(m.MACRO_IGNOREUSER % (userid, "su")) statement.append(m.MACRO_BLOCKTAG_SUBMIT % (userid, userid)) else: statement.append(" AND su.settings !~ 'f'") return statement
def prepare(token): # Remove records from the forgotpassword table which have been active for # more than one hour, regardless of whether or not the user has clicked the # associated link provided to them in the password reset request email, or # which have been visited but have not been removed by the password reset # script within five minutes of being visited d.execute("DELETE FROM forgotpassword WHERE set_time < %i OR link_time > 0 AND link_time < %i", [d.get_time() - 3600, d.get_time() - 300]) # Set the unixtime record for which the link associated with `token` was # visited by the user d.execute("UPDATE forgotpassword SET link_time = %i WHERE token = '%s'", [d.get_time(), token])
def select_streaming(userid, rating, limit, following=True, order_by=None): statement = [ "SELECT userid, pr.username, pr.stream_url, pr.config, pr.stream_text, start_time " "FROM profile pr " "JOIN user_streams USING (userid) " "WHERE end_time > %i" % (d.get_time(),) ] if userid: statement.append(m.MACRO_IGNOREUSER % (userid, "pr")) if following: pass # todo if order_by: statement.append(" ORDER BY %s LIMIT %i" % (order_by, limit)) else: statement.append(" ORDER BY RANDOM() LIMIT %i" % limit) ret = [{ "userid": i[0], "username": i[1], "stream_url": i[2], "stream_text": i[4], "stream_time": i[5], } for i in d.execute("".join(statement)) if i[2]] media.populate_with_user_media(ret) return ret
def verify(db, userid, token): # Select purchased terms terms = define.execute( db, "SELECT terms FROM premiumpurchase WHERE token = '%s'", [token], ["element"]) if not terms: raise error.WeasylError("tokenInvalid") # Select current terms current = define.execute( db, "SELECT terms FROM userpremium WHERE userid = %i", [userid], ["element"]) # Update premium status if current: define.execute( db, "UPDATE userpremium SET terms = terms + %i WHERE userid = %i", [terms, userid]) else: define.execute(db, "INSERT INTO userpremium VALUES (%i, %i, %i)", [userid, define.get_time(), terms]) define.execute( db, "UPDATE profile SET config = config || 'd' WHERE userid = %i AND config !~ 'd'", [userid]) define.execute(db, "DELETE FROM premiumpurchase WHERE token = '%s'", [token])
def request(form): token = security.generate_key(100) email = emailer.normalize_address(form.email) # Determine the user associated with `username`; if the user is not found, # raise an exception user_id = d.engine.scalar(""" SELECT userid FROM login WHERE email = %(email)s """, email=email) # If `user_id` exists, then the supplied email was valid; if not valid, do nothing, raising # no errors for plausible deniability of email existence if user_id: # Insert a record into the forgotpassword table for the user, # or update an existing one now = d.get_time() address = d.get_address() d.engine.execute(""" INSERT INTO forgotpassword (userid, token, set_time, address) VALUES (%(id)s, %(token)s, %(time)s, %(address)s) ON CONFLICT (userid) DO UPDATE SET token = %(token)s, set_time = %(time)s, address = %(address)s """, id=user_id, token=token, time=now, address=address) # Generate and send an email to the user containing a password reset link emailer.append([email], None, "Weasyl Password Recovery", d.render("email/reset_password.html", [token]))
def offer(userid, submitid, otherid): query = d.engine.execute( "SELECT userid, rating, settings FROM submission WHERE submitid = %(id)s", id=submitid, ).first() if not query or "h" in query[2]: raise WeasylError("Unexpected") elif userid != query[0]: raise WeasylError("Unexpected") # Check collection acceptability if otherid: rating = d.get_rating(otherid) if rating < query[1]: raise WeasylError("collectionUnacceptable") if "f" in query[2]: raise WeasylError("collectionUnacceptable") if ignoreuser.check(otherid, userid): raise WeasylError("IgnoredYou") if ignoreuser.check(userid, otherid): raise WeasylError("YouIgnored") if blocktag.check(otherid, submitid=submitid): raise WeasylError("collectionUnacceptable") try: d.execute( "INSERT INTO collection (userid, submitid, unixtime) VALUES (%i, %i, %i)", [otherid, submitid, d.get_time()]) except PostgresError: raise WeasylError("collectionExists") welcome.collectoffer_insert(userid, otherid, submitid)
def test_commish_search_invalid(): # searcher u1 = db_utils.create_user(username="******") # user not open for commissions, but with submissions and commish classes defined create_commish_searchable_user("u2", commish_status='c') # user open for commission but without any commish classes u3 = create_commish_searchable_user("u3") classid = commishinfo.select_list(u3)["class"][0]["classid"] commishinfo.remove_class(u3, classid) # user meets all requirements, but is suspended u4 = create_commish_searchable_user("u4") db_utils.create_suspenduser(u4, "", d.get_time() + 604800) # user meets all requirements, but is banned u5 = create_commish_searchable_user("u5") db_utils.create_banuser(u5, "") # user meets all requirements, but is ignored by searching user u6 = create_commish_searchable_user("u6") db_utils.create_ignoreuser(u1, u6) results = commishinfo.select_commissionable(userid=u1, limit=10, offset=0, q="", commishclass="", min_price=None, max_price=None, currency='') assert not results
def request(form): token = security.generate_key(100) email = emailer.normalize_address(form.email) username = d.get_sysname(form.username) # Determine the user associated with `username`; if the user is not found, # raise an exception user = d.engine.execute( "SELECT userid, email FROM login WHERE login_name = %(username)s", username=username).first() if not user: raise WeasylError("loginRecordMissing") # Check the user's email address against the provided e-mail address, # raising an exception if there is a mismatch if email != emailer.normalize_address(user.email): raise WeasylError("emailInvalid") # Insert a record into the forgotpassword table for the user, # or update an existing one now = d.get_time() address = d.get_address() d.engine.execute(""" INSERT INTO forgotpassword (userid, token, set_time, address) VALUES (%(id)s, %(token)s, %(time)s, %(address)s) ON CONFLICT (userid) DO UPDATE SET token = %(token)s, set_time = %(time)s, address = %(address)s """, id=user.userid, token=token, time=now, address=address) # Generate and send an email to the user containing a password reset link emailer.append([email], None, "Weasyl Password Recovery", d.render("email/reset_password.html", [token]))
def select_streaming(userid, rating, limit, following=True, order_by=None): statement = [ "SELECT userid, pr.username, pr.stream_url, pr.config, pr.stream_text, start_time " "FROM profile pr " "JOIN user_streams USING (userid) " "WHERE end_time > %i" % (d.get_time(), ) ] if userid: statement.append(m.MACRO_IGNOREUSER % (userid, "pr")) if following: pass # todo if order_by: statement.append(" ORDER BY %s LIMIT %i" % (order_by, limit)) else: statement.append(" ORDER BY RANDOM() LIMIT %i" % limit) ret = [{ "userid": i[0], "username": i[1], "stream_url": i[2], "stream_text": i[4], "stream_time": i[5], } for i in d.execute("".join(statement)) if i[2]] media.populate_with_user_media(ret) return ret
def signin(userid): # Update the last login record for the user d.execute("UPDATE login SET last_login = %i WHERE userid = %i", [d.get_time(), userid]) # set the userid on the session sess = d.get_weasyl_session() sess.userid = userid sess.save = True
def test_login_fails_if_user_is_suspended(): user_id = db_utils.create_user(password=raw_password, username=user_name) d.engine.execute("UPDATE login SET settings = 's' WHERE userid = %(id)s", id=user_id) release_date = d.get_time() + 60 d.engine.execute("INSERT INTO suspension VALUES (%(id)s, %(reason)s, %(rel)s)", id=user_id, reason='test', rel=release_date) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, 'suspended')
def run_periodic_tasks(): now = arrow.utcnow() time_now = get_time() db = engine.connect() with db.begin(): locked = db.scalar("SELECT pg_try_advisory_xact_lock(0)") if not locked: return last_run = arrow.get(db.scalar("SELECT last_run FROM cron_runs")) if not last_run or now < last_run.replace(seconds=59): return # Recache the latest submissions # Every 2 minutes if now.minute % 2 == 0: index.recent_submissions.refresh() log.msg('refreshed recent submissions') # Recache the active user counts # Every 5 minutes if now.minute % 5 == 0: active_users.refresh() log.msg('refreshed active user counts') # Recalculate recently popular submissions # Every 10 minutes if now.minute % 10 == 0: submission.select_recently_popular.refresh() log.msg('refreshed recently popular submissions') # Delete all records from views table # Every 15 minutes if now.minute % 15 == 0: db.execute("DELETE FROM views") log.msg('cleared views') # Daily at 0:00 if now.hour == 0 and now.minute == 0: # Delete password resets older than one day db.execute("DELETE FROM forgotpassword WHERE set_time < %(expiry)s", expiry=time_now - 86400) log.msg('cleared old forgotten password requests') # Delete email reset requests older than two days db.execute(""" DELETE FROM emailverify WHERE createtimestamp < (NOW() - INTERVAL '2 days') """) log.msg('cleared stale email change records') # Purge stale logincreate records older than seven days db.execute(""" DELETE FROM logincreate WHERE unixtime < %(time)s """, time=now - (86400 * 2)) log.msg('cleared stale account creation records') db.execute("UPDATE cron_runs SET last_run = %(now)s", now=now.naive)
def remove_all_submissions(userid, only_before=None): if not only_before: only_before = d.get_time() d.engine.execute( "DELETE FROM welcome WHERE userid = %(user)s AND type IN (2010, 2030, 2040, 2050) AND unixtime < %(before)s", user=userid, before=only_before) d._page_header_info.invalidate(userid)
def signin(userid): # Update the last login record for the user d.execute("UPDATE login SET last_login = %i WHERE userid = %i", [d.get_time(), userid]) # set the userid on the session sess = d.web.ctx.weasyl_session sess.userid = userid sess.save = True
def force_resetbirthday(userid, birthday): if not birthday: raise WeasylError("birthdayInvalid") elif birthday > d.get_time(): raise WeasylError("birthdayInvalid") d.execute("UPDATE userinfo SET birthday = %i WHERE userid = %i", [birthday, userid]) d.execute("UPDATE login SET settings = REPLACE(settings, 'i', '') WHERE userid = %i", [userid]) d.get_login_settings.invalidate(userid)
def test_stale_records_get_deleted_when_function_is_called(): token_store = [] for i in range(20): user_name = "testPrepare%d" % (i,) email_addr = "*****@*****.**" % (i,) user_id = db_utils.create_user(email_addr=email_addr, username=user_name) form_for_request = Bag(email=email_addr, username=user_name, day=arrow.now().day, month=arrow.now().month, year=arrow.now().year) resetpassword.request(form_for_request) pw_reset_token = d.engine.scalar("SELECT token FROM forgotpassword WHERE userid = %(id)s", id=user_id) token_store.append(pw_reset_token) # All tokens should exist at this point for i in range(20): assert resetpassword.checktoken(token_store[i]) # Set 5 tokens to be two hours old (0,5) (7200) for i in range(0, 5): d.engine.execute("UPDATE forgotpassword SET set_time = %(time)s WHERE token = %(token)s", time=d.get_time() - 7200, token=token_store[i]) # Set 5 tokens to be 30 minutes old (5,10) (1800) for i in range(5, 10): d.engine.execute("UPDATE forgotpassword SET set_time = %(time)s WHERE token = %(token)s", time=d.get_time() - 1800, token=token_store[i]) # Set 5 tokens to be 10 minutes old for the last visit time (10,15) (600) for i in range(10, 15): d.engine.execute("UPDATE forgotpassword SET link_time = %(time)s WHERE token = %(token)s", time=d.get_time() - 600, token=token_store[i]) # Set 5 tokens to be 2 minutes old for the last visit time (10,15) (120) for i in range(15, 20): d.engine.execute("UPDATE forgotpassword SET link_time = %(time)s WHERE token = %(token)s", time=d.get_time() - 120, token=token_store[i]) # This should clear all tokens >1hr old, and all tokens >5 minutes from last visit (10 total) resetpassword.prepare('foo') # This range should be cleared (set_time > 3600) for i in range(0, 5): assert not resetpassword.checktoken(token_store[i]) # This range should still be present (set_time < 3600) for i in range(5, 10): assert resetpassword.checktoken(token_store[i]) # This range should be cleared (link_time > 300) for i in range(10, 15): assert not resetpassword.checktoken(token_store[i]) # This range should still be present (link_time < 300) for i in range(15, 20): assert resetpassword.checktoken(token_store[i])
def edit_streaming_settings(my_userid, userid, profile, set_stream=None, stream_length=0): if set_stream == 'start': try: stream_length = int(stream_length) except: raise WeasylError("streamDurationOutOfRange") if stream_length < 1 or stream_length > 360: raise WeasylError("streamDurationOutOfRange") if set_stream == 'start' and not profile.stream_url: raise WeasylError("streamLocationNotSet") # unless we're specifically still streaming, clear the user_streams record if set_stream != 'still': d.execute("DELETE FROM user_streams WHERE userid = %i", [userid]) settings_flag = '' stream_status = None # if we're starting to stream, update user_streams to reflect that if set_stream == 'start': now = d.get_time() stream_end = now + stream_length * 60 # stream_length is minutes; we need seconds d.execute("INSERT INTO user_streams VALUES (%i, %i, %i)", [userid, now, stream_end]) stream_status = 'n' # if we're going to stream later, update profile.settings to reflect that elif set_stream == 'later': settings_flag = stream_status = 'l' # if stream_status is None, any rows in `welcome` will get cleared. but, if # the user is still streaming, that shouldn't happen. otherwise, `welcome` # will get updated with the current stream state. if set_stream != 'still': welcome.stream_insert(userid, stream_status) d.execute( "UPDATE profile " "SET (stream_text, stream_url, settings) = ('%s', '%s', REGEXP_REPLACE(settings, '[nli]', '') || '%s') " "WHERE userid = %i", [profile.stream_text, profile.stream_url, settings_flag, userid]) if my_userid != userid: from weasyl import moderation note_body = ('- Stream url: %s\n' '- Stream description: %s\n' '- Stream status: %s' % (profile.stream_url, profile.stream_text, STREAMING_ACTION_MAP[set_stream])) moderation.note_about(my_userid, userid, 'Streaming settings updated:', note_body)
def edit_streaming_settings(my_userid, userid, profile, set_stream=None, stream_length=0): if set_stream == 'start': if stream_length < 1 or stream_length > 360: raise WeasylError("streamDurationOutOfRange") if not profile.stream_url: raise WeasylError("streamLocationNotSet") # unless we're specifically still streaming, clear the user_streams record if set_stream != 'still': d.execute("DELETE FROM user_streams WHERE userid = %i", [userid]) settings_flag = '' stream_status = None # if we're starting to stream, update user_streams to reflect that if set_stream == 'start': now = d.get_time() stream_end = now + stream_length * 60 # stream_length is minutes; we need seconds d.execute("INSERT INTO user_streams VALUES (%i, %i, %i)", [userid, now, stream_end]) stream_status = 'n' # if we're going to stream later, update profile.settings to reflect that elif set_stream == 'later': settings_flag = stream_status = 'l' # if stream_status is None, any rows in `welcome` will get cleared. but, if # the user is still streaming, that shouldn't happen. otherwise, `welcome` # will get updated with the current stream state. if set_stream != 'still': welcome.stream_insert(userid, stream_status) pr = d.meta.tables['profile'] d.engine.execute(pr.update().where(pr.c.userid == userid).values({ 'stream_text': profile.stream_text, 'stream_url': profile.stream_url, 'settings': sa.func.regexp_replace(pr.c.settings, "[nli]", "").concat(settings_flag), })) if my_userid != userid: from weasyl import moderation note_body = ('- Stream url: %s\n' '- Stream description: %s\n' '- Stream status: %s' % (profile.stream_url, profile.stream_text, STREAMING_ACTION_MAP[set_stream])) moderation.note_about(my_userid, userid, 'Streaming settings updated:', note_body)
def test_login_fails_if_user_is_suspended(): user_id = db_utils.create_user(password=raw_password, username=user_name) release_date = d.get_time() + 60 db_utils.create_suspenduser(userid=user_id, reason="Testing", release=release_date) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, 'suspended')
def run_periodic_tasks(): # An arrow object representing the current UTC time now = arrow.utcnow() # An integer representing the current unixtime (with an offset applied sourced from libweasyl.legacy) time_now = get_time() db = engine.connect() with db.begin(): locked = db.scalar("SELECT pg_try_advisory_xact_lock(0)") if not locked: return last_run = arrow.get(db.scalar("SELECT last_run FROM cron_runs")) if not last_run or now < last_run.replace(second=59): return # Recache the latest submissions # Every 2 minutes if now.minute % 2 == 0: index.recent_submissions.refresh() log.msg('refreshed recent submissions') # Recalculate recently popular submissions # Every 10 minutes if now.minute % 10 == 0: submission.select_recently_popular.refresh() log.msg('refreshed recently popular submissions') # Delete all records from views table # Every 15 minutes if now.minute % 15 == 0: db.execute("DELETE FROM views") log.msg('cleared views') # Daily at 0:00 if now.hour == 0 and now.minute == 0: # Delete password resets older than one day db.execute("DELETE FROM forgotpassword WHERE set_time < %(expiry)s", expiry=time_now - 86400) log.msg('cleared old forgotten password requests') # Delete email reset requests older than two days db.execute(""" DELETE FROM emailverify WHERE createtimestamp < (NOW() - INTERVAL '2 days') """) log.msg('cleared stale email change records') # Purge stale logincreate records older than two days db.execute(""" DELETE FROM logincreate WHERE created_at < (NOW() - INTERVAL '2 days') """) log.msg('cleared stale account creation records') db.execute("UPDATE cron_runs SET last_run = %(now)s", now=now.naive)
def insert(userid, submitid=None, charid=None, journalid=None): if submitid: content_table, id_field, target = "submission", "submitid", submitid elif charid: content_table, id_field, target = "character", "charid", charid else: content_table, id_field, target = "journal", "journalid", journalid query = d.execute("SELECT userid, settings FROM %s WHERE %s = %i", [content_table, id_field, target], options="single") if not query: raise WeasylError("TargetRecordMissing") elif userid == query[0]: raise WeasylError("CannotSelfFavorite") elif "f" in query[1] and not frienduser.check(userid, query[0]): raise WeasylError("FriendsOnly") elif ignoreuser.check(userid, query[0]): raise WeasylError("YouIgnored") elif ignoreuser.check(query[0], userid): raise WeasylError("contentOwnerIgnoredYou") insert_result = d.engine.execute( 'INSERT INTO favorite (userid, targetid, type, unixtime) ' 'VALUES (%(user)s, %(target)s, %(type)s, %(now)s) ' 'ON CONFLICT DO NOTHING', user=userid, target=d.get_targetid(submitid, charid, journalid), type='s' if submitid else 'f' if charid else 'j', now=d.get_time()) if insert_result.rowcount == 0: return # create a list of users to notify notified = set(collection.find_owners(submitid)) # conditions under which "other" should be notified def can_notify(other): other_jsonb = d.get_profile_settings(other) allow_notify = other_jsonb.allow_collection_notifs not_ignored = not ignoreuser.check(other, userid) return allow_notify and not_ignored notified = set(filter(can_notify, notified)) # always notify for own content notified.add(query[0]) for other in notified: welcome.favorite_insert(userid, submitid=submitid, charid=charid, journalid=journalid, otherid=other)
def test_link_time_field_is_updated_when_valid_token_supplied_to_function(): user_name = "test" email_addr = "*****@*****.**" user_id = db_utils.create_user(email_addr=email_addr, username=user_name) form_for_request = Bag(email=email_addr, username=user_name, day=arrow.now().day, month=arrow.now().month, year=arrow.now().year) resetpassword.request(form_for_request) pw_reset_token = d.engine.scalar("SELECT token FROM forgotpassword WHERE userid = %(id)s", id=user_id) resetpassword.prepare(pw_reset_token) link_time = d.engine.scalar("SELECT link_time FROM forgotpassword WHERE token = %(token)s", token=pw_reset_token) assert link_time >= d.get_time()
def test_true_returned_if_token_exists(): user_id = db_utils.create_user(username='******') token = "testtokentesttokentesttokentesttokentesttokentesttokentesttokentesttokentesttokentesttokentest000001" d.engine.execute(d.meta.tables["forgotpassword"].insert(), { "userid": user_id, "token": token, "set_time": d.get_time(), "link_time": 0, "address": d.get_address(), }) assert resetpassword.prepare(token)
def test_true_returned_if_token_exists(): user_id = db_utils.create_user(username='******') token = "testtokentesttokentesttokentesttokentesttokentesttokentesttokentesttokentesttokentesttokentest000001" d.engine.execute(d.meta.tables["forgotpassword"].insert(), { "userid": user_id, "token": token, "set_time": d.get_time(), "link_time": 0, "address": d.get_address(), }) assert resetpassword.checktoken(token)
def signin(userid): # Update the last login record for the user d.execute("UPDATE login SET last_login = %i WHERE userid = %i", [d.get_time(), userid]) # Log the successful login and increment the login count d.append_to_log('login.success', userid=userid, ip=d.get_address()) d.metric('increment', 'logins') # set the userid on the session sess = d.get_weasyl_session() sess.userid = userid sess.save = True
def prepare(token): # Remove records from the forgotpassword table which have been active for # more than one hour, regardless of whether or not the user has clicked the # associated link provided to them in the password reset request email, or # which have been visited but have not been removed by the password reset # script within five minutes of being visited d.engine.execute( "DELETE FROM forgotpassword WHERE set_time < %(set_cutoff)s OR link_time > 0 AND link_time < %(link_cutoff)s", set_cutoff=d.get_time() - 3600, link_cutoff=d.get_time() - 300, ) # Set the unixtime record for which the link associated with `token` was # visited by the user result = d.engine.execute( "UPDATE forgotpassword SET link_time = %(now)s WHERE token = %(token)s", now=d.get_time(), token=token, ) return result.rowcount == 1
def test_login_fails_if_user_is_suspended(): user_id = db_utils.create_user(password=raw_password, username=user_name) d.engine.execute("UPDATE login SET settings = 's' WHERE userid = %(id)s", id=user_id) release_date = d.get_time() + 60 d.engine.execute( "INSERT INTO suspension VALUES (%(id)s, %(reason)s, %(rel)s)", id=user_id, reason='test', rel=release_date) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, 'suspended')
def edit_streaming_settings(my_userid, userid, profile, set_stream=None, stream_length=0): if set_stream == 'start': try: stream_length = int(stream_length) except: raise WeasylError("streamDurationOutOfRange") if stream_length < 1 or stream_length > 360: raise WeasylError("streamDurationOutOfRange") if set_stream == 'start' and not profile.stream_url: raise WeasylError("streamLocationNotSet") # unless we're specifically still streaming, clear the user_streams record if set_stream != 'still': d.execute("DELETE FROM user_streams WHERE userid = %i", [userid]) settings_flag = '' stream_status = None # if we're starting to stream, update user_streams to reflect that if set_stream == 'start': now = d.get_time() stream_end = now + stream_length * 60 # stream_length is minutes; we need seconds d.execute("INSERT INTO user_streams VALUES (%i, %i, %i)", [userid, now, stream_end]) stream_status = 'n' # if we're going to stream later, update profile.settings to reflect that elif set_stream == 'later': settings_flag = stream_status = 'l' # if stream_status is None, any rows in `welcome` will get cleared. but, if # the user is still streaming, that shouldn't happen. otherwise, `welcome` # will get updated with the current stream state. if set_stream != 'still': welcome.stream_insert(userid, stream_status) d.execute( "UPDATE profile " "SET (stream_text, stream_url, settings) = ('%s', '%s', REGEXP_REPLACE(settings, '[nli]', '') || '%s') " "WHERE userid = %i", [profile.stream_text, profile.stream_url, settings_flag, userid]) if my_userid != userid: from weasyl import moderation note_body = ( '- Stream url: %s\n' '- Stream description: %s\n' '- Stream status: %s' % (profile.stream_url, profile.stream_text, STREAMING_ACTION_MAP[set_stream])) moderation.note_about(my_userid, userid, 'Streaming settings updated:', note_body)
def reset(form): from weasyl import login # Raise an exception if `password` does not enter `passcheck` (indicating # that the user mistyped one of the fields) or if `password` does not meet # the system's password security requirements if form.password != form.passcheck: raise WeasylError("passwordMismatch") elif not login.password_secure(form.password): raise WeasylError("passwordInsecure") # Select the user information and record data from the forgotpassword table # pertaining to `token`, requiring that the link associated with the record # be visited no more than five minutes prior; if the forgotpassword record is # not found or does not meet this requirement, raise an exception query = d.engine.execute(""" SELECT lo.userid, lo.login_name, lo.email, fp.link_time, fp.address FROM login lo INNER JOIN userinfo ui USING (userid) INNER JOIN forgotpassword fp USING (userid) WHERE fp.token = %(token)s AND fp.link_time > %(cutoff)s """, token=form.token, cutoff=d.get_time() - 300).first() if not query: raise WeasylError("forgotpasswordRecordMissing") USERID, USERNAME, EMAIL, LINKTIME, ADDRESS = query # Check `username` and `email` against known correct values and raise an # exception if there is a mismatch if emailer.normalize_address( form.email) != emailer.normalize_address(EMAIL): raise WeasylError("emailIncorrect") elif d.get_sysname(form.username) != USERNAME: raise WeasylError("usernameIncorrect") elif d.get_address() != ADDRESS: raise WeasylError("addressInvalid") # Update the authbcrypt table with a new password hash d.engine.execute( 'INSERT INTO authbcrypt (userid, hashsum) VALUES (%(user)s, %(hash)s) ' 'ON CONFLICT (userid) DO UPDATE SET hashsum = %(hash)s', user=USERID, hash=login.passhash(form.password)) d.engine.execute("DELETE FROM forgotpassword WHERE token = %(token)s", token=form.token)
def run_periodic_tasks(): web.ctx.clear() now = arrow.utcnow() time_now = get_time() db = connect().connection() with db.begin(): locked = db.scalar("SELECT pg_try_advisory_xact_lock(0)") if not locked: return last_run = arrow.get(db.scalar("SELECT last_run FROM cron_runs")) if not last_run or now < last_run.replace(seconds=59): return # Recache the latest submissions # Every 2 minutes if now.minute % 2 == 0: index.recent_submissions.refresh() log.msg('refreshed recent submissions') # Recache the active user counts # Every 5 minutes if now.minute % 5 == 0: active_users.refresh() log.msg('refreshed active user counts') # Recalculate recently popular submissions # Every 10 minutes if now.minute % 10 == 0: submission.select_recently_popular.refresh() log.msg('refreshed recently popular submissions') # Delete all records from contentview table # Every 15 minutes if now.minute % 15 == 0: db.execute("DELETE FROM views") log.msg('cleared views') # Delete password resets older than one day # Daily at 0:00 if now.hour == 0 and now.minute == 0: db.execute( "DELETE FROM forgotpassword WHERE set_time < %(expiry)s", expiry=time_now - 86400) log.msg('cleared old forgotten password requests') db.execute("UPDATE cron_runs SET last_run = %(now)s", now=now.naive)
def reset(form): import login # Raise an exception if `password` does not enter `passcheck` (indicating # that the user mistyped one of the fields) or if `password` does not meet # the system's password security requirements if form.password != form.passcheck: raise WeasylError("passwordMismatch") elif not login.password_secure(form.password): raise WeasylError("passwordInsecure") # Select the user information and record data from the forgotpassword table # pertaining to `token`, requiring that the link associated with the record # be visited no more than five minutes prior; if the forgotpassword record is # not found or does not meet this requirement, raise an exception query = d.execute(""" SELECT lo.userid, lo.login_name, lo.email, fp.link_time, fp.address FROM login lo INNER JOIN userinfo ui USING (userid) INNER JOIN forgotpassword fp USING (userid) WHERE fp.token = '%s' AND fp.link_time > %i """, [form.token, d.get_time() - 300], options="single") if not query: raise WeasylError("forgotpasswordRecordMissing") USERID, USERNAME, EMAIL, LINKTIME, ADDRESS = query # Check `username` and `email` against known correct values and raise an # exception if there is a mismatch if emailer.normalize_address(form.email) != emailer.normalize_address(EMAIL): raise WeasylError("emailIncorrect") elif d.get_sysname(form.username) != USERNAME: raise WeasylError("usernameIncorrect") elif d.get_address() != ADDRESS: raise WeasylError("addressInvalid") # Update the authbcrypt table with a new password hash """ TODO TEMPORARY """ try: d.execute("INSERT INTO authbcrypt VALUES (%i, '')", [USERID]) except: pass d.execute("UPDATE authbcrypt SET hashsum = '%s' WHERE userid = %i", [login.passhash(form.password), USERID]) d.execute("DELETE FROM forgotpassword WHERE token = '%s'", [form.token])
def request(form): token = security.generate_key(100) email = emailer.normalize_address(form.email) username = d.get_sysname(form.username) # Determine the user associated with `username`; if the user is not found, # raise an exception user = d.engine.execute( "SELECT userid, email FROM login WHERE login_name = %(username)s", username=username).first() if not user: raise WeasylError("loginRecordMissing") # Check the user's email address against the provided e-mail address, # raising an exception if there is a mismatch if email != emailer.normalize_address(user.email): raise WeasylError("emailInvalid") # Insert a record into the forgotpassword table for the user, # or update an existing one now = d.get_time() address = d.get_address() try: d.engine.execute( "INSERT INTO forgotpassword (userid, token, set_time, address)" " VALUES (%(id)s, %(token)s, %(time)s, %(address)s)", id=user.userid, token=token, time=now, address=address) except IntegrityError: # An IntegrityError will probably indicate that a password reset request # already exists and that the existing row should be updated. If the update # doesn't find anything, though, the original error should be re-raised. result = d.engine.execute(""" UPDATE forgotpassword SET token = %(token)s, set_time = %(time)s, address = %(address)s WHERE userid = %(id)s """, id=user.userid, token=token, time=now, address=address) if result.rowcount != 1: raise # Generate and send an email to the user containing a password reset link emailer.append([email], None, "Weasyl Password Recovery", d.render("email/reset_password.html", [token]))
def pending_accept(userid, submissions): if not submissions: return d.engine.execute( "UPDATE collection SET " "unixtime = %(now)s, " "settings = REGEXP_REPLACE(settings, '[pr]', '') " "WHERE settings ~ '[pr]' " "AND (submitid, userid) = ANY (%(submissions)s)", submissions=submissions, now=d.get_time()) for s in submissions: welcome.collection_insert(s[1], s[0]) welcome.collectrequest_remove(userid, s[1], s[0]) d._page_header_info.invalidate(userid)
def select_profile(userid, avatar=False, banner=False, propic=False, images=False, commish=True, viewer=None): query = d.execute( """ SELECT pr.username, pr.full_name, pr.catchphrase, pr.unixtime, pr.profile_text, pr.settings, pr.stream_url, pr.config, pr.stream_text, lo.settings, us.end_time FROM profile pr INNER JOIN login lo USING (userid) LEFT JOIN user_streams us USING (userid) WHERE userid = %i """, [userid], ["single"]) if not query: raise WeasylError('RecordMissing') streaming_status = "stopped" if query[6]: # profile.stream_url if query[10] > d.get_time(): # user_streams.end_time streaming_status = "started" elif 'l' in query[5]: streaming_status = "later" return { "userid": userid, "user_media": media.get_user_media(userid), "username": query[0], "full_name": query[1], "catchphrase": query[2], "unixtime": query[3], "profile_text": query[4], "settings": query[5], "stream_url": query[6], "stream_text": query[8], "config": query[7], "show_favorites_bar": "u" not in query[7] and "v" not in query[7], "show_favorites_tab": userid == viewer or "v" not in query[7], "commish_slots": 0, "banned": "b" in query[9], "suspended": "s" in query[9], "streaming_status": streaming_status, }
def run_periodic_tasks(): web.ctx.clear() now = arrow.utcnow() time_now = get_time() db = connect().connection() with db.begin(): locked = db.scalar("SELECT pg_try_advisory_xact_lock(0)") if not locked: return last_run = arrow.get(db.scalar("SELECT last_run FROM cron_runs")) if not last_run or now < last_run.replace(seconds=59): return # Recache the latest submissions # Every 2 minutes if now.minute % 2 == 0: index.recent_submissions.refresh() log.msg('refreshed recent submissions') # Recache the active user counts # Every 5 minutes if now.minute % 5 == 0: active_users.refresh() log.msg('refreshed active user counts') # Recalculate recently popular submissions # Every 10 minutes if now.minute % 10 == 0: submission.select_recently_popular.refresh() log.msg('refreshed recently popular submissions') # Delete all records from contentview table # Every 15 minutes if now.minute % 15 == 0: db.execute("DELETE FROM views") log.msg('cleared views') # Delete password resets older than one day # Daily at 0:00 if now.hour == 0 and now.minute == 0: db.execute("DELETE FROM forgotpassword WHERE set_time < %(expiry)s", expiry=time_now - 86400) log.msg('cleared old forgotten password requests') db.execute("UPDATE cron_runs SET last_run = %(now)s", now=now.naive)
def verify(db, userid, token): # Select purchased terms terms = define.execute(db, "SELECT terms FROM premiumpurchase WHERE token = '%s'", [token], ["element"]) if not terms: raise error.WeasylError("tokenInvalid") # Select current terms current = define.execute(db, "SELECT terms FROM userpremium WHERE userid = %i", [userid], ["element"]) # Update premium status if current: define.execute(db, "UPDATE userpremium SET terms = terms + %i WHERE userid = %i", [terms, userid]) else: define.execute(db, "INSERT INTO userpremium VALUES (%i, %i, %i)", [userid, define.get_time(), terms]) define.execute(db, "UPDATE profile SET config = config || 'd' WHERE userid = %i AND config !~ 'd'", [userid]) define.execute(db, "DELETE FROM premiumpurchase WHERE token = '%s'", [token])
def edit(userid, form): commentid = d.get_int(form.commentid) if form.feature not in ["submit", "char", "journal"]: raise WeasylError("Unexpected") query = d.execute("SELECT userid, unixtime FROM %scomment WHERE commentid = %i AND settings !~ 'h'", [form.feature, commentid], options="single") if not query: raise WeasylError("RecordMissing") elif userid != query[0] and userid not in staff.MODS: raise WeasylError("InsufficientPermissions") elif query[1] < d.get_time() - 600: raise WeasylError("TimeLimitExceeded") d.execute("UPDATE %scomment SET content = '%s' WHERE commentid = %i", [form.feature, form.content.strip(), commentid])
def request(userid, submitid, otherid): query = d.engine.execute( "SELECT userid, rating, settings " "FROM submission WHERE submitid = %(submission)s", submission=submitid).first() rating = d.get_rating(userid) if not query or "h" in query.settings: raise WeasylError("Unexpected") if otherid != query.userid: raise WeasylError("Unexpected") # not checking for blocktags here because if you want to collect # something with a tag you don't like that's your business if rating < query.rating: raise WeasylError("RatingExceeded") if "f" in query.settings: raise WeasylError("collectionUnacceptable") if ignoreuser.check(otherid, userid): raise WeasylError("IgnoredYou") if ignoreuser.check(userid, otherid): raise WeasylError("YouIgnored") if _check_throttle(userid, otherid): raise WeasylError("collectionThrottle") settings = d.get_profile_settings(otherid) if not settings.allow_collection_requests: raise WeasylError("Unexpected") request_settings = "r" try: d.engine.execute( "INSERT INTO collection (userid, submitid, unixtime, settings) " "VALUES (%(userid)s, %(submitid)s, %(now)s, %(settings)s)", userid=userid, submitid=submitid, now=d.get_time(), settings=request_settings) except PostgresError: raise WeasylError("collectionExists") welcome.collectrequest_insert(userid, otherid, submitid)
def signin(request, userid, ip_address=None, user_agent=None): # Update the last login record for the user d.execute("UPDATE login SET last_login = %i WHERE userid = %i", [d.get_time(), userid]) # Log the successful login and increment the login count d.append_to_log('login.success', userid=userid, ip=d.get_address()) d.metric('increment', 'logins') # set the userid on the session sess = create_session(userid) sess.ip_address = ip_address sess.user_agent_id = get_user_agent_id(user_agent) sess.create = True if not isinstance(request.weasyl_session, GuestSession): request.pg_connection.delete(request.weasyl_session) request.pg_connection.flush() request.weasyl_session = sess
def select_profile(userid, viewer=None): query = d.engine.execute(""" SELECT pr.username, pr.full_name, pr.catchphrase, pr.created_at, pr.profile_text, pr.settings, pr.stream_url, pr.config, pr.stream_text, us.end_time FROM profile pr INNER JOIN login lo USING (userid) LEFT JOIN user_streams us USING (userid) WHERE userid = %(user)s """, user=userid).first() if not query: raise WeasylError('userRecordMissing') is_banned, is_suspended = d.get_login_settings(userid) streaming_status = "stopped" if query[6]: # profile.stream_url if 'l' in query[5]: streaming_status = "later" elif query[9] is not None and query[9] > d.get_time( ): # user_streams.end_time streaming_status = "started" return { "userid": userid, "user_media": media.get_user_media(userid), "username": query[0], "full_name": query[1], "catchphrase": query[2], "unixtime": query[3], "profile_text": query[4], "settings": query[5], "stream_url": query[6], "stream_text": query[8], "config": query[7], "show_favorites_bar": "u" not in query[7] and "v" not in query[7], "show_favorites_tab": userid == viewer or "v" not in query[7], "commish_slots": 0, "banned": is_banned, "suspended": is_suspended, "streaming_status": streaming_status, }
def edit(userid, form): commentid = d.get_int(form.commentid) if form.feature not in ["submit", "char", "journal"]: raise WeasylError("Unexpected") query = d.execute( "SELECT userid, unixtime FROM %scomment WHERE commentid = %i AND settings !~ 'h'", [form.feature, commentid], options="single") if not query: raise WeasylError("RecordMissing") elif userid != query[0] and userid not in staff.MODS: raise WeasylError("InsufficientPermissions") elif query[1] < d.get_time() - 600: raise WeasylError("TimeLimitExceeded") d.execute("UPDATE %scomment SET content = '%s' WHERE commentid = %i", [form.feature, form.content.strip(), commentid])
def select_profile(userid, avatar=False, banner=False, propic=False, images=False, commish=True, viewer=None): query = d.execute(""" SELECT pr.username, pr.full_name, pr.catchphrase, pr.unixtime, pr.profile_text, pr.settings, pr.stream_url, pr.config, pr.stream_text, lo.settings, us.end_time FROM profile pr INNER JOIN login lo USING (userid) LEFT JOIN user_streams us USING (userid) WHERE userid = %i """, [userid], ["single"]) if not query: raise WeasylError('RecordMissing') streaming_status = "stopped" if query[6]: # profile.stream_url if query[10] > d.get_time(): # user_streams.end_time streaming_status = "started" elif 'l' in query[5]: streaming_status = "later" return { "userid": userid, "user_media": media.get_user_media(userid), "username": query[0], "full_name": query[1], "catchphrase": query[2], "unixtime": query[3], "profile_text": query[4], "settings": query[5], "stream_url": query[6], "stream_text": query[8], "config": query[7], "show_favorites_bar": "u" not in query[7] and "v" not in query[7], "show_favorites_tab": userid == viewer or "v" not in query[7], "commish_slots": 0, "banned": "b" in query[9], "suspended": "s" in query[9], "streaming_status": streaming_status, }
def send(userid, form): form.title = form.title.strip() form.content = form.content.strip() if not form.content: raise WeasylError("contentInvalid") elif not form.title: raise WeasylError("titleInvalid") elif len(form.title) > 100: raise WeasylError("titleTooLong") users = set(i for i in d.get_userid_list(form.recipient) if i != userid) users.difference_update( d.execute("SELECT userid FROM ignoreuser WHERE otherid = %i", [userid], options="within")) users.difference_update( d.execute("SELECT otherid FROM ignoreuser WHERE userid = %i", [userid], options="within")) if not users: raise WeasylError("recipientInvalid") configs = d.execute( "SELECT userid, config FROM profile WHERE userid IN %s", [d.sql_number_list(list(users))]) if userid not in staff.MODS: # Staff notes only users.difference_update(j[0] for j in configs if "y" in j[1]) # Friend notes only users.difference_update(j[0] for j in configs if "z" in j[1] and not frienduser.check(userid, j[0])) if not users: raise WeasylError("recipientInvalid") elif len(users) > 10: raise WeasylError("recipientExcessive") argv = [] unixtime = d.get_time() statement = ["INSERT INTO message (userid, otherid, title, content, unixtime) VALUES"] for i in users: argv.extend([form.title if form.title else "None", form.content]) statement.append(" (%i, %i, '%%s', '%%s', %i)," % (userid, i, unixtime)) d._page_header_info.invalidate(i) statement[-1] = statement[-1][:-1] d.execute("".join(statement), argv) if form.mod_copy and userid in staff.MODS: mod_content = ( '## The following message was sent as a note to the user.\n\n### %s\n\n%s' % ( form.title, form.content)) if form.staff_note: mod_content = '%s\n\n%s' % (form.staff_note, mod_content) now = arrow.utcnow() mod_copies = [] for target in users: mod_copies.append({ 'userid': userid, 'target_user': target, 'unixtime': now, 'settings': 's', 'content': mod_content, }) d.engine.execute( d.meta.tables['comments'].insert() .values(mod_copies))
def authenticate_bcrypt(username, password, request, ip_address=None, user_agent=None): """ Return a result tuple of the form (userid, error); `error` is None if the login was successful. Pass None as the `request` to authenticate a user without creating a new session. :param username: The username of the user attempting authentication. :param password: The user's claimed password to check against the stored hash. :param ip_address: The address requesting authentication. :param user_agent: The user agent string of the submitting client. Possible errors are: - "invalid" - "unexpected" - "address" - "banned" - "suspended" - "2fa" - Indicates the user has opted-in to 2FA. Additional authentication required. """ # Check that the user entered potentially valid values for `username` and # `password` before attempting to authenticate them if not username or not password: return 0, "invalid" # Select the authentication data necessary to check that the the user-entered # credentials are valid query = d.execute("SELECT ab.userid, ab.hashsum, lo.settings, lo.twofa_secret FROM authbcrypt ab" " RIGHT JOIN login lo USING (userid)" " WHERE lo.login_name = '%s'", [d.get_sysname(username)], ["single"]) if not query: return 0, "invalid" USERID, HASHSUM, SETTINGS, TWOFA = query HASHSUM = HASHSUM.encode('utf-8') d.metric('increment', 'attemptedlogins') unicode_success = bcrypt.checkpw(password.encode('utf-8'), HASHSUM) if not unicode_success and not bcrypt.checkpw(d.plaintext(password).encode('utf-8'), HASHSUM): # Log the failed login attempt in a security log if the account the user # attempted to log into is a privileged account if USERID in staff.MODS: d.append_to_log('login.fail', userid=USERID, ip=d.get_address()) d.metric('increment', 'failedlogins') # Return a zero userid and an error code (indicating the entered password # was incorrect) return 0, "invalid" elif "b" in SETTINGS: # Return the proper userid and an error code (indicating the user's account # has been banned) return USERID, "banned" elif "s" in SETTINGS: suspension = moderation.get_suspension(USERID) if d.get_time() > suspension.release: d.execute("UPDATE login SET settings = REPLACE(settings, 's', '') WHERE userid = %i", [USERID]) d.execute("DELETE FROM suspension WHERE userid = %i", [USERID]) d.get_login_settings.invalidate(USERID) else: # Return the proper userid and an error code (indicating the user's # account has been temporarily suspended) return USERID, "suspended" # Attempt to create a new session if this is a request to log in, then log the signin # if it succeeded. if request is not None: # If the user's record has ``login.twofa_secret`` set (not nulled), return that password authentication succeeded. if TWOFA: if not isinstance(request.weasyl_session, GuestSession): request.pg_connection.delete(request.weasyl_session) request.pg_connection.flush() request.weasyl_session = create_session(None) request.weasyl_session.additional_data = {} return USERID, "2fa" else: signin(request, USERID, ip_address=ip_address, user_agent=user_agent) status = None if not unicode_success: # Oops; the user's password was stored badly, but they did successfully authenticate. status = 'unicode-failure' # Either way, authentication succeeded, so return the userid and a status. return USERID, status
def insert(userid, submitid=None, charid=None, journalid=None, parentid=None, content=None): if not submitid and not charid and not journalid: raise WeasylError("Unexpected") elif not content: raise WeasylError("commentInvalid") # Determine indent and parentuserid if parentid: query = d.execute("SELECT userid, indent FROM %s WHERE commentid = %i", ["comments" if submitid else "charcomment" if charid else "journalcomment", parentid], options="single") if not query: raise WeasylError("Unexpected") indent = query[1] + 1 parentuserid = query[0] else: indent = 0 parentuserid = None # Determine otherid otherid = d.execute("SELECT userid FROM %s WHERE %s = %i AND settings !~ 'h'", ["submission", "submitid", submitid] if submitid else ["character", "charid", charid] if charid else ["journal", "journalid", journalid], options="element") # Check permissions if not otherid: raise WeasylError("submissionRecordMissing") elif ignoreuser.check(otherid, userid): raise WeasylError("pageOwnerIgnoredYou") elif ignoreuser.check(userid, otherid): raise WeasylError("youIgnoredPageOwner") elif parentuserid and ignoreuser.check(parentuserid, userid): raise WeasylError("replyRecipientIgnoredYou") elif parentuserid and ignoreuser.check(userid, parentuserid): raise WeasylError("youIgnoredReplyRecipient") # Create comment if submitid: co = d.meta.tables['comments'] db = d.connect() commentid = db.scalar( co.insert() .values(userid=userid, target_sub=submitid, parentid=parentid or None, content=content, unixtime=arrow.utcnow(), indent=indent) .returning(co.c.commentid)) else: commentid = d.execute( "INSERT INTO %s (userid, targetid, parentid, " "content, unixtime, indent) VALUES (%i, %i, %i, '%s', %i, %i) RETURNING " "commentid", [ "charcomment" if charid else "journalcomment", userid, d.get_targetid(submitid, charid, journalid), parentid, content, d.get_time(), indent ], options="element") # Create notification if parentid and (userid != parentuserid): welcome.commentreply_insert(userid, commentid, parentuserid, parentid, submitid, charid, journalid) elif not parentid: # build a list of people this comment should notify # circular imports are cool and fun from weasyl.collection import find_owners notified = set(find_owners(submitid)) # check to see who we should deliver comment notifications to def can_notify(other): other_jsonb = d.get_profile_settings(other) allow_notify = other_jsonb.allow_collection_notifs ignored = ignoreuser.check(other, userid) return allow_notify and not ignored notified = set(filter(can_notify, notified)) # always give notification on own content notified.add(otherid) # don't give me a notification for my own comment notified.discard(userid) for other in notified: welcome.comment_insert(userid, commentid, other, submitid, charid, journalid) d.metric('increment', 'comments') return commentid
def authenticate_bcrypt(username, password, session=True): """ Return a result tuple of the form (userid, error); `error` is None if the login was successful. Pass `session` as False to authenticate a user without creating a new session. Possible errors are: - "invalid" - "unexpected" - "address" - "banned" - "suspended" """ # Check that the user entered potentially valid values for `username` and # `password` before attempting to authenticate them if not username or not password: return 0, "invalid" # Select the authentication data necessary to check that the the user-entered # credentials are valid query = d.execute("SELECT ab.userid, ab.hashsum, lo.settings FROM authbcrypt ab" " RIGHT JOIN login lo USING (userid)" " WHERE lo.login_name = '%s'", [d.get_sysname(username)], ["single"]) if not query: return 0, "invalid" USERID, HASHSUM, SETTINGS = query HASHSUM = HASHSUM.encode('utf-8') d.metric('increment', 'attemptedlogins') unicode_success = bcrypt.checkpw(password.encode('utf-8'), HASHSUM) if not unicode_success and not bcrypt.checkpw(d.plaintext(password).encode('utf-8'), HASHSUM): # Log the failed login attempt in a security log if the account the user # attempted to log into is a privileged account if USERID in staff.MODS: d.append_to_log('login.fail', userid=USERID, ip=d.get_address()) d.metric('increment', 'failedlogins') # Return a zero userid and an error code (indicating the entered password # was incorrect) return 0, "invalid" elif "b" in SETTINGS: # Return the proper userid and an error code (indicating the user's account # has been banned) return USERID, "banned" elif "s" in SETTINGS: suspension = moderation.get_suspension(USERID) if d.get_time() > suspension.release: d.execute("UPDATE login SET settings = REPLACE(settings, 's', '') WHERE userid = %i", [USERID]) d.execute("DELETE FROM suspension WHERE userid = %i", [USERID]) d.get_login_settings.invalidate(USERID) else: # Return the proper userid and an error code (indicating the user's # account has been temporarily suspended) return USERID, "suspended" # Attempt to create a new session if `session` is True, then log the signin # if it succeeded. if session: signin(USERID) d.append_to_log('login.success', userid=USERID, ip=d.get_address()) d.metric('increment', 'logins') status = None if not unicode_success: # Oops; the user's password was stored badly, but they did successfully authenticate. status = 'unicode-failure' # Either way, authentication succeeded, so return the userid and a status. return USERID, status
def associate(userid, tags, submitid=None, charid=None, journalid=None, preferred_tags_userid=None, optout_tags_userid=None): """ Associates searchtags with a content item. Parameters: userid: The userid of the user associating tags tags: A set of tags submitid: The ID number of a submission content item to associate ``tags`` to. (default: None) charid: The ID number of a character content item to associate ``tags`` to. (default: None) journalid: The ID number of a journal content item to associate ``tags`` to. (default: None) preferred_tags_userid: The ID number of a user to associate ``tags`` to for Preferred tags. (default: None) optout_tags_userid: The ID number of a user to associate ``tags`` to for Opt-Out tags. (default: None) Returns: A dict containing two elements. 1) ``add_failure_restricted_tags``, which contains a space separated string of tag titles which failed to be added to the content item due to the user or global restricted tag lists; and 2) ``remove_failure_owner_set_tags``, which contains a space separated string of tag titles which failed to be removed from the content item due to the owner of the aforementioned item prohibiting users from removing tags set by the content owner. If an element does not have tags, the element is set to None. If neither elements are set, the function returns None. """ targetid = d.get_targetid(submitid, charid, journalid) # Assign table, feature, ownerid if submitid: table, feature = "searchmapsubmit", "submit" ownerid = d.get_ownerid(submitid=targetid) elif charid: table, feature = "searchmapchar", "char" ownerid = d.get_ownerid(charid=targetid) elif journalid: table, feature = "searchmapjournal", "journal" ownerid = d.get_ownerid(journalid=targetid) elif preferred_tags_userid: table, feature = "artist_preferred_tags", "user" targetid = ownerid = preferred_tags_userid elif optout_tags_userid: table, feature = "artist_optout_tags", "user" targetid = ownerid = optout_tags_userid else: raise WeasylError("Unexpected") # Check permissions and invalid target if not ownerid: raise WeasylError("TargetRecordMissing") elif userid != ownerid and ("g" in d.get_config(userid) or preferred_tags_userid or optout_tags_userid): # disallow if user is forbidden from tagging, or trying to set artist tags on someone other than themselves raise WeasylError("InsufficientPermissions") elif ignoreuser.check(ownerid, userid): raise WeasylError("contentOwnerIgnoredYou") # Determine previous tagids, titles, and settings existing = d.engine.execute( "SELECT tagid, title, settings FROM {} INNER JOIN searchtag USING (tagid) WHERE targetid = %(target)s".format(table), target=targetid).fetchall() # Retrieve tag titles and tagid pairs, for new (if any) and existing tags query = add_and_get_searchtags(tags) existing_tagids = {t.tagid for t in existing} entered_tagids = {t.tagid for t in query} # Assign added and removed added = entered_tagids - existing_tagids removed = existing_tagids - entered_tagids # enforce the limit on artist preference tags if preferred_tags_userid and (len(added) - len(removed) + len(existing)) > MAX_PREFERRED_TAGS: raise WeasylError("tooManyPreferenceTags") # Track which tags fail to be added or removed to later notify the user (Note: These are tagids at this stage) add_failure_restricted_tags = None remove_failure_owner_set_tags = None # If the modifying user is not the owner of the object, and is not staff, check user/global restriction lists if userid != ownerid and userid not in staff.MODS: user_rtags = set(query_user_restricted_tags(ownerid)) global_rtags = set(query_global_restricted_tags()) add_failure_restricted_tags = remove_restricted_tags(user_rtags | global_rtags, query) added -= add_failure_restricted_tags if len(add_failure_restricted_tags) == 0: add_failure_restricted_tags = None # Check removed artist tags if not can_remove_tags(userid, ownerid): existing_artist_tags = {t.tagid for t in existing if 'a' in t.settings} remove_failure_owner_set_tags = removed & existing_artist_tags removed.difference_update(existing_artist_tags) entered_tagids.update(existing_artist_tags) # Submission items use a different method of tag protection for artist tags; ignore them if submitid or len(remove_failure_owner_set_tags) == 0: remove_failure_owner_set_tags = None # Remove tags if removed: d.engine.execute( "DELETE FROM {} WHERE targetid = %(target)s AND tagid = ANY (%(removed)s)".format(table), target=targetid, removed=list(removed)) if added: d.engine.execute( "INSERT INTO {} SELECT tag, %(target)s FROM UNNEST (%(added)s) AS tag".format(table), target=targetid, added=list(added)) # preference/optout tags can only be set by the artist, so this settings column does not apply if userid == ownerid and not (preferred_tags_userid or optout_tags_userid): d.execute( "UPDATE %s SET settings = settings || 'a' WHERE targetid = %i AND tagid IN %s", [table, targetid, d.sql_number_list(list(added))]) if submitid: d.engine.execute( 'INSERT INTO submission_tags (submitid, tags) VALUES (%(submission)s, %(tags)s) ' 'ON CONFLICT (submitid) DO UPDATE SET tags = %(tags)s', submission=submitid, tags=list(entered_tagids)) db = d.connect() db.execute( d.meta.tables['tag_updates'].insert() .values(submitid=submitid, userid=userid, added=tag_array(added), removed=tag_array(removed))) if userid != ownerid: welcome.tag_update_insert(ownerid, submitid) files.append( "%stag.%s.%s.log" % (m.MACRO_SYS_LOG_PATH, feature, d.get_timestamp()), "-%sID %i -T %i -UID %i -X %s\n" % (feature[0].upper(), targetid, d.get_time(), userid, " ".join(tags))) # Return dict with any tag titles as a string that failed to be added or removed if add_failure_restricted_tags or remove_failure_owner_set_tags: if add_failure_restricted_tags: add_failure_restricted_tags = " ".join({tag.title for tag in query if tag.tagid in add_failure_restricted_tags}) if remove_failure_owner_set_tags: remove_failure_owner_set_tags = " ".join({tag.title for tag in existing if tag.tagid in remove_failure_owner_set_tags}) return {"add_failure_restricted_tags": add_failure_restricted_tags, "remove_failure_owner_set_tags": remove_failure_owner_set_tags} else: return None
def associate(userid, tags, submitid=None, charid=None, journalid=None): targetid = d.get_targetid(submitid, charid, journalid) # Assign table, feature, ownerid if submitid: table, feature = "searchmapsubmit", "submit" ownerid = d.get_ownerid(submitid=targetid) elif charid: table, feature = "searchmapchar", "char" ownerid = d.get_ownerid(charid=targetid) else: table, feature = "searchmapjournal", "journal" ownerid = d.get_ownerid(journalid=targetid) # Check permissions and invalid target if not ownerid: raise WeasylError("TargetRecordMissing") elif userid != ownerid and "g" in d.get_config(userid): raise WeasylError("InsufficientPermissions") elif ignoreuser.check(ownerid, userid): raise WeasylError("contentOwnerIgnoredYou") # Determine previous tags existing = d.engine.execute( "SELECT tagid, settings FROM {} WHERE targetid = %(target)s".format(table), target=targetid).fetchall() # Determine tag titles and tagids query = d.engine.execute( "SELECT tagid, title FROM searchtag WHERE title = ANY (%(tags)s)", tags=list(tags)).fetchall() newtags = list(tags - {x.title for x in query}) if newtags: query.extend( d.engine.execute( "INSERT INTO searchtag (title) SELECT * FROM UNNEST (%(newtags)s) AS title RETURNING tagid, title", newtags=newtags ).fetchall()) existing_tagids = {t.tagid for t in existing} entered_tagids = {t.tagid for t in query} # Assign added and removed added = entered_tagids - existing_tagids removed = existing_tagids - entered_tagids # Check removed artist tags if not can_remove_tags(userid, ownerid): existing_artist_tags = {t.tagid for t in existing if 'a' in t.settings} removed.difference_update(existing_artist_tags) entered_tagids.update(existing_artist_tags) # Remove tags if removed: d.engine.execute( "DELETE FROM {} WHERE targetid = %(target)s AND tagid = ANY (%(removed)s)".format(table), target=targetid, removed=list(removed)) if added: d.engine.execute( "INSERT INTO {} SELECT tag, %(target)s FROM UNNEST (%(added)s) AS tag".format(table), target=targetid, added=list(added)) if userid == ownerid: d.execute( "UPDATE %s SET settings = settings || 'a' WHERE targetid = %i AND tagid IN %s", [table, targetid, d.sql_number_list(list(added))]) if submitid: d.engine.execute( 'INSERT INTO submission_tags (submitid, tags) VALUES (%(submission)s, %(tags)s) ' 'ON CONFLICT (submitid) DO UPDATE SET tags = %(tags)s', submission=submitid, tags=list(entered_tagids)) db = d.connect() db.execute( d.meta.tables['tag_updates'].insert() .values(submitid=submitid, userid=userid, added=tag_array(added), removed=tag_array(removed))) if userid != ownerid: welcome.tag_update_insert(ownerid, submitid) files.append( "%stag.%s.%s.log" % (m.MACRO_SYS_LOG_PATH, feature, d.get_timestamp()), "-%sID %i -T %i -UID %i -X %s\n" % (feature[0].upper(), targetid, d.get_time(), userid, " ".join(tags)))