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

/r2/r2/controllers/front.py

https://github.com/wangmxf/lesswrong
Python | 642 lines | 583 code | 23 blank | 36 comment | 28 complexity | 49b1fe1ebfa774f8899755824cbc8f11 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 validator import *
  23. from pylons.i18n import _, ungettext
  24. from reddit_base import RedditController, base_listing
  25. from api import link_listing_by_url
  26. from r2 import config
  27. from r2.models import *
  28. from r2.lib.pages import *
  29. from r2.lib.menus import *
  30. from r2.lib.filters import _force_unicode
  31. from r2.lib.utils import to36, sanitize_url, check_cheating, title_to_url, query_string, UrlParser
  32. from r2.lib.template_helpers import get_domain
  33. from r2.lib.emailer import has_opted_out, Email
  34. from r2.lib.db.operators import desc
  35. from r2.lib.strings import strings
  36. from r2.lib.solrsearch import RelatedSearchQuery, SubredditSearchQuery, LinkSearchQuery
  37. import r2.lib.db.thing as thing
  38. from listingcontroller import ListingController
  39. from pylons import c, request
  40. import random as rand
  41. import re
  42. import time as time_module
  43. from urllib import quote_plus
  44. class FrontController(RedditController):
  45. @validate(article = VLink('article'),
  46. comment = VCommentID('comment'))
  47. def GET_oldinfo(self, article, type, dest, rest=None, comment=''):
  48. """Legacy: supporting permalink pages from '06,
  49. and non-search-engine-friendly links"""
  50. if not (dest in ('comments','related','details')):
  51. dest = 'comments'
  52. if type == 'ancient':
  53. #this could go in config, but it should never change
  54. max_link_id = 10000000
  55. new_id = max_link_id - int(article._id)
  56. return self.redirect('/info/' + to36(new_id) + '/' + rest)
  57. if type == 'old':
  58. new_url = "/%s/%s/%s" % \
  59. (dest, article._id36,
  60. quote_plus(title_to_url(article.title).encode('utf-8')))
  61. if not c.default_sr:
  62. new_url = "/r/%s%s" % (c.site.name, new_url)
  63. if comment:
  64. new_url = new_url + "/%s" % comment._id36
  65. if c.extension:
  66. new_url = new_url + "/.%s" % c.extension
  67. new_url = new_url + query_string(request.get)
  68. # redirect should be smarter and handle extensions, etc.
  69. return self.redirect(new_url, code=301)
  70. def GET_random(self):
  71. """The Serendipity button"""
  72. n = rand.randint(0, 9)
  73. sort = 'new' if n > 5 else 'hot'
  74. links = c.site.get_links(sort, 'all')
  75. if isinstance(links, thing.Query):
  76. links._limit = 25
  77. links = [x._fullname for x in links]
  78. else:
  79. links = links[:25]
  80. if links:
  81. name = links[rand.randint(0, min(24, len(links)-1))]
  82. link = Link._by_fullname(name, data = True)
  83. return self.redirect(link.url)
  84. else:
  85. return self.redirect('/')
  86. def GET_password(self):
  87. """The 'what is my password' page"""
  88. return BoringPage(_("Password"), content=Password()).render()
  89. @validate(user = VCacheKey('reset', ('key', 'name')),
  90. key = nop('key'))
  91. def GET_resetpassword(self, user, key):
  92. """page hit once a user has been sent a password reset email
  93. to verify their identity before allowing them to update their
  94. password."""
  95. done = False
  96. if not key and request.referer:
  97. referer_path = request.referer.split(g.domain)[-1]
  98. done = referer_path.startswith(request.fullpath)
  99. elif not user:
  100. return self.abort404()
  101. return BoringPage(_("Reset password"),
  102. content=ResetPassword(key=key, done=done)).render()
  103. @validate(VAdmin(),
  104. article = VLink('article'))
  105. def GET_details(self, article):
  106. """The (now depricated) details page. Content on this page
  107. has been subsubmed by the presence of the LinkInfoBar on the
  108. rightbox, so it is only useful for Admin-only wizardry."""
  109. return DetailsPage(link = article).render()
  110. @validate(article = VLink('article'),
  111. comment = VCommentID('comment'),
  112. context = VInt('context', min = 0, max = 8),
  113. sort = VMenu('controller', CommentSortMenu),
  114. num_comments = VMenu('controller', NumCommentsMenu))
  115. def GET_comments(self, article, comment, context, sort, num_comments):
  116. """Comment page for a given 'article'."""
  117. if comment and comment.link_id != article._id:
  118. return self.abort404()
  119. if not c.default_sr and c.site._id != article.sr_id:
  120. return self.redirect(article.make_permalink_slow(), 301)
  121. # moderator is either reddit's moderator or an admin
  122. is_moderator = c.user_is_loggedin and c.site.is_moderator(c.user) or c.user_is_admin
  123. if article._spam and not is_moderator:
  124. return self.abort404()
  125. if not article.subreddit_slow.can_view(c.user):
  126. abort(403, 'forbidden')
  127. #check for 304
  128. self.check_modified(article, 'comments')
  129. # if there is a focal comment, communicate down to comment_skeleton.html who
  130. # that will be
  131. if comment:
  132. c.focal_comment = comment._id36
  133. # check if we just came from the submit page
  134. infotext = None
  135. if request.get.get('already_submitted'):
  136. infotext = strings.already_submitted % article.resubmit_link()
  137. check_cheating('comments')
  138. # figure out number to show based on the menu
  139. user_num = c.user.pref_num_comments or g.num_comments
  140. num = g.max_comments if num_comments == 'true' else user_num
  141. # Override sort if the link has a default set
  142. if hasattr(article, 'comment_sort_order'):
  143. sort = article.comment_sort_order
  144. builder = CommentBuilder(article, CommentSortMenu.operator(sort),
  145. comment, context)
  146. listing = NestedListing(builder, num = num,
  147. parent_name = article._fullname)
  148. displayPane = PaneStack()
  149. # if permalink page, add that message first to the content
  150. if comment:
  151. permamessage = PermalinkMessage(
  152. comment.make_anchored_permalink(
  153. context = context + 1 if context else 1,
  154. anchor = 'comments'
  155. ),
  156. has_more_comments = hasattr(comment, 'parent_id')
  157. )
  158. displayPane.append(permamessage)
  159. # insert reply box only for logged in user
  160. if c.user_is_loggedin and article.subreddit_slow.can_comment(c.user):
  161. displayPane.append(CommentReplyBox())
  162. #no comment box for permalinks
  163. if not comment:
  164. displayPane.append(CommentReplyBox(link_name =
  165. article._fullname))
  166. # finally add the comment listing
  167. displayPane.append(listing.listing())
  168. loc = None if c.focal_comment or context is not None else 'comments'
  169. if article.comments_enabled:
  170. sort_menu = CommentSortMenu(default = sort, type='dropdown2')
  171. if hasattr(article, 'comment_sort_order'):
  172. sort_menu.enabled = False
  173. nav_menus = [sort_menu,
  174. NumCommentsMenu(article.num_comments,
  175. default=num_comments)]
  176. content = CommentListing(
  177. content = displayPane,
  178. num_comments = article.num_comments,
  179. nav_menus = nav_menus,
  180. )
  181. else:
  182. content = PaneStack()
  183. is_canonical = article.canonical_url.endswith(_force_unicode(request.path)) and not request.GET
  184. res = LinkInfoPage(link = article, comment = comment,
  185. content = content,
  186. infotext = infotext,
  187. is_canonical = is_canonical).render()
  188. if c.user_is_loggedin:
  189. article._click(c.user)
  190. return res
  191. @validate(VUser(),
  192. location = nop("location"))
  193. def GET_prefs(self, location=''):
  194. """Preference page"""
  195. content = None
  196. infotext = None
  197. if not location or location == 'options':
  198. content = PrefOptions(done=request.get.get('done'))
  199. elif location == 'friends':
  200. content = PaneStack()
  201. infotext = strings.friends % Friends.path
  202. content.append(FriendList())
  203. elif location == 'update':
  204. content = PrefUpdate()
  205. elif location == 'delete':
  206. content = PrefDelete()
  207. return PrefsPage(content = content, infotext=infotext).render()
  208. @validate(VAdmin(),
  209. name = nop('name'))
  210. def GET_newreddit(self, name):
  211. """Create a reddit form"""
  212. title = _('Create a category')
  213. content=CreateSubreddit(name = name or '', listings = ListingController.listing_names())
  214. res = FormPage(_("Create a category"),
  215. content = content,
  216. ).render()
  217. return res
  218. def GET_stylesheet(self):
  219. if hasattr(c.site,'stylesheet_contents') and not g.css_killswitch:
  220. self.check_modified(c.site,'stylesheet_contents')
  221. c.response_content_type = 'text/css'
  222. c.response.content = c.site.stylesheet_contents
  223. return c.response
  224. else:
  225. return self.abort404()
  226. @base_listing
  227. @validate(location = nop('location'))
  228. def GET_editreddit(self, location, num, after, reverse, count):
  229. """Edit reddit form."""
  230. if isinstance(c.site, FakeSubreddit):
  231. return self.abort404()
  232. # moderator is either reddit's moderator or an admin
  233. is_moderator = c.user_is_loggedin and c.site.is_moderator(c.user) or c.user_is_admin
  234. if is_moderator and location == 'edit':
  235. pane = CreateSubreddit(site = c.site, listings = ListingController.listing_names())
  236. elif location == 'moderators':
  237. pane = ModList(editable = is_moderator)
  238. elif location == 'editors':
  239. pane = EditorList(editable = c.user_is_admin)
  240. elif is_moderator and location == 'banned':
  241. pane = BannedList(editable = is_moderator)
  242. elif location == 'contributors' and c.site.type != 'public':
  243. pane = ContributorList(editable = is_moderator)
  244. elif (location == 'stylesheet'
  245. and c.site.can_change_stylesheet(c.user)
  246. and not g.css_killswitch):
  247. if hasattr(c.site,'stylesheet_contents_user') and c.site.stylesheet_contents_user:
  248. stylesheet_contents = c.site.stylesheet_contents_user
  249. elif hasattr(c.site,'stylesheet_contents') and c.site.stylesheet_contents:
  250. stylesheet_contents = c.site.stylesheet_contents
  251. else:
  252. stylesheet_contents = ''
  253. pane = SubredditStylesheet(site = c.site,
  254. stylesheet_contents = stylesheet_contents)
  255. elif is_moderator and location == 'reports':
  256. links = Link._query(Link.c.reported != 0,
  257. Link.c._spam == False)
  258. comments = Comment._query(Comment.c.reported != 0,
  259. Comment.c._spam == False)
  260. query = thing.Merge((links, comments),
  261. Link.c.sr_id == c.site._id,
  262. sort = desc('_date'),
  263. data = True)
  264. builder = QueryBuilder(query, num = num, after = after,
  265. count = count, reverse = reverse,
  266. wrap = ListingController.builder_wrapper)
  267. listing = LinkListing(builder)
  268. pane = listing.listing()
  269. elif is_moderator and location == 'spam':
  270. links = Link._query(Link.c._spam == True)
  271. comments = Comment._query(Comment.c._spam == True)
  272. query = thing.Merge((links, comments),
  273. Link.c.sr_id == c.site._id,
  274. sort = desc('_date'),
  275. data = True)
  276. builder = QueryBuilder(query, num = num, after = after,
  277. count = count, reverse = reverse,
  278. wrap = ListingController.builder_wrapper)
  279. listing = LinkListing(builder)
  280. pane = listing.listing()
  281. else:
  282. return self.abort404()
  283. return EditReddit(content = pane).render()
  284. # def GET_stats(self):
  285. # """The stats page."""
  286. # return BoringPage(_("Stats"), content = UserStats()).render()
  287. # filter for removing punctuation which could be interpreted as lucene syntax
  288. related_replace_regex = re.compile('[?\\&|!{}+~^()":*-]+')
  289. related_replace_with = ' '
  290. @base_listing
  291. @validate(article = VLink('article'))
  292. def GET_related(self, num, article, after, reverse, count):
  293. """Related page: performs a search using title of article as
  294. the search query."""
  295. title = c.site.name + ((': ' + article.title) if hasattr(article, 'title') else '')
  296. query = self.related_replace_regex.sub(self.related_replace_with,
  297. article.title)
  298. if len(query) > 1024:
  299. # could get fancier and break this into words, but titles
  300. # longer than this are typically ascii art anyway
  301. query = query[0:1023]
  302. q = RelatedSearchQuery(query, ignore = [article._fullname])
  303. num, t, pane = self._search(q,
  304. num = num, after = after, reverse = reverse,
  305. count = count)
  306. return LinkInfoPage(link = article, content = pane).render()
  307. @base_listing
  308. @validate(query = nop('q'))
  309. def GET_search_reddits(self, query, reverse, after, count, num):
  310. """Search reddits by title and description."""
  311. q = SubredditSearchQuery(query)
  312. num, t, spane = self._search(q, num = num, reverse = reverse,
  313. after = after, count = count)
  314. res = SubredditsPage(content=spane,
  315. prev_search = query,
  316. elapsed_time = t,
  317. num_results = num,
  318. title = _("Search results")).render()
  319. return res
  320. verify_langs_regex = re.compile(r"^[a-z][a-z](,[a-z][a-z])*$")
  321. @base_listing
  322. @validate(query = nop('q'),
  323. time = VMenu('action', TimeMenu, remember = False),
  324. sort = VMenu('sort', SearchSortMenu, remember = False),
  325. langs = nop('langs'))
  326. def GET_search(self, query, num, time, reverse, after, count, langs, sort):
  327. """Search links page."""
  328. if query and '.' in query:
  329. url = sanitize_url(query, require_scheme = True)
  330. if url:
  331. return self.redirect("/submit" + query_string({'url':url}))
  332. if langs and self.verify_langs_regex.match(langs):
  333. langs = langs.split(',')
  334. else:
  335. langs = c.content_langs
  336. subreddits = None
  337. authors = None
  338. if c.site == subreddit.Friends and c.user_is_loggedin and c.user.friends:
  339. authors = c.user.friends
  340. elif isinstance(c.site, MultiReddit):
  341. subreddits = c.site.sr_ids
  342. elif not isinstance(c.site, FakeSubreddit):
  343. subreddits = [c.site._id]
  344. q = LinkSearchQuery(q = query, timerange = time, langs = langs,
  345. subreddits = subreddits, authors = authors,
  346. sort = SearchSortMenu.operator(sort))
  347. num, t, spane = self._search(q, num = num, after = after, reverse = reverse,
  348. count = count)
  349. if not isinstance(c.site,FakeSubreddit) and not c.cname:
  350. all_reddits_link = "%s/search%s" % (subreddit.All.path,
  351. query_string({'q': query}))
  352. d = {'reddit_name': c.site.name,
  353. 'reddit_link': "http://%s/"%get_domain(cname = c.cname),
  354. 'all_reddits_link': all_reddits_link}
  355. infotext = strings.searching_a_reddit % d
  356. else:
  357. infotext = None
  358. res = SearchPage(_('Search results'), query, t, num, content=spane,
  359. nav_menus = [TimeMenu(default = time),
  360. SearchSortMenu(default=sort)],
  361. infotext = infotext).render()
  362. return res
  363. def _search(self, query_obj, num, after, reverse, count=0):
  364. """Helper function for interfacing with search. Basically a
  365. thin wrapper for SearchBuilder."""
  366. builder = SearchBuilder(query_obj,
  367. after = after, num = num, reverse = reverse,
  368. count = count,
  369. wrap = ListingController.builder_wrapper)
  370. listing = LinkListing(builder, show_nums=True)
  371. # have to do it in two steps since total_num and timing are only
  372. # computed after fetch_more
  373. res = listing.listing()
  374. timing = time_module.time() - builder.start_time
  375. return builder.total_num, timing, res
  376. def GET_search_results(self):
  377. return GoogleSearchResults(_('Search results')).render()
  378. def GET_login(self):
  379. """The /login form. No link to this page exists any more on
  380. the site (all actions invoking it now go through the login
  381. cover). However, this page is still used for logging the user
  382. in during submission or voting from the bookmarklets."""
  383. # dest is the location to redirect to upon completion
  384. dest = request.get.get('dest','') or request.referer or '/'
  385. return LoginPage(dest = dest).render()
  386. def GET_logout(self):
  387. """wipe login cookie and redirect to front page."""
  388. self.logout()
  389. return self.redirect('/')
  390. @validate(VUser())
  391. def GET_adminon(self):
  392. """Enable admin interaction with site"""
  393. #check like this because c.user_is_admin is still false
  394. if not c.user.name in g.admins:
  395. return self.abort404()
  396. self.login(c.user, admin = True)
  397. dest = request.referer or '/'
  398. return self.redirect(dest)
  399. @validate(VAdmin())
  400. def GET_adminoff(self):
  401. """disable admin interaction with site."""
  402. if not c.user.name in g.admins:
  403. return self.abort404()
  404. self.login(c.user, admin = False)
  405. dest = request.referer or '/'
  406. return self.redirect(dest)
  407. def GET_validuser(self):
  408. """checks login cookie to verify that a user is logged in and
  409. returns their user name"""
  410. c.response_content_type = 'text/plain'
  411. if c.user_is_loggedin:
  412. return c.user.name
  413. else:
  414. return ''
  415. @validate(VUser(),
  416. can_submit = VSRSubmitPage(),
  417. url = VRequired('url', None),
  418. title = VRequired('title', None),
  419. tags = VTags('tags'))
  420. def GET_submit(self, can_submit, url, title, tags):
  421. """Submit form."""
  422. if not can_submit:
  423. return BoringPage(_("Not Enough Karma"),
  424. infotext="You do not have enough karma to post.",
  425. content=NotEnoughKarmaToPost()).render()
  426. if url and not request.get.get('resubmit'):
  427. # check to see if the url has already been submitted
  428. listing = link_listing_by_url(url)
  429. redirect_link = None
  430. if listing.things:
  431. # if there is only one submission, the operation is clear
  432. if len(listing.things) == 1:
  433. redirect_link = listing.things[0]
  434. # if there is more than one, check the users' subscriptions
  435. else:
  436. subscribed = [l for l in listing.things
  437. if c.user_is_loggedin
  438. and l.subreddit.is_subscriber_defaults(c.user)]
  439. #if there is only 1 link to be displayed, just go there
  440. if len(subscribed) == 1:
  441. redirect_link = subscribed[0]
  442. else:
  443. infotext = strings.multiple_submitted % \
  444. listing.things[0].resubmit_link()
  445. res = BoringPage(_("Seen it"),
  446. content = listing,
  447. infotext = infotext).render()
  448. return res
  449. # we've found a link already. Redirect to its permalink page
  450. if redirect_link:
  451. return self.redirect(redirect_link.already_submitted_link)
  452. captcha = Captcha(tabular=False) if c.user.needs_captcha() else None
  453. srs = Subreddit.submit_sr(c.user)
  454. # Set the default sr to the user's draft when creating a new article
  455. try:
  456. sr = Subreddit._by_name(c.user.draft_sr_name)
  457. except NotFound:
  458. sr = None
  459. return FormPage(_("Submit Article"),
  460. content=NewLink(title=title or '',
  461. subreddits = srs,
  462. tags=tags,
  463. sr_id = sr._id if sr else None,
  464. captcha=captcha)).render()
  465. @validate(VUser(),
  466. VSRSubmitPage(),
  467. article = VSubmitLink('article'))
  468. def GET_editarticle(self, article):
  469. author = Account._byID(article.author_id, data=True)
  470. subreddits = Subreddit.submit_sr(author)
  471. article_sr = Subreddit._byID(article.sr_id)
  472. if c.user_is_admin:
  473. # Add this admins subreddits to the list
  474. subreddits = list(set(subreddits).union([article_sr] + Subreddit.submit_sr(c.user)))
  475. elif article_sr.is_editor(c.user) and c.user != author:
  476. # An editor can save to the current subreddit irrspective of the original author's karma
  477. subreddits = [sr for sr in Subreddit.submit_sr(c.user) if sr.is_editor(c.user)]
  478. captcha = Captcha(tabular=False) if c.user.needs_captcha() else None
  479. return FormPage(_("Edit article"),
  480. content=EditLink(article, subreddits=subreddits, tags=article.tag_names(), captcha=captcha)).render()
  481. def _render_opt_in_out(self, msg_hash, leave):
  482. """Generates the form for an optin/optout page"""
  483. email = Email.handler.get_recipient(msg_hash)
  484. if not email:
  485. return self.abort404()
  486. sent = (has_opted_out(email) == leave)
  487. return BoringPage(_("Opt out") if leave else _("Welcome back"),
  488. content = OptOut(email = email, leave = leave,
  489. sent = sent,
  490. msg_hash = msg_hash)).render()
  491. @validate(msg_hash = nop('x'))
  492. def GET_optout(self, msg_hash):
  493. """handles /mail/optout to add an email to the optout mailing
  494. list. The actual email addition comes from the user posting
  495. the subsequently rendered form and is handled in
  496. ApiController.POST_optout."""
  497. return self._render_opt_in_out(msg_hash, True)
  498. @validate(msg_hash = nop('x'))
  499. def GET_optin(self, msg_hash):
  500. """handles /mail/optin to remove an email address from the
  501. optout list. The actual email removal comes from the user
  502. posting the subsequently rendered form and is handled in
  503. ApiController.POST_optin."""
  504. return self._render_opt_in_out(msg_hash, False)
  505. def GET_frame(self):
  506. """used for cname support. makes a frame and
  507. puts the proper url as the frame source"""
  508. sub_domain = request.environ.get('sub_domain')
  509. original_path = request.environ.get('original_path')
  510. sr = Subreddit._by_domain(sub_domain)
  511. return Cnameframe(original_path, sr, sub_domain).render()
  512. def GET_framebuster(self):
  513. if c.site.domain and c.user_is_loggedin:
  514. u = UrlParser(c.site.path + "/frame")
  515. u.put_in_frame()
  516. c.cname = True
  517. return self.redirect(u.unparse())
  518. return "fail"
  519. def GET_catchall(self):
  520. return self.abort404()
  521. @validate(VUser(),
  522. article = VSubmitLink('article', redirect=False))
  523. def GET_imagebrowser(self, article):
  524. return ImageBrowser(article).render()
  525. #XXX These 'redirect pages' should be generalised
  526. def _redirect_to_link(self, link_id):
  527. try:
  528. link = Link._byID(int(link_id, 36), data=True)
  529. except NotFound:
  530. return self.abort404()
  531. return self.redirect(link.make_permalink(subreddit.Default))
  532. def GET_about(self):
  533. try:
  534. return self._redirect_to_link(g.about_post_id)
  535. except AttributeError:
  536. return self.abort404()
  537. def GET_issues(self):
  538. try:
  539. return self._redirect_to_link(g.issues_post_id)
  540. except AttributeError:
  541. return self.abort404()
  542. def GET_blank(self):
  543. return ''