PageRenderTime 62ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/r2/r2/controllers/validator/validator.py

https://github.com/wangmxf/lesswrong
Python | 971 lines | 890 code | 47 blank | 34 comment | 88 complexity | 7d7fe94f027e1034fd6e530b27955cbd MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-2.1
  1. # The contents of this file are subject to the Common Public Attribution
  2. # License Version 1.0. (the "License"); you may not use this file except in
  3. # compliance with the License. You may obtain a copy of the License at
  4. # http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
  5. # License Version 1.1, but Sections 14 and 15 have been added to cover use of
  6. # software over a computer network and provide for limited attribution for the
  7. # Original Developer. In addition, Exhibit A has been modified to be consistent
  8. # with Exhibit B.
  9. #
  10. # Software distributed under the License is distributed on an "AS IS" basis,
  11. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
  12. # the specific language governing rights and limitations under the License.
  13. #
  14. # The Original Code is Reddit.
  15. #
  16. # The Original Developer is the Initial Developer. The Initial Developer of the
  17. # Original Code is CondeNet, Inc.
  18. #
  19. # All portions of the code written by CondeNet are Copyright (c) 2006-2008
  20. # CondeNet, Inc. All Rights Reserved.
  21. ################################################################################
  22. from pylons import c, request, g
  23. from pylons.i18n import _
  24. from pylons.controllers.util import abort
  25. from r2.lib import utils, captcha
  26. from r2.lib.filters import unkeep_space, websafe, _force_utf8, _force_ascii
  27. from r2.lib.wikipagecached import WikiPageCached
  28. from r2.lib.db.operators import asc, desc
  29. from r2.config import cache
  30. from r2.lib.template_helpers import add_sr
  31. from r2.lib.jsonresponse import json_respond
  32. from r2.lib.errors import errors, UserRequiredException
  33. from r2.models import *
  34. from copy import copy
  35. from datetime import datetime, timedelta
  36. import pytz
  37. import re
  38. class Validator(object):
  39. default_param = None
  40. def __init__(self, param=None, default=None, post=True, get=True, url=True):
  41. if param:
  42. self.param = param
  43. else:
  44. self.param = self.default_param
  45. self.default = default
  46. self.post, self.get, self.url = post, get, url
  47. def __call__(self, url):
  48. a = []
  49. if self.param:
  50. for p in utils.tup(self.param):
  51. if self.post and request.post.get(p):
  52. val = request.post[p]
  53. elif self.get and request.get.get(p):
  54. val = request.get[p]
  55. elif self.url and url.get(p):
  56. val = url[p]
  57. else:
  58. val = self.default
  59. a.append(val)
  60. return self.run(*a)
  61. def validate(*simple_vals, **param_vals):
  62. def val(fn):
  63. def newfn(self, *a, **env):
  64. try:
  65. for validator in simple_vals:
  66. validator(env)
  67. kw = self.build_arg_list(fn, env)
  68. for var, validator in param_vals.iteritems():
  69. kw[var] = validator(env)
  70. return fn(self, *a, **kw)
  71. except UserRequiredException:
  72. if request.method == "POST" and hasattr(self, "ajax_login_redirect"):
  73. # ajax failure, so redirect accordingly
  74. return self.ajax_login_redirect("/")
  75. return self.intermediate_redirect('/login')
  76. return newfn
  77. return val
  78. #### validators ####
  79. class nop(Validator):
  80. def run(self, x):
  81. return x
  82. class VLang(Validator):
  83. def run(self, lang):
  84. if lang:
  85. lang = str(lang.split('[')[1].strip(']'))
  86. if lang in g.all_languages:
  87. return lang
  88. #else
  89. return 'en'
  90. class VRequired(Validator):
  91. def __init__(self, param, error, *a, **kw):
  92. Validator.__init__(self, param, *a, **kw)
  93. self._error = error
  94. def error(self, e = None):
  95. if not e: e = self._error
  96. if e:
  97. c.errors.add(e)
  98. def run(self, item):
  99. if not item:
  100. self.error()
  101. else:
  102. return item
  103. class ValueOrBlank(Validator):
  104. def run(self, value):
  105. """Returns the value as is if present, else an empty string"""
  106. return '' if value is None else value
  107. class VLink(Validator):
  108. def __init__(self, param, redirect = True, *a, **kw):
  109. Validator.__init__(self, param, *a, **kw)
  110. self.redirect = redirect
  111. def run(self, link_id):
  112. if link_id:
  113. try:
  114. aid = int(link_id, 36)
  115. return Link._byID(aid, True)
  116. except (NotFound, ValueError):
  117. if self.redirect:
  118. abort(404, 'page not found')
  119. else:
  120. return None
  121. class VCommentFullName(Validator):
  122. valid_re = re.compile(Comment._type_prefix + str(Comment._type_id) + r'_([0-9a-z]+)$')
  123. def run(self, thing_fullname):
  124. if thing_fullname:
  125. match = self.valid_re.match(thing_fullname)
  126. if match:
  127. try:
  128. parsed_id = int(match.group(1), 36)
  129. return Comment._byID(parsed_id, True)
  130. except Exception:
  131. return None
  132. class VMeetup(Validator):
  133. def __init__(self, param, redirect = True, *a, **kw):
  134. Validator.__init__(self, param, *a, **kw)
  135. self.redirect = redirect
  136. def run(self, meetup_id36):
  137. if meetup_id36:
  138. try:
  139. meetup_id = int(meetup_id36, 36)
  140. return Meetup._byID(meetup_id, True)
  141. except (NotFound, ValueError):
  142. if self.redirect:
  143. abort(404, 'page not found')
  144. else:
  145. return None
  146. class VEditMeetup(VMeetup):
  147. def __init__(self, param, redirect = True, *a, **kw):
  148. VMeetup.__init__(self, param, redirect = redirect, *a, **kw)
  149. def run(self, param):
  150. meetup = VMeetup.run(self, param)
  151. if meetup and not (c.user_is_loggedin and
  152. meetup.can_edit(c.user, c.user_is_admin)):
  153. abort(403, "forbidden")
  154. return meetup
  155. class VTagByName(Validator):
  156. def __init__(self, param, *a, **kw):
  157. Validator.__init__(self, param, *a, **kw)
  158. def run(self, name):
  159. if name:
  160. cleaned = _force_ascii(name)
  161. if cleaned == name:
  162. try:
  163. return Tag._by_name(cleaned)
  164. except:
  165. pass
  166. abort(404, 'page not found')
  167. class VTags(Validator):
  168. comma_sep = re.compile('[,\s]+', re.UNICODE)
  169. def __init__(self, param, *a, **kw):
  170. Validator.__init__(self, param, *a, **kw)
  171. def run(self, tag_field):
  172. tags = []
  173. if tag_field:
  174. # Tags are comma delimited
  175. tags = [x for x in self.comma_sep.split(tag_field) if x==_force_ascii(x)]
  176. return tags
  177. class VMessage(Validator):
  178. def run(self, message_id):
  179. if message_id:
  180. try:
  181. aid = int(message_id, 36)
  182. return Message._byID(aid, True)
  183. except (NotFound, ValueError):
  184. abort(404, 'page not found')
  185. class VCommentID(Validator):
  186. def run(self, cid):
  187. if cid:
  188. try:
  189. cid = int(cid, 36)
  190. return Comment._byID(cid, True)
  191. except (NotFound, ValueError):
  192. pass
  193. class VCount(Validator):
  194. def run(self, count):
  195. try:
  196. count = int(count)
  197. except (TypeError, ValueError):
  198. count = 0
  199. return max(count, 0)
  200. class VLimit(Validator):
  201. def run(self, limit):
  202. if limit is None:
  203. return c.user.pref_numsites
  204. return min(max(int(limit), 1), 250)
  205. class VCssMeasure(Validator):
  206. measure = re.compile(r"^\s*[\d\.]+\w{0,3}\s*$")
  207. def run(self, value):
  208. return value if value and self.measure.match(value) else ''
  209. subreddit_rx = re.compile(r"^[\w]{3,20}$", re.UNICODE)
  210. def chksrname(x):
  211. #notice the space before reddit.com
  212. if x in ('friends', 'all', ' reddit.com'):
  213. return False
  214. try:
  215. return str(x) if x and subreddit_rx.match(x) else None
  216. except UnicodeEncodeError:
  217. return None
  218. class VLinkUrls(Validator):
  219. "A comma-separated list of link urls"
  220. splitter = re.compile('[ ,]+')
  221. id_re = re.compile('^/lw/([^/]+)/')
  222. def __init__(self, item, *a, **kw):
  223. self.item = item
  224. Validator.__init__(self, item, *a, **kw)
  225. def run(self, val):
  226. res=[]
  227. for v in self.splitter.split(val):
  228. link_id = self.id_re.match(v)
  229. if link_id:
  230. l = VLink(None,False).run(link_id.group(1))
  231. if l:
  232. res.append(l)
  233. return res
  234. class VLinkFullnames(Validator):
  235. "A space- or comma-separated list of fullnames for Links"
  236. valid_re = re.compile(r'^(' + Link._type_prefix + str(Link._type_id) +
  237. r'_[0-9a-z]+[ ,]?)+$')
  238. splitter = re.compile('[ ,]+')
  239. def __init__(self, item, *a, **kw):
  240. self.item = item
  241. Validator.__init__(self, item, *a, **kw)
  242. def run(self, val):
  243. if val and self.valid_re.match(val):
  244. return self.splitter.split(val)
  245. class VLength(Validator):
  246. def __init__(self, item, length = 10000,
  247. empty_error = errors.BAD_COMMENT,
  248. length_error = errors.COMMENT_TOO_LONG, **kw):
  249. Validator.__init__(self, item, **kw)
  250. self.length = length
  251. self.len_error = length_error
  252. self.emp_error = empty_error
  253. def run(self, title):
  254. if not title:
  255. if self.emp_error is not None:
  256. c.errors.add(self.emp_error)
  257. elif len(title) > self.length:
  258. c.errors.add(self.len_error)
  259. else:
  260. return title
  261. class VTitle(VLength):
  262. only_whitespace = re.compile(r"^\s*$", re.UNICODE)
  263. def __init__(self, item, length = 200, **kw):
  264. VLength.__init__(self, item, length = length,
  265. empty_error = errors.NO_TITLE,
  266. length_error = errors.TITLE_TOO_LONG, **kw)
  267. def run(self, title):
  268. title = VLength.run(self, title)
  269. if title and self.only_whitespace.match(title):
  270. c.errors.add(errors.NO_TITLE)
  271. else:
  272. return title
  273. class VComment(VLength):
  274. def __init__(self, item, length = 10000, **kw):
  275. VLength.__init__(self, item, length = length, **kw)
  276. class VMessage(VLength):
  277. def __init__(self, item, length = 10000, **kw):
  278. VLength.__init__(self, item, length = length,
  279. empty_error = errors.NO_MSG_BODY, **kw)
  280. class VSubredditName(VRequired):
  281. def __init__(self, item, *a, **kw):
  282. VRequired.__init__(self, item, errors.BAD_SR_NAME, *a, **kw)
  283. def run(self, name):
  284. name = chksrname(name)
  285. if not name:
  286. return self.error()
  287. else:
  288. try:
  289. a = Subreddit._by_name(name)
  290. return self.error(errors.SUBREDDIT_EXISTS)
  291. except NotFound:
  292. return name
  293. class VSubredditTitle(Validator):
  294. def run(self, title):
  295. if not title:
  296. c.errors.add(errors.NO_TITLE)
  297. elif len(title) > 100:
  298. c.errors.add(errors.TITLE_TOO_LONG)
  299. else:
  300. return title
  301. class VSubredditDesc(Validator):
  302. def run(self, description):
  303. if description and len(description) > 500:
  304. c.errors.add(errors.DESC_TOO_LONG)
  305. return unkeep_space(description or '')
  306. class VAccountByName(VRequired):
  307. def __init__(self, param, error = errors.USER_DOESNT_EXIST, *a, **kw):
  308. VRequired.__init__(self, param, error, *a, **kw)
  309. def run(self, name):
  310. if name:
  311. try:
  312. return Account._by_name(name)
  313. except NotFound: pass
  314. return self.error()
  315. class VByName(VRequired):
  316. def __init__(self, param,
  317. error = errors.NO_THING_ID, *a, **kw):
  318. VRequired.__init__(self, param, error, *a, **kw)
  319. def run(self, fullname):
  320. if fullname:
  321. try:
  322. return Thing._by_fullname(fullname, False, data=True)
  323. except NotFound:
  324. pass
  325. return self.error()
  326. class VByNameIfAuthor(VByName):
  327. def run(self, fullname):
  328. thing = VByName.run(self, fullname)
  329. if thing:
  330. if not thing._loaded: thing._load()
  331. if c.user_is_loggedin and thing.author_id == c.user._id:
  332. return thing
  333. return self.error(errors.NOT_AUTHOR)
  334. class VCaptcha(Validator):
  335. default_param = ('iden', 'captcha')
  336. def run(self, iden, solution):
  337. if (not c.user_is_loggedin or c.user.needs_captcha()):
  338. if not captcha.valid_solution(iden, solution):
  339. c.errors.add(errors.BAD_CAPTCHA)
  340. class VUser(Validator):
  341. def run(self, password = None):
  342. if not c.user_is_loggedin:
  343. raise UserRequiredException
  344. if (password is not None) and not valid_password(c.user, password):
  345. c.errors.add(errors.WRONG_PASSWORD)
  346. class VModhash(Validator):
  347. default_param = 'uh'
  348. def run(self, uh):
  349. if not c.user_is_loggedin:
  350. raise UserRequiredException
  351. if not c.user.valid_hash(uh):
  352. g.log.info("Invalid hash on form submission : "+str(c.user))
  353. raise UserRequiredException
  354. #abort(403, 'forbidden')
  355. class VVotehash(Validator):
  356. def run(self, vh, thing_name):
  357. return True
  358. class VAdmin(Validator):
  359. def run(self):
  360. if not c.user_is_admin:
  361. abort(404, "page not found")
  362. class VSponsor(Validator):
  363. def run(self):
  364. if not c.user_is_sponsor:
  365. abort(403, 'forbidden')
  366. class VSrModerator(Validator):
  367. def run(self):
  368. if not (c.user_is_loggedin and c.site.is_moderator(c.user)
  369. or c.user_is_admin):
  370. abort(403, "forbidden")
  371. class VSrCanBan(Validator):
  372. def run(self, thing_name):
  373. if c.user_is_admin:
  374. return True
  375. elif c.user_is_loggedin:
  376. item = Thing._by_fullname(thing_name,data=True)
  377. # will throw a legitimate 500 if this isn't a link or
  378. # comment, because this should only be used on links and
  379. # comments
  380. subreddit = item.subreddit_slow
  381. if subreddit.can_ban(c.user):
  382. return True
  383. abort(403,'forbidden')
  384. class VSrSpecial(Validator):
  385. def run(self, thing_name):
  386. if c.user_is_admin:
  387. return True
  388. elif c.user_is_loggedin:
  389. item = Thing._by_fullname(thing_name,data=True)
  390. # will throw a legitimate 500 if this isn't a link or
  391. # comment, because this should only be used on links and
  392. # comments
  393. subreddit = item.subreddit_slow
  394. if subreddit.is_special(c.user):
  395. return True
  396. abort(403,'forbidden')
  397. class VSRSubmitPage(Validator):
  398. def run(self):
  399. if not (c.default_sr or c.user_is_loggedin and c.site.can_submit(c.user)):
  400. return False
  401. else:
  402. return True
  403. class VCreateMeetup(Validator):
  404. def run(self):
  405. if (c.user_is_loggedin and c.user.safe_karma >= g.discussion_karma_to_post):
  406. return True
  407. abort(403, "forbidden")
  408. class VSubmitParent(Validator):
  409. def run(self, fullname):
  410. if fullname:
  411. parent = Thing._by_fullname(fullname, False, data=True)
  412. if isinstance(parent, Message):
  413. return parent
  414. else:
  415. sr = parent.subreddit_slow
  416. if c.user_is_loggedin and sr.can_comment(c.user):
  417. return parent
  418. #else
  419. abort(403, "forbidden")
  420. class VSubmitLink(VLink):
  421. def __init__(self, param, redirect = True, *a, **kw):
  422. VLink.__init__(self, param, redirect = redirect, *a, **kw)
  423. def run(self, link_name):
  424. link = VLink.run(self, link_name)
  425. if link and not (c.user_is_loggedin and link.can_submit(c.user)):
  426. abort(403, "forbidden")
  427. return link
  428. class VSubmitSR(Validator):
  429. def run(self, sr_name):
  430. try:
  431. sr = Subreddit._by_name(sr_name)
  432. except NotFound:
  433. c.errors.add(errors.SUBREDDIT_NOEXIST)
  434. sr = None
  435. if sr and not (c.user_is_loggedin and sr.can_submit(c.user)):
  436. c.errors.add(errors.SUBREDDIT_FORBIDDEN)
  437. sr = None
  438. return sr
  439. pass_rx = re.compile(r".{3,20}")
  440. def chkpass(x):
  441. return x if x and pass_rx.match(x) else None
  442. class VPassword(VRequired):
  443. def __init__(self, item, *a, **kw):
  444. VRequired.__init__(self, item, errors.BAD_PASSWORD, *a, **kw)
  445. def run(self, password, verify):
  446. if not chkpass(password):
  447. return self.error()
  448. elif verify != password:
  449. return self.error(errors.BAD_PASSWORD_MATCH)
  450. else:
  451. return password
  452. user_rx = re.compile(r"^[\w-]{3,20}$", re.UNICODE)
  453. def chkuser(x):
  454. try:
  455. return str(x) if user_rx.match(x) else None
  456. except TypeError:
  457. return None
  458. except UnicodeEncodeError:
  459. return None
  460. def whyuserbad(x):
  461. if not x:
  462. return errors.BAD_USERNAME_CHARS
  463. if len(x)<3:
  464. return errors.BAD_USERNAME_SHORT
  465. if len(x)>20:
  466. return errors.BAD_USERNAME_LONG
  467. return errors.BAD_USERNAME_CHARS
  468. class VUname(VRequired):
  469. def __init__(self, item, *a, **kw):
  470. VRequired.__init__(self, item, errors.BAD_USERNAME, *a, **kw)
  471. def run(self, user_name):
  472. original_user_name = user_name;
  473. user_name = chkuser(user_name)
  474. if not user_name:
  475. return self.error(whyuserbad(original_user_name))
  476. else:
  477. try:
  478. a = Account._by_name(user_name, True)
  479. return self.error(errors.USERNAME_TAKEN)
  480. except NotFound:
  481. return user_name
  482. class VLogin(VRequired):
  483. def __init__(self, item, *a, **kw):
  484. VRequired.__init__(self, item, errors.WRONG_PASSWORD, *a, **kw)
  485. def run(self, user_name, password):
  486. user_name = chkuser(user_name)
  487. user = None
  488. if user_name:
  489. user = valid_login(user_name, password)
  490. if not user:
  491. return self.error()
  492. return user
  493. class VSanitizedUrl(Validator):
  494. def run(self, url):
  495. return utils.sanitize_url(url)
  496. class VUserWebsiteUrl(VSanitizedUrl):
  497. def run(self, url):
  498. val = VSanitizedUrl.run(self, url)
  499. if val is None:
  500. return ''
  501. else:
  502. return val
  503. class VUrl(VRequired):
  504. def __init__(self, item, *a, **kw):
  505. VRequired.__init__(self, item, errors.NO_URL, *a, **kw)
  506. def run(self, url, sr = None):
  507. if sr is None and not isinstance(c.site, FakeSubreddit):
  508. sr = c.site
  509. elif sr:
  510. try:
  511. sr = Subreddit._by_name(sr)
  512. except NotFound:
  513. c.errors.add(errors.SUBREDDIT_NOEXIST)
  514. sr = None
  515. else:
  516. sr = None
  517. if not url:
  518. return self.error(errors.NO_URL)
  519. url = utils.sanitize_url(url)
  520. if url == 'self':
  521. return url
  522. elif url:
  523. try:
  524. l = Link._by_url(url, sr)
  525. self.error(errors.ALREADY_SUB)
  526. return utils.tup(l)
  527. except NotFound:
  528. return url
  529. return self.error(errors.BAD_URL)
  530. class VExistingUname(VRequired):
  531. def __init__(self, item, *a, **kw):
  532. VRequired.__init__(self, item, errors.NO_USER, *a, **kw)
  533. def run(self, username):
  534. if username:
  535. try:
  536. name = _force_utf8(username)
  537. return Account._by_name(name)
  538. except (TypeError, UnicodeEncodeError, NotFound):
  539. return self.error(errors.USER_DOESNT_EXIST)
  540. self.error()
  541. class VUserWithEmail(VExistingUname):
  542. def run(self, name):
  543. user = VExistingUname.run(self, name)
  544. if not user or not hasattr(user, 'email') or not user.email:
  545. return self.error(errors.NO_EMAIL_FOR_USER)
  546. return user
  547. class VTimestamp(Validator):
  548. def run(self, val):
  549. if not val:
  550. c.errors.add(errors.INVALID_DATE)
  551. return
  552. try:
  553. val = float(val) / 1000.0
  554. datetime.fromtimestamp(val, pytz.utc) # Check it can be converted to a datetime
  555. return val
  556. except ValueError:
  557. c.errors.add(errors.INVALID_DATE)
  558. class VBoolean(Validator):
  559. def run(self, val):
  560. return val != "off" and bool(val)
  561. class VLocation(VLength):
  562. def __init__(self, item, length = 100, **kw):
  563. VLength.__init__(self, item, length = length,
  564. length_error = errors.LOCATION_TOO_LONG,
  565. empty_error = None, **kw)
  566. def run(self, val):
  567. val = VLength.run(self, val)
  568. if val == None:
  569. return ''
  570. else:
  571. return val
  572. class VInt(Validator):
  573. def __init__(self, param, min=None, max=None, *a, **kw):
  574. self.min = min
  575. self.max = max
  576. Validator.__init__(self, param, *a, **kw)
  577. def run(self, val):
  578. if not val:
  579. return
  580. try:
  581. val = int(val)
  582. if self.min is not None and val < self.min:
  583. val = self.min
  584. elif self.max is not None and val > self.max:
  585. val = self.max
  586. return val
  587. except ValueError:
  588. c.errors.add(errors.BAD_NUMBER)
  589. class VFloat(Validator):
  590. def __init__(self, param, min=None, max=None, allow_none=False, error=errors.BAD_NUMBER, *a, **kw):
  591. self.min = min
  592. self.max = max
  593. self.allow_none = allow_none
  594. self.error = error
  595. Validator.__init__(self, param, *a, **kw)
  596. def run(self, val):
  597. if not val:
  598. if self.allow_none:
  599. return None
  600. c.errors.add(self.error)
  601. return
  602. try:
  603. val = float(val)
  604. if self.min is not None and val < self.min:
  605. val = self.min
  606. elif self.max is not None and val > self.max:
  607. val = self.max
  608. return val
  609. except ValueError:
  610. c.errors.add(self.error)
  611. class VCssName(Validator):
  612. """
  613. returns a name iff it consists of alphanumeric characters and
  614. possibly "-", and is below the length limit.
  615. """
  616. r_css_name = re.compile(r"^[a-zA-Z0-9\-]{1,100}$")
  617. def run(self, name):
  618. if name and self.r_css_name.match(name):
  619. return name
  620. class VMenu(Validator):
  621. def __init__(self, param, menu_cls, remember = True, default_item = None, **kw):
  622. self.nav = menu_cls
  623. self.remember = remember
  624. self.default_item = default_item or self.nav.default
  625. param = (menu_cls.get_param, param)
  626. Validator.__init__(self, param, **kw)
  627. def run(self, sort, where):
  628. if self.remember:
  629. pref = "%s_%s" % (where, self.nav.get_param)
  630. user_prefs = copy(c.user.sort_options) if c.user else {}
  631. user_pref = user_prefs.get(pref)
  632. # check to see if a default param has been set
  633. if not sort:
  634. sort = user_pref
  635. if not sort:
  636. sort = self.default_item
  637. # validate the sort
  638. if sort not in self.nav.options:
  639. sort = self.nav.default
  640. # commit the sort if changed
  641. if self.remember and c.user_is_loggedin and sort != user_pref:
  642. user_prefs[pref] = sort
  643. c.user.sort_options = user_prefs
  644. user = c.user
  645. utils.worker.do(lambda: user._commit())
  646. return sort
  647. class VRatelimit(Validator):
  648. def __init__(self, rate_user = False, rate_ip = False,
  649. prefix = 'rate_', *a, **kw):
  650. self.rate_user = rate_user
  651. self.rate_ip = rate_ip
  652. self.prefix = prefix
  653. Validator.__init__(self, *a, **kw)
  654. def run (self):
  655. to_check = []
  656. if self.rate_user and c.user_is_loggedin:
  657. to_check.append('user' + str(c.user._id36))
  658. if self.rate_ip:
  659. to_check.append('ip' + str(request.ip))
  660. r = cache.get_multi(to_check, self.prefix)
  661. if r:
  662. expire_time = max(r.values())
  663. time = utils.timeuntil(expire_time)
  664. c.errors.add(errors.RATELIMIT, {'time': time})
  665. @classmethod
  666. def ratelimit(self, rate_user = False, rate_ip = False, prefix = "rate_"):
  667. to_set = {}
  668. seconds = g.RATELIMIT*60
  669. if seconds <= 0:
  670. return
  671. expire_time = datetime.now(g.tz) + timedelta(seconds = seconds)
  672. if rate_user and c.user_is_loggedin:
  673. to_set['user' + str(c.user._id36)] = expire_time
  674. if rate_ip:
  675. to_set['ip' + str(request.ip)] = expire_time
  676. cache.set_multi(to_set, prefix, time = seconds)
  677. class VCommentIDs(Validator):
  678. #id_str is a comma separated list of id36's
  679. def run(self, id_str):
  680. if not id_str:
  681. return None
  682. cids = [int(i, 36) for i in id_str.split(',')]
  683. comments = Comment._byID(cids, data=True, return_dict = False)
  684. return comments
  685. class VFullNames(Validator):
  686. #id_str is a comma separated list of id36's
  687. def run(self, id_str):
  688. tids = id_str.split(',')
  689. return Thing._by_fullname(tids, data=True, return_dict = False)
  690. class VSubreddits(Validator):
  691. #the subreddits are just in the post, this is for the my.reddit pref page
  692. def run(self):
  693. subreddits = Subreddit._by_fullname(request.post.keys())
  694. return subreddits.values()
  695. class VCacheKey(Validator):
  696. def __init__(self, cache_prefix, param, *a, **kw):
  697. self.cache_prefix = cache_prefix
  698. Validator.__init__(self, param, *a, **kw)
  699. def run(self, key, name):
  700. if key:
  701. uid = cache.get(str(self.cache_prefix + "_" + key))
  702. try:
  703. a = Account._byID(uid, data = True)
  704. except NotFound:
  705. return None
  706. if name and a.name.lower() != name.lower():
  707. c.errors.add(errors.BAD_USERNAME)
  708. if a:
  709. return a
  710. c.errors.add(errors.EXPIRED)
  711. class VOneOf(Validator):
  712. def __init__(self, param, options = (), *a, **kw):
  713. Validator.__init__(self, param, *a, **kw)
  714. self.options = options
  715. def run(self, val):
  716. if self.options and val not in self.options:
  717. c.errors.add(errors.INVALID_OPTION)
  718. return self.default
  719. else:
  720. return val
  721. class VReason(Validator):
  722. def run(self, reason):
  723. if not reason:
  724. return
  725. if reason.startswith('redirect_'):
  726. dest = reason[9:]
  727. if (not dest.startswith(c.site.path) and
  728. not dest.startswith("http:")):
  729. dest = (c.site.path + dest).replace('//', '/')
  730. return ('redirect', dest)
  731. if reason.startswith('vote_'):
  732. fullname = reason[5:]
  733. t = Thing._by_fullname(fullname, data=True)
  734. return ('redirect', t.make_permalink_slow())
  735. elif reason.startswith('share_'):
  736. fullname = reason[6:]
  737. t = Thing._by_fullname(fullname, data=True)
  738. return ('redirect', t.make_permalink_slow())
  739. elif reason.startswith('reply_'):
  740. fullname = reason[6:]
  741. t = Thing._by_fullname(fullname, data=True)
  742. return ('redirect', t.make_permalink_slow())
  743. elif reason.startswith('sr_change_'):
  744. sr_list = reason[10:].split(',')
  745. fullnames = dict(i.split(':') for i in sr_list)
  746. srs = Subreddit._by_fullname(fullnames.keys(), data = True,
  747. return_dict = False)
  748. sr_onoff = dict((sr, fullnames[sr._fullname] == 1) for sr in srs)
  749. return ('subscribe', sr_onoff)
  750. class ValidEmails(Validator):
  751. """Validates a list of email addresses passed in as a string and
  752. delineated by whitespace, ',' or ';'. Also validates quantity of
  753. provided emails. Returns a list of valid email addresses on
  754. success"""
  755. separator = re.compile(r'[^\s,;]+')
  756. email_re = re.compile(r'.+@.+\..+')
  757. def __init__(self, param, num = 20, **kw):
  758. self.num = num
  759. Validator.__init__(self, param = param, **kw)
  760. def run(self, emails0):
  761. emails = set(self.separator.findall(emails0) if emails0 else [])
  762. failures = set(e for e in emails if not self.email_re.match(e))
  763. emails = emails - failures
  764. # make sure the number of addresses does not exceed the max
  765. if self.num > 0 and len(emails) + len(failures) > self.num:
  766. # special case for 1: there should be no delineators at all, so
  767. # send back original string to the user
  768. if self.num == 1:
  769. c.errors.add(errors.BAD_EMAILS,
  770. {'emails': '"%s"' % emails0})
  771. # else report the number expected
  772. else:
  773. c.errors.add(errors.TOO_MANY_EMAILS,
  774. {'num': self.num})
  775. # correct number, but invalid formatting
  776. elif failures:
  777. c.errors.add(errors.BAD_EMAILS,
  778. {'emails': ', '.join(failures)})
  779. # no emails
  780. elif not emails:
  781. c.errors.add(errors.NO_EMAILS)
  782. else:
  783. # return single email if one is expected, list otherwise
  784. return list(emails)[0] if self.num == 1 else emails
  785. class VCnameDomain(Validator):
  786. domain_re = re.compile(r'^([\w]+\.)+[\w]+$')
  787. def run(self, domain):
  788. if (domain
  789. and (not self.domain_re.match(domain)
  790. or domain.endswith('.reddit.com')
  791. or len(domain) > 300)):
  792. c.errors.add(errors.BAD_CNAME)
  793. elif domain:
  794. try:
  795. return str(domain).lower()
  796. except UnicodeEncodeError:
  797. c.errors.add(errors.BAD_CNAME)
  798. class VWikiPageURL(Validator):
  799. page_name_re = re.compile('^[ -.0-:A-Z_-z]+$')
  800. def run(self, url):
  801. if not url or not url.startswith(WikiPageCached.url_prefix):
  802. c.errors.add(errors.BAD_URL)
  803. return None
  804. page_name = url[len(WikiPageCached.url_prefix):]
  805. if not self.page_name_re.match(page_name):
  806. c.errors.add(errors.BAD_URL)
  807. return None
  808. return url
  809. # NOTE: make sure *never* to have res check these are present
  810. # otherwise, the response could contain reference to these errors...!
  811. class ValidIP(Validator):
  812. def run(self):
  813. if is_banned_IP(request.ip):
  814. c.errors.add(errors.BANNED_IP)
  815. return request.ip
  816. class ValidDomain(Validator):
  817. def run(self, url):
  818. if url and is_banned_domain(url):
  819. c.errors.add(errors.BANNED_DOMAIN)