diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index a14a911c..c7e259d4 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -577,15 +577,28 @@ def POST_resendconfirmation(self, res): VModhash(), curpass = nop('curpass'), email = ValidEmail("email"), + realname = VRealName("real_name"), newpass = nop("newpass"), verpass = nop("verpass"), password = VPassword(['newpass', 'verpass'])) - def POST_update(self, res, email, curpass, password, newpass, verpass): + def POST_update(self, res, email, curpass, realname, password, newpass, verpass): res._update('status', innerHTML='') if res._chk_error(errors.WRONG_PASSWORD): res._focus('curpass') res._update('curpass', value='') return + + if res._chk_error(errors.BAD_REALNAME_CHARS) or res._chk_error(errors.BAD_REALNAME_LONG): + res._focus('real_name') + if realname is None and c.user.real_name is not None: + c.user.real_name = None + c.user._commit() + res._update('status', innerHTML=_('Your real name has been removed')) + elif realname: + c.user.real_name = realname + c.user._commit() + res._update('status', innerHTML=_('Your real name has been updated')) + updated = False if res._chk_error(errors.BAD_EMAIL): res._focus('email') diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 0d4de246..c4e8e245 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -1,3 +1,4 @@ +# coding: utf8 # The contents of this file are subject to the Common Public Attribution # License Version 1.0. (the "License"); you may not use this file except in # compliance with the License. You may obtain a copy of the License at @@ -6,16 +7,16 @@ # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. -# +# # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. -# +# # The Original Code is Reddit. -# +# # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is CondeNet, Inc. -# +# # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ @@ -23,7 +24,7 @@ from pylons.i18n import _ from pylons.controllers.util import abort from r2.lib import utils, captcha -from r2.lib.filters import unkeep_space, websafe, _force_utf8, _force_ascii +from r2.lib.filters import unkeep_space, websafe, _force_utf8, _force_ascii, _force_unicode from r2.lib.wikipagecached import WikiPageCached from r2.lib.db.operators import asc, desc from r2.config import cache @@ -70,11 +71,11 @@ def newfn(self, *a, **env): try: for validator in simple_vals: validator(env) - + kw = self.build_arg_list(fn, env) for var, validator in param_vals.iteritems(): kw[var] = validator(env) - + return fn(self, *a, **kw) except UserRequiredException: @@ -109,7 +110,7 @@ def error(self, e = None): if not e: e = self._error if e: c.errors.add(e) - + def run(self, item): if not item: self.error() @@ -134,7 +135,7 @@ def error(self, e = None): if not e: e = self._error if e: c.errors.add(e) - + def run(self, item): if not item: self.error() @@ -156,7 +157,7 @@ class VLink(Validator): def __init__(self, param, redirect = True, *a, **kw): Validator.__init__(self, param, *a, **kw) self.redirect = redirect - + def run(self, link_id): if link_id: try: @@ -170,7 +171,7 @@ def run(self, link_id): class VCommentFullName(Validator): valid_re = re.compile(Comment._type_prefix + str(Comment._type_id) + r'_([0-9a-z]+)$') - + def run(self, thing_fullname): if thing_fullname: match = self.valid_re.match(thing_fullname) @@ -203,7 +204,7 @@ def __init__(self, param, redirect = True, *a, **kw): def run(self, param): meetup = VMeetup.run(self, param) - if meetup and not (c.user_is_loggedin and + if meetup and not (c.user_is_loggedin and meetup.can_edit(c.user, c.user_is_admin)): abort(403, "forbidden") return meetup @@ -211,7 +212,7 @@ def run(self, param): class VTagByName(Validator): def __init__(self, param, *a, **kw): Validator.__init__(self, param, *a, **kw) - + def run(self, name): if name: cleaned = _force_ascii(name) @@ -224,10 +225,10 @@ def run(self, name): class VTags(Validator): comma_sep = re.compile('[,\s]+', re.UNICODE) - + def __init__(self, param, *a, **kw): Validator.__init__(self, param, *a, **kw) - + def run(self, tag_field): tags = [] if tag_field: @@ -266,7 +267,7 @@ def run(self, count): class VLimit(Validator): def run(self, limit): if limit is None: - return c.user.pref_numsites + return c.user.pref_numsites return min(max(int(limit), 1), 250) class VCssMeasure(Validator): @@ -293,7 +294,7 @@ class VLinkUrls(Validator): def __init__(self, item, *a, **kw): self.item = item Validator.__init__(self, item, *a, **kw) - + def run(self, val): res=[] for v in self.splitter.split(val): @@ -313,11 +314,11 @@ class VLinkFullnames(Validator): def __init__(self, item, *a, **kw): self.item = item Validator.__init__(self, item, *a, **kw) - + def run(self, val): if val and self.valid_re.match(val): return self.splitter.split(val) - + class VLength(Validator): def __init__(self, item, length = 10000, empty_error = errors.BAD_COMMENT, @@ -335,10 +336,10 @@ def run(self, title): c.errors.add(self.len_error) else: return title - + class VTitle(VLength): only_whitespace = re.compile(r"^\s*$", re.UNICODE) - + def __init__(self, item, length = 200, **kw): VLength.__init__(self, item, length = length, empty_error = errors.NO_TITLE, @@ -350,15 +351,15 @@ def run(self, title): c.errors.add(errors.NO_TITLE) else: return title - + class VComment(VLength): def __init__(self, item, length = 10000, **kw): VLength.__init__(self, item, length = length, **kw) - + class VMessage(VLength): def __init__(self, item, length = 10000, **kw): - VLength.__init__(self, item, length = length, + VLength.__init__(self, item, length = length, empty_error = errors.NO_MSG_BODY, **kw) @@ -395,7 +396,7 @@ def run(self, description): class VAccountByName(VRequired): def __init__(self, param, error = errors.USER_DOESNT_EXIST, *a, **kw): VRequired.__init__(self, param, error, *a, **kw) - + def run(self, name): if name: try: @@ -404,7 +405,7 @@ def run(self, name): return self.error() class VByName(VRequired): - def __init__(self, param, + def __init__(self, param, error = errors.NO_THING_ID, *a, **kw): VRequired.__init__(self, param, error, *a, **kw) @@ -427,7 +428,7 @@ def run(self, fullname): class VCaptcha(Validator): default_param = ('iden', 'captcha') - + def run(self, iden, solution): if (not c.user_is_loggedin or c.user.needs_captcha()): if not captcha.valid_solution(iden, solution): @@ -440,7 +441,7 @@ def run(self, password = None): if (password is not None) and not valid_password(c.user, password): c.errors.add(errors.WRONG_PASSWORD) - + class VModhash(Validator): default_param = 'uh' def run(self, uh): @@ -468,7 +469,7 @@ def run(self): class VSrModerator(Validator): def run(self): - if not (c.user_is_loggedin and c.site.is_moderator(c.user) + if not (c.user_is_loggedin and c.site.is_moderator(c.user) or c.user_is_admin): abort(403, "forbidden") @@ -525,11 +526,11 @@ def run(self, fullname): return parent #else abort(403, "forbidden") - + class VSubmitLink(VLink): def __init__(self, param, redirect = True, *a, **kw): VLink.__init__(self, param, redirect = redirect, *a, **kw) - + def run(self, link_name): link = VLink.run(self, link_name) if link and not (c.user_is_loggedin and link.can_submit(c.user)): @@ -549,7 +550,7 @@ def run(self, sr_name): sr = None return sr - + pass_rx = re.compile(r".{3,20}") def chkpass(x): @@ -600,10 +601,40 @@ def run(self, user_name): except NotFound: return user_name +class VRealName(VRequired): + def __init__(self, item, *a, **kw): + VRequired.__init__(self, item, errors.BAD_REALNAME, *a, **kw) + def run(self, real_name): + if real_name is None: + return None + original_real_name = real_name + real_name = self.check_real_name(real_name) + if not real_name: + return self.error(self.why_real_name_bad(original_real_name)) + else: + return real_name.encode('utf8') + + @staticmethod + def check_real_name(x): + try: + return x if re.match(ur"^[\w\s\-]{1,40}$", x, re.UNICODE) else None + except TypeError: + return None + except UnicodeEncodeError: + return None + + @staticmethod + def why_real_name_bad(x): + if not x: + return errors.BAD_REALNAME_CHARS + if len(x)>40: + return errors.BAD_REALNAME_LONG + return errors.BAD_REALNAME_CHARS + class VLogin(VRequired): def __init__(self, item, *a, **kw): VRequired.__init__(self, item, errors.WRONG_PASSWORD, *a, **kw) - + def run(self, user_name, password): user_name = chkuser(user_name) user = None @@ -641,7 +672,7 @@ def run(self, url, sr = None): sr = None else: sr = None - + if not url: return self.error(errors.NO_URL) url = utils.sanitize_url(url) @@ -695,7 +726,7 @@ def run(self, val): class VLocation(VLength): def __init__(self, item, length = 100, **kw): - VLength.__init__(self, item, length = length, + VLength.__init__(self, item, length = length, length_error = errors.LOCATION_TOO_LONG, empty_error = None, **kw) @@ -760,7 +791,7 @@ class VCssName(Validator): def run(self, name): if name and self.r_css_name.match(name): return name - + class VMenu(Validator): def __init__(self, param, menu_cls, remember = True, default_item = None, **kw): @@ -775,14 +806,14 @@ def run(self, sort, where): pref = "%s_%s" % (where, self.nav.get_param) user_prefs = copy(c.user.sort_options) if c.user else {} user_pref = user_prefs.get(pref) - + # check to see if a default param has been set if not sort: sort = user_pref if not sort: sort = self.default_item - + # validate the sort if sort not in self.nav.options: sort = self.nav.default @@ -795,7 +826,7 @@ def run(self, sort, where): utils.worker.do(lambda: user._commit()) return sort - + class VRatelimit(Validator): def __init__(self, rate_user = False, rate_ip = False, @@ -892,7 +923,7 @@ def run(self, reason): if reason.startswith('redirect_'): dest = reason[9:] - if (not dest.startswith(c.site.path) and + if (not dest.startswith(c.site.path) and not dest.startswith("http:")): dest = (c.site.path + dest).replace('//', '/') return ('redirect', dest) @@ -918,12 +949,12 @@ def run(self, reason): class ValidEmail(Validator): """Validates an email address""" - + email_re = re.compile(r'.+@.+\..+') def __init__(self, param, **kw): Validator.__init__(self, param = param, **kw) - + def run(self, email): if not email: c.errors.add(errors.NO_EMAIL) @@ -937,14 +968,14 @@ class ValidEmails(Validator): delineated by whitespace, ',' or ';'. Also validates quantity of provided emails. Returns a list of valid email addresses on success""" - + separator = re.compile(r'[^\s,;]+') email_re = re.compile(r'.+@.+\..+') def __init__(self, param, num = 20, **kw): self.num = num Validator.__init__(self, param = param, **kw) - + def run(self, emails0): emails = set(self.separator.findall(emails0) if emails0 else []) failures = set(e for e in emails if not self.email_re.match(e)) diff --git a/r2/r2/lib/errors.py b/r2/r2/lib/errors.py index 681f2577..f05c412d 100644 --- a/r2/r2/lib/errors.py +++ b/r2/r2/lib/errors.py @@ -6,16 +6,16 @@ # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. -# +# # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. -# +# # The Original Code is Reddit. -# +# # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is CondeNet, Inc. -# +# # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ @@ -35,6 +35,9 @@ ('BAD_USERNAME_SHORT', _('Username is too short')), ('BAD_USERNAME_LONG', _('Username is too long')), ('BAD_USERNAME_CHARS', _('Username may not contain special characters')), + ('BAD_REALNAME', _('Invalid name')), + ('BAD_REALNAME_LONG', _('Name is too long')), + ('BAD_REALNAME_CHARS', _('Name may not contain special characters')), ('USERNAME_TAKEN', _('That username is already taken')), ('NO_THING_ID', _('Id not specified')), ('NOT_AUTHOR', _("Only the author can do that")), @@ -94,7 +97,7 @@ def __init__(self, name, i18n_message, msg_params = None): self.name = name self.i18n_message = i18n_message self.msg_params = msg_params or {} - + @property def message(self): return _(self.i18n_message) % self.msg_params @@ -123,10 +126,10 @@ def __repr__(self): def __iter__(self): for x in self.errors: yield x - + def _add(self, error_name, msg, msg_params = None): self.errors[error_name] = Error(error_name, msg, msg_params) - + def add(self, error_name, msg_params = None): msg = error_list[error_name] self._add(error_name, msg, msg_params = msg_params) diff --git a/r2/r2/models/account.py b/r2/r2/models/account.py index f4912514..0fb4170e 100644 --- a/r2/r2/models/account.py +++ b/r2/r2/models/account.py @@ -87,10 +87,18 @@ class Account(Thing): messagebanned = False, dashboard_visit = datetime(2006,10,1, tzinfo = g.tz), wiki_association_attempted_at = None, # None or datetime - wiki_account = None # None, str(account name) or the special string '__taken__', if a new + wiki_account = None, # None, str(account name) or the special string '__taken__', if a new # user didn't get an account because someone else already had the name. + real_name = None ) + @property + def printable_name(self): + if self.real_name: + return self.real_name + " [" + self.name + "]" + else: + return self.name + def karma_ups_downs(self, kind, sr = None): # NOTE: There is a legacy inconsistency in this method. If no subreddit # is specified, karma from all subreddits will be totaled, with each diff --git a/r2/r2/templates/articlenavigation.html b/r2/r2/templates/articlenavigation.html index 9cb23104..5e3b5c3e 100644 --- a/r2/r2/templates/articlenavigation.html +++ b/r2/r2/templates/articlenavigation.html @@ -60,7 +60,7 @@ ${_('Unknown')} %else: <% author_path = unsafe(add_sr("/user/%s/" % websafe(author.name), sr_path = False)) %> - ${author.name} + ${author.printable_name} %endif %def> diff --git a/r2/r2/templates/award.html b/r2/r2/templates/award.html index 89023f82..51520741 100644 --- a/r2/r2/templates/award.html +++ b/r2/r2/templates/award.html @@ -11,11 +11,11 @@