/r2/r2/controllers/reddit_base.py

https://github.com/wangmxf/lesswrong · Python · 630 lines · 461 code · 89 blank · 80 comment · 157 complexity · f899526ae3f29f7a9cbda8378b73520b MD5 · raw file

  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. import r2.lib.helpers as h
  23. from pylons import c, g, request
  24. from pylons.controllers.util import abort, redirect_to
  25. from pylons.i18n import _
  26. from pylons.i18n.translation import LanguageError
  27. from r2.lib.base import BaseController, proxyurl, current_login_cookie
  28. from r2.lib import pages, utils, filters
  29. from r2.lib.utils import http_utils
  30. from r2.lib.cache import LocalCache
  31. import random as rand
  32. from r2.models.account import valid_cookie, FakeAccount
  33. from r2.models.subreddit import Subreddit
  34. import r2.config as config
  35. from r2.models import *
  36. from r2.lib.errors import ErrorSet
  37. from validator import *
  38. from r2.lib.template_helpers import add_sr
  39. from r2.lib.jsontemplates import api_type
  40. from copy import copy
  41. from Cookie import CookieError
  42. from datetime import datetime
  43. import hashlib, inspect, simplejson
  44. from urllib import quote, unquote
  45. from r2.lib.tracking import encrypt, decrypt
  46. NEVER = 'Thu, 31 Dec 2037 23:59:59 GMT'
  47. cache_affecting_cookies = ('reddit_first','over18')
  48. class Cookies(dict):
  49. def add(self, name, value, *k, **kw):
  50. self[name] = Cookie(value, *k, **kw)
  51. class Cookie(object):
  52. def __init__(self, value, expires = None, domain = None, dirty = True):
  53. self.value = value
  54. self.expires = expires
  55. self.dirty = dirty
  56. if domain:
  57. self.domain = domain
  58. elif c.authorized_cname:
  59. self.domain = c.site.domain
  60. else:
  61. self.domain = g.domain
  62. def __repr__(self):
  63. return ("Cookie(value=%r, expires=%r, domain=%r, dirty=%r)"
  64. % (self.value, self.expires, self.domain, self.dirty))
  65. class UnloggedUser(FakeAccount):
  66. _cookie = 'options'
  67. allowed_prefs = ('pref_content_langs', 'pref_lang')
  68. def __init__(self, browser_langs, *a, **kw):
  69. FakeAccount.__init__(self, *a, **kw)
  70. if browser_langs:
  71. lang = browser_langs[0]
  72. content_langs = list(browser_langs)
  73. content_langs.sort()
  74. else:
  75. lang = 'en'
  76. content_langs = 'all'
  77. self._defaults = self._defaults.copy()
  78. self._defaults['pref_lang'] = lang
  79. self._defaults['pref_content_langs'] = content_langs
  80. self._load()
  81. @property
  82. def name(self):
  83. raise NotImplementedError
  84. def _from_cookie(self):
  85. z = read_user_cookie(self._cookie)
  86. try:
  87. d = simplejson.loads(decrypt(z))
  88. return dict((k, v) for k, v in d.iteritems()
  89. if k in self.allowed_prefs)
  90. except ValueError:
  91. return {}
  92. def _to_cookie(self, data):
  93. data = data.copy()
  94. for k in data.keys():
  95. if k not in self.allowed_prefs:
  96. del k
  97. set_user_cookie(self._cookie, encrypt(simplejson.dumps(data)))
  98. def _subscribe(self, sr):
  99. pass
  100. def _unsubscribe(self, sr):
  101. pass
  102. def _commit(self):
  103. if self._dirty:
  104. self._t.update(self._dirties)
  105. self._to_cookie(self._t)
  106. def _load(self):
  107. self._t.update(self._from_cookie())
  108. self._loaded = True
  109. def read_user_cookie(name):
  110. uname = c.user.name if c.user_is_loggedin else ""
  111. cookie_name = uname + '_' + name
  112. if cookie_name in c.cookies:
  113. return c.cookies[cookie_name].value
  114. else:
  115. return ''
  116. def set_user_cookie(name, val):
  117. uname = c.user.name if c.user_is_loggedin else ""
  118. c.cookies[uname + '_' + name] = Cookie(value = val)
  119. valid_click_cookie = re.compile(r'(t[0-9]_[a-zA-Z0-9]+:)+').match
  120. def read_click_cookie():
  121. if c.user_is_loggedin:
  122. click_cookie = read_user_cookie('click')
  123. if click_cookie and valid_click_cookie(click_cookie):
  124. ids = [s for s in click_cookie.split(':') if s]
  125. things = Thing._by_fullname(ids, return_dict = False)
  126. for t in things:
  127. def foo(t1, user):
  128. return lambda: t1._click(user)
  129. #don't record clicks for the time being
  130. #utils.worker.do(foo(t, c.user))
  131. set_user_cookie('click', '')
  132. def read_mod_cookie():
  133. cook = [s.split('=')[0:2] for s in read_user_cookie('mod').split(':') if s]
  134. if cook:
  135. set_user_cookie('mod', '')
  136. def firsttime():
  137. if get_redditfirst('firsttime'):
  138. return False
  139. else:
  140. set_redditfirst('firsttime','first')
  141. return True
  142. def get_redditfirst(key,default=None):
  143. try:
  144. cookie = simplejson.loads(c.cookies['reddit_first'].value)
  145. return cookie[key]
  146. except (ValueError,TypeError,KeyError),e:
  147. # it's not a proper json dict, or the cookie isn't present, or
  148. # the key isn't part of the cookie; we don't really want a
  149. # broken cookie to propogate an exception up
  150. return default
  151. def set_redditfirst(key,val):
  152. try:
  153. cookie = simplejson.loads(c.cookies['reddit_first'].value)
  154. cookie[key] = val
  155. except (ValueError,TypeError,KeyError),e:
  156. # invalid JSON data; we'll just construct a new cookie
  157. cookie = {key: val}
  158. c.cookies['reddit_first'] = Cookie(simplejson.dumps(cookie),
  159. expires = NEVER)
  160. # this cookie is also accessed by organic.js, so changes to the format
  161. # will have to be made there as well
  162. organic_pos_key = 'organic_pos'
  163. def organic_pos():
  164. "organic_pos() -> (calc_date = str(), pos = int())"
  165. try:
  166. d,p = get_redditfirst(organic_pos_key, ('',0))
  167. except ValueError:
  168. d,p = ('',0)
  169. return d,p
  170. def set_organic_pos(key,pos):
  171. "set_organic_pos(str(), int()) -> None"
  172. set_redditfirst(organic_pos_key,[key,pos])
  173. def over18():
  174. if c.user.pref_over_18 or c.user_is_admin:
  175. return True
  176. else:
  177. if 'over18' in c.cookies:
  178. cookie = c.cookies['over18'].value
  179. if cookie == hashlib.sha1(request.ip).hexdigest():
  180. return True
  181. def set_subreddit():
  182. #the r parameter gets added by javascript for POST requests so we
  183. #can reference c.site in api.py
  184. sr_name = request.environ.get("subreddit", request.POST.get('r'))
  185. domain = request.environ.get("domain")
  186. if not sr_name:
  187. #check for cnames
  188. sub_domain = request.environ.get('sub_domain')
  189. sr = Subreddit._by_domain(sub_domain) if sub_domain else None
  190. c.site = sr or Default
  191. elif sr_name == 'r':
  192. #reddits
  193. c.site = Sub
  194. else:
  195. try:
  196. if '+' in sr_name:
  197. srs = set()
  198. sr_names = sr_name.split('+')
  199. real_path = sr_name
  200. for sr_name in sr_names:
  201. srs.add(Subreddit._by_name(sr_name))
  202. sr_ids = [sr._id for sr in srs]
  203. c.site = MultiReddit(sr_ids, real_path)
  204. else:
  205. c.site = Subreddit._by_name(sr_name)
  206. except NotFound:
  207. c.site = Default
  208. if chksrname(sr_name):
  209. redirect_to("/categories/create?name=%s" % sr_name)
  210. elif not c.error_page:
  211. abort(404, "not found")
  212. #if we didn't find a subreddit, check for a domain listing
  213. if not sr_name and c.site == Default and domain:
  214. c.site = DomainSR(domain)
  215. if isinstance(c.site, FakeSubreddit):
  216. c.default_sr = True
  217. try:
  218. c.current_or_default_sr = Subreddit._by_name(g.default_sr)
  219. except NotFound:
  220. c.current_or_default_sr = None
  221. else:
  222. c.current_or_default_sr = c.site
  223. # check that the site is available:
  224. if c.site._spam and not c.user_is_admin and not c.error_page:
  225. abort(404, "not found")
  226. def set_content_type():
  227. e = request.environ
  228. c.render_style = e['render_style']
  229. c.response_content_type = e['content_type']
  230. if e.has_key('extension'):
  231. ext = e['extension']
  232. if ext == 'api' or ext.startswith('json'):
  233. c.response_access_control = 'allow <*>'
  234. if ext in ('embed', 'wired'):
  235. c.response_wrappers.append(utils.to_js)
  236. def get_browser_langs():
  237. browser_langs = []
  238. langs = request.environ.get('HTTP_ACCEPT_LANGUAGE')
  239. if langs:
  240. langs = langs.split(',')
  241. browser_langs = []
  242. seen_langs = set()
  243. # extract languages from browser string
  244. for l in langs:
  245. if ';' in l:
  246. l = l.split(';')[0]
  247. if l not in seen_langs:
  248. browser_langs.append(l)
  249. seen_langs.add(l)
  250. if '-' in l:
  251. l = l.split('-')[0]
  252. if l not in seen_langs:
  253. browser_langs.append(l)
  254. seen_langs.add(l)
  255. return browser_langs
  256. def set_host_lang():
  257. # try to grab the language from the domain
  258. host_lang = request.environ.get('reddit-prefer-lang')
  259. if host_lang:
  260. c.host_lang = host_lang
  261. def set_iface_lang():
  262. lang = ['en']
  263. # GET param wins
  264. if c.host_lang:
  265. lang = [c.host_lang]
  266. else:
  267. lang = [c.user.pref_lang]
  268. #choose the first language
  269. c.lang = lang[0]
  270. #then try to overwrite it if we have the translation for another
  271. #one
  272. for l in lang:
  273. try:
  274. h.set_lang(l)
  275. c.lang = l
  276. break
  277. except h.LanguageError:
  278. #we don't have a translation for that language
  279. h.set_lang('en', graceful_fail = True)
  280. #TODO: add exceptions here for rtl languages
  281. if c.lang in ('ar', 'he', 'fa'):
  282. c.lang_rtl = True
  283. def set_content_lang():
  284. if c.user.pref_content_langs != 'all':
  285. c.content_langs = list(c.user.pref_content_langs)
  286. c.content_langs.sort()
  287. else:
  288. c.content_langs = c.user.pref_content_langs
  289. def set_cnameframe():
  290. if (bool(request.params.get(utils.UrlParser.cname_get))
  291. or not request.host.split(":")[0].endswith(g.domain)):
  292. c.cname = True
  293. request.environ['REDDIT_CNAME'] = 1
  294. if request.params.has_key(utils.UrlParser.cname_get):
  295. del request.params[utils.UrlParser.cname_get]
  296. if request.get.has_key(utils.UrlParser.cname_get):
  297. del request.get[utils.UrlParser.cname_get]
  298. c.frameless_cname = request.environ.get('frameless_cname', False)
  299. if hasattr(c.site, 'domain'):
  300. c.authorized_cname = request.environ.get('authorized_cname', False)
  301. def set_colors():
  302. theme_rx = re.compile(r'')
  303. color_rx = re.compile(r'^([a-fA-F0-9]){3}(([a-fA-F0-9]){3})?$')
  304. c.theme = None
  305. if color_rx.match(request.get.get('bgcolor') or ''):
  306. c.bgcolor = request.get.get('bgcolor')
  307. if color_rx.match(request.get.get('bordercolor') or ''):
  308. c.bordercolor = request.get.get('bordercolor')
  309. def set_recent_reddits():
  310. names = read_user_cookie('recent_reddits')
  311. c.recent_reddits = []
  312. if names:
  313. try:
  314. names = filter(None, names.split(','))
  315. srs = Subreddit._by_fullname(names, data = True,
  316. return_dict = False)
  317. # Ensure all the objects returned are Subreddits. Due to the nature
  318. # of _by_fullname its possible to get any type back
  319. c.recent_reddits = filter(lambda x: isinstance(x, Subreddit), names)
  320. except:
  321. pass
  322. def ratelimit_agents():
  323. user_agent = request.user_agent
  324. for s in g.agents:
  325. if s and user_agent and s in user_agent.lower():
  326. key = 'rate_agent_' + s
  327. if cache.get(s):
  328. abort(503, 'service temporarily unavailable')
  329. else:
  330. cache.set(s, 't', time = 1)
  331. #TODO i want to get rid of this function. once the listings in front.py are
  332. #moved into listingcontroller, we shouldn't have a need for this
  333. #anymore
  334. def base_listing(fn):
  335. @validate(num = VLimit('limit'),
  336. after = VByName('after'),
  337. before = VByName('before'),
  338. count = VCount('count'))
  339. def new_fn(self, before, num, **env):
  340. kw = self.build_arg_list(fn, env)
  341. # Multiply the number per page by the per page multiplier for the reddit
  342. if num:
  343. kw['num'] = c.site.posts_per_page_multiplier * num
  344. #turn before into after/reverse
  345. kw['reverse'] = False
  346. if before:
  347. kw['after'] = before
  348. kw['reverse'] = True
  349. return fn(self, **kw)
  350. return new_fn
  351. class RedditController(BaseController):
  352. @staticmethod
  353. def build_arg_list(fn, env):
  354. """given a fn and and environment the builds a keyword argument list
  355. for fn"""
  356. kw = {}
  357. argspec = inspect.getargspec(fn)
  358. # if there is a **kw argument in the fn definition,
  359. # just pass along the environment
  360. if argspec[2]:
  361. kw = env
  362. #else for each entry in the arglist set the value from the environment
  363. else:
  364. #skip self
  365. argnames = argspec[0][1:]
  366. for name in argnames:
  367. if name in env:
  368. kw[name] = env[name]
  369. return kw
  370. def request_key(self):
  371. # note that this references the cookie at request time, not
  372. # the current value of it
  373. cookie_keys = []
  374. for x in cache_affecting_cookies:
  375. cookie_keys.append(request.cookies.get(x,''))
  376. key = ''.join((str(c.lang),
  377. str(c.content_langs),
  378. request.host,
  379. str(c.cname),
  380. str(request.fullpath),
  381. str(c.over18),
  382. ''.join(cookie_keys)))
  383. return key
  384. def cached_response(self):
  385. return c.response
  386. @staticmethod
  387. def login(user, admin = False, rem = False):
  388. c.cookies[g.login_cookie] = Cookie(value = user.make_cookie(admin = admin),
  389. expires = NEVER if rem else None)
  390. @staticmethod
  391. def logout(admin = False):
  392. c.cookies[g.login_cookie] = Cookie(value='')
  393. def pre(self):
  394. g.cache.caches = (LocalCache(),) + g.cache.caches[1:]
  395. #check if user-agent needs a dose of rate-limiting
  396. if not c.error_page:
  397. ratelimit_agents()
  398. # the domain has to be set before Cookies get initialized
  399. set_subreddit()
  400. set_cnameframe()
  401. # populate c.cookies
  402. c.cookies = Cookies()
  403. try:
  404. for k,v in request.cookies.iteritems():
  405. # we can unquote even if it's not quoted
  406. c.cookies[k] = Cookie(value=unquote(v), dirty=False)
  407. except CookieError:
  408. #pylons or one of the associated retarded libraries can't
  409. #handle broken cookies
  410. request.environ['HTTP_COOKIE'] = ''
  411. c.response_wrappers = []
  412. c.errors = ErrorSet()
  413. c.firsttime = firsttime()
  414. (c.user, maybe_admin) = valid_cookie(current_login_cookie())
  415. if c.user:
  416. c.user_is_loggedin = True
  417. else:
  418. c.user = UnloggedUser(get_browser_langs())
  419. c.user._load()
  420. if c.user_is_loggedin:
  421. if not c.user._loaded:
  422. c.user._load()
  423. c.modhash = c.user.modhash()
  424. if request.method.lower() == 'get':
  425. read_click_cookie()
  426. read_mod_cookie()
  427. if hasattr(c.user, 'msgtime') and c.user.msgtime:
  428. c.have_messages = c.user.msgtime
  429. c.user_is_admin = maybe_admin and c.user.name in g.admins
  430. c.user_is_sponsor = c.user_is_admin or c.user.name in g.sponsors
  431. c.over18 = over18()
  432. #set_browser_langs()
  433. set_host_lang()
  434. set_content_type()
  435. set_iface_lang()
  436. set_content_lang()
  437. set_colors()
  438. set_recent_reddits()
  439. # set some environmental variables in case we hit an abort
  440. if not isinstance(c.site, FakeSubreddit):
  441. request.environ['REDDIT_NAME'] = c.site.name
  442. # check if the user has access to this subreddit
  443. if not c.site.can_view(c.user) and not c.error_page:
  444. abort(403, "forbidden")
  445. #check over 18
  446. if (c.site.over_18 and not c.over18 and
  447. request.path not in ("/frame", "/over18")
  448. and c.render_style == 'html'):
  449. return self.intermediate_redirect("/over18")
  450. #check whether to allow custom styles
  451. c.allow_styles = True
  452. if g.css_killswitch:
  453. c.allow_styles = False
  454. #if the preference is set and we're not at a cname
  455. elif not c.user.pref_show_stylesheets and not c.cname:
  456. c.allow_styles = False
  457. #if the site has a cname, but we're not using it
  458. elif c.site.domain and not c.cname:
  459. c.allow_styles = False
  460. #check content cache
  461. if not c.user_is_loggedin:
  462. r = cache.get(self.request_key())
  463. if r and request.method == 'GET':
  464. response = c.response
  465. response.headers = r.headers
  466. response.content = r.content
  467. for x in r.cookies.keys():
  468. if x in cache_affecting_cookies:
  469. cookie = r.cookies[x]
  470. response.set_cookie(key = x,
  471. value = cookie.value,
  472. domain = cookie.get('domain',None),
  473. expires = cookie.get('expires',None),
  474. path = cookie.get('path',None))
  475. response.status_code = r.status_code
  476. request.environ['pylons.routes_dict']['action'] = 'cached_response'
  477. # make sure to carry over the content type
  478. c.response_content_type = r.headers['content-type']
  479. if r.headers.has_key('access-control'):
  480. c.response_access_control = r.headers['access-control']
  481. c.used_cache = True
  482. # response wrappers have already been applied before cache write
  483. c.response_wrappers = []
  484. def post(self):
  485. response = c.response
  486. content = response.content
  487. if isinstance(content, (list, tuple)):
  488. content = ''.join(content)
  489. for w in c.response_wrappers:
  490. content = w(content)
  491. response.content = content
  492. if c.response_content_type:
  493. response.headers['Content-Type'] = c.response_content_type
  494. if c.response_access_control:
  495. c.response.headers['Access-Control'] = c.response_access_control
  496. if c.user_is_loggedin and 'Cache-Control' not in response.headers:
  497. response.headers['Cache-Control'] = 'no-cache'
  498. response.headers['Pragma'] = 'no-cache'
  499. # send cookies
  500. if not c.used_cache:
  501. # if we used the cache, these cookies should be set by the
  502. # cached response object instead
  503. for k,v in c.cookies.iteritems():
  504. if v.dirty:
  505. response.set_cookie(key = k,
  506. value = quote(v.value),
  507. domain = v.domain,
  508. expires = v.expires)
  509. #return
  510. #set content cache
  511. if (g.page_cache_time
  512. and request.method == 'GET'
  513. and not c.user_is_loggedin
  514. and not c.used_cache
  515. and response.content and response.content[0]):
  516. config.cache.set(self.request_key(),
  517. response,
  518. g.page_cache_time)
  519. def check_modified(self, thing, action):
  520. if c.user_is_loggedin:
  521. return
  522. date = utils.is_modified_since(thing, action, request.if_modified_since)
  523. if date is True:
  524. abort(304, 'not modified')
  525. else:
  526. c.response.headers['Last-Modified'] = http_utils.http_date_str(date)
  527. def abort404(self):
  528. abort(404, "not found")
  529. def sendpng(self, string):
  530. c.response_content_type = 'image/png'
  531. c.response.content = string
  532. return c.response
  533. def sendstring(self,string):
  534. '''sends a string and automatically escapes &, < and > to make sure no code injection happens'''
  535. c.response.headers['Content-Type'] = 'text/html; charset=UTF-8'
  536. c.response.content = filters.websafe_json(string)
  537. return c.response
  538. def update_qstring(self, dict):
  539. merged = copy(request.get)
  540. merged.update(dict)
  541. return request.path + utils.query_string(merged)